@corenel/cli 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +606 -72
  2. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -1500,7 +1500,7 @@ var require_stringify = __commonJS({
1500
1500
  props.push(doc.directives.tagString(tag));
1501
1501
  return props.join(" ");
1502
1502
  }
1503
- function stringify2(item, ctx, onComment, onChompKeep) {
1503
+ function stringify3(item, ctx, onComment, onChompKeep) {
1504
1504
  if (identity.isPair(item))
1505
1505
  return item.toString(ctx, onComment, onChompKeep);
1506
1506
  if (identity.isAlias(item)) {
@@ -1529,7 +1529,7 @@ var require_stringify = __commonJS({
1529
1529
  ${ctx.indent}${str5}`;
1530
1530
  }
1531
1531
  exports.createStringifyContext = createStringifyContext;
1532
- exports.stringify = stringify2;
1532
+ exports.stringify = stringify3;
1533
1533
  }
1534
1534
  });
1535
1535
 
@@ -1539,7 +1539,7 @@ var require_stringifyPair = __commonJS({
1539
1539
  "use strict";
1540
1540
  var identity = require_identity();
1541
1541
  var Scalar = require_Scalar();
1542
- var stringify2 = require_stringify();
1542
+ var stringify3 = require_stringify();
1543
1543
  var stringifyComment = require_stringifyComment();
1544
1544
  function stringifyPair({ key, value }, ctx, onComment, onChompKeep) {
1545
1545
  const { allNullValues, doc, indent, indentStep, options: { commentString, indentSeq, simpleKeys } } = ctx;
@@ -1561,7 +1561,7 @@ var require_stringifyPair = __commonJS({
1561
1561
  });
1562
1562
  let keyCommentDone = false;
1563
1563
  let chompKeep = false;
1564
- let str5 = stringify2.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true);
1564
+ let str5 = stringify3.stringify(key, ctx, () => keyCommentDone = true, () => chompKeep = true);
1565
1565
  if (!explicitKey && !ctx.inFlow && str5.length > 1024) {
1566
1566
  if (simpleKeys)
1567
1567
  throw new Error("With simple keys, single line scalar must not span more than 1024 characters");
@@ -1613,7 +1613,7 @@ ${indent}:`;
1613
1613
  ctx.indent = ctx.indent.substring(2);
1614
1614
  }
1615
1615
  let valueCommentDone = false;
1616
- const valueStr = stringify2.stringify(value, ctx, () => valueCommentDone = true, () => chompKeep = true);
1616
+ const valueStr = stringify3.stringify(value, ctx, () => valueCommentDone = true, () => chompKeep = true);
1617
1617
  let ws = " ";
1618
1618
  if (keyComment || vsb || vcb) {
1619
1619
  ws = vsb ? "\n" : "";
@@ -1754,7 +1754,7 @@ var require_addPairToJSMap = __commonJS({
1754
1754
  "use strict";
1755
1755
  var log = require_log();
1756
1756
  var merge = require_merge();
1757
- var stringify2 = require_stringify();
1757
+ var stringify3 = require_stringify();
1758
1758
  var identity = require_identity();
1759
1759
  var toJS = require_toJS();
1760
1760
  function addPairToJSMap(ctx, map, { key, value }) {
@@ -1790,7 +1790,7 @@ var require_addPairToJSMap = __commonJS({
1790
1790
  if (typeof jsKey !== "object")
1791
1791
  return String(jsKey);
1792
1792
  if (identity.isNode(key) && ctx?.doc) {
1793
- const strCtx = stringify2.createStringifyContext(ctx.doc, {});
1793
+ const strCtx = stringify3.createStringifyContext(ctx.doc, {});
1794
1794
  strCtx.anchors = /* @__PURE__ */ new Set();
1795
1795
  for (const node of ctx.anchors.keys())
1796
1796
  strCtx.anchors.add(node.anchor);
@@ -1857,12 +1857,12 @@ var require_stringifyCollection = __commonJS({
1857
1857
  "../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyCollection.js"(exports) {
1858
1858
  "use strict";
1859
1859
  var identity = require_identity();
1860
- var stringify2 = require_stringify();
1860
+ var stringify3 = require_stringify();
1861
1861
  var stringifyComment = require_stringifyComment();
1862
1862
  function stringifyCollection(collection, ctx, options) {
1863
1863
  const flow = ctx.inFlow ?? collection.flow;
1864
- const stringify3 = flow ? stringifyFlowCollection : stringifyBlockCollection;
1865
- return stringify3(collection, ctx, options);
1864
+ const stringify4 = flow ? stringifyFlowCollection : stringifyBlockCollection;
1865
+ return stringify4(collection, ctx, options);
1866
1866
  }
1867
1867
  function stringifyBlockCollection({ comment, items }, ctx, { blockItemPrefix, flowChars, itemIndent, onChompKeep, onComment }) {
1868
1868
  const { indent, options: { commentString } } = ctx;
@@ -1887,7 +1887,7 @@ var require_stringifyCollection = __commonJS({
1887
1887
  }
1888
1888
  }
1889
1889
  chompKeep = false;
1890
- let str6 = stringify2.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true);
1890
+ let str6 = stringify3.stringify(item, itemCtx, () => comment2 = null, () => chompKeep = true);
1891
1891
  if (comment2)
1892
1892
  str6 += stringifyComment.lineComment(str6, itemIndent, commentString(comment2));
1893
1893
  if (chompKeep && comment2)
@@ -1954,7 +1954,7 @@ ${indent}${line}` : "\n";
1954
1954
  }
1955
1955
  if (comment)
1956
1956
  reqNewline = true;
1957
- let str5 = stringify2.stringify(item, itemCtx, () => comment = null);
1957
+ let str5 = stringify3.stringify(item, itemCtx, () => comment = null);
1958
1958
  reqNewline || (reqNewline = lines.length > linesAtValue || str5.includes("\n"));
1959
1959
  if (i < items.length - 1) {
1960
1960
  str5 += ",";
@@ -3315,7 +3315,7 @@ var require_stringifyDocument = __commonJS({
3315
3315
  "../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/stringify/stringifyDocument.js"(exports) {
3316
3316
  "use strict";
3317
3317
  var identity = require_identity();
3318
- var stringify2 = require_stringify();
3318
+ var stringify3 = require_stringify();
3319
3319
  var stringifyComment = require_stringifyComment();
3320
3320
  function stringifyDocument(doc, options) {
3321
3321
  const lines = [];
@@ -3330,7 +3330,7 @@ var require_stringifyDocument = __commonJS({
3330
3330
  }
3331
3331
  if (hasDirectives)
3332
3332
  lines.push("---");
3333
- const ctx = stringify2.createStringifyContext(doc, options);
3333
+ const ctx = stringify3.createStringifyContext(doc, options);
3334
3334
  const { commentString } = ctx.options;
3335
3335
  if (doc.commentBefore) {
3336
3336
  if (lines.length !== 1)
@@ -3352,7 +3352,7 @@ var require_stringifyDocument = __commonJS({
3352
3352
  contentComment = doc.contents.comment;
3353
3353
  }
3354
3354
  const onChompKeep = contentComment ? void 0 : () => chompKeep = true;
3355
- let body = stringify2.stringify(doc.contents, ctx, () => contentComment = null, onChompKeep);
3355
+ let body = stringify3.stringify(doc.contents, ctx, () => contentComment = null, onChompKeep);
3356
3356
  if (contentComment)
3357
3357
  body += stringifyComment.lineComment(body, "", commentString(contentComment));
3358
3358
  if ((body[0] === "|" || body[0] === ">") && lines[lines.length - 1] === "---") {
@@ -3360,7 +3360,7 @@ var require_stringifyDocument = __commonJS({
3360
3360
  } else
3361
3361
  lines.push(body);
3362
3362
  } else {
3363
- lines.push(stringify2.stringify(doc.contents, ctx));
3363
+ lines.push(stringify3.stringify(doc.contents, ctx));
3364
3364
  }
3365
3365
  if (doc.directives?.docEnd) {
3366
3366
  if (doc.comment) {
@@ -5490,7 +5490,7 @@ var require_cst_scalar = __commonJS({
5490
5490
  var require_cst_stringify = __commonJS({
5491
5491
  "../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/parse/cst-stringify.js"(exports) {
5492
5492
  "use strict";
5493
- var stringify2 = (cst) => "type" in cst ? stringifyToken(cst) : stringifyItem(cst);
5493
+ var stringify3 = (cst) => "type" in cst ? stringifyToken(cst) : stringifyItem(cst);
5494
5494
  function stringifyToken(token) {
5495
5495
  switch (token.type) {
5496
5496
  case "block-scalar": {
@@ -5543,7 +5543,7 @@ var require_cst_stringify = __commonJS({
5543
5543
  res += stringifyToken(value);
5544
5544
  return res;
5545
5545
  }
5546
- exports.stringify = stringify2;
5546
+ exports.stringify = stringify3;
5547
5547
  }
5548
5548
  });
5549
5549
 
@@ -7245,7 +7245,7 @@ var require_public_api = __commonJS({
7245
7245
  }
7246
7246
  return doc;
7247
7247
  }
7248
- function parse2(src, reviver, options) {
7248
+ function parse3(src, reviver, options) {
7249
7249
  let _reviver = void 0;
7250
7250
  if (typeof reviver === "function") {
7251
7251
  _reviver = reviver;
@@ -7264,7 +7264,7 @@ var require_public_api = __commonJS({
7264
7264
  }
7265
7265
  return doc.toJS(Object.assign({ reviver: _reviver }, options));
7266
7266
  }
7267
- function stringify2(value, replacer, options) {
7267
+ function stringify3(value, replacer, options) {
7268
7268
  let _replacer = null;
7269
7269
  if (typeof replacer === "function" || Array.isArray(replacer)) {
7270
7270
  _replacer = replacer;
@@ -7286,10 +7286,10 @@ var require_public_api = __commonJS({
7286
7286
  return value.toString(options);
7287
7287
  return new Document.Document(value, _replacer, options).toString(options);
7288
7288
  }
7289
- exports.parse = parse2;
7289
+ exports.parse = parse3;
7290
7290
  exports.parseAllDocuments = parseAllDocuments;
7291
7291
  exports.parseDocument = parseDocument;
7292
- exports.stringify = stringify2;
7292
+ exports.stringify = stringify3;
7293
7293
  }
7294
7294
  });
7295
7295
 
@@ -7812,7 +7812,7 @@ var require_parse = __commonJS({
7812
7812
  "../../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/parse.js"(exports, module) {
7813
7813
  "use strict";
7814
7814
  var SemVer = require_semver();
7815
- var parse2 = (version, options, throwErrors = false) => {
7815
+ var parse3 = (version, options, throwErrors = false) => {
7816
7816
  if (version instanceof SemVer) {
7817
7817
  return version;
7818
7818
  }
@@ -7825,7 +7825,7 @@ var require_parse = __commonJS({
7825
7825
  throw er;
7826
7826
  }
7827
7827
  };
7828
- module.exports = parse2;
7828
+ module.exports = parse3;
7829
7829
  }
7830
7830
  });
7831
7831
 
@@ -7833,9 +7833,9 @@ var require_parse = __commonJS({
7833
7833
  var require_valid = __commonJS({
7834
7834
  "../../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/valid.js"(exports, module) {
7835
7835
  "use strict";
7836
- var parse2 = require_parse();
7836
+ var parse3 = require_parse();
7837
7837
  var valid = (version, options) => {
7838
- const v = parse2(version, options);
7838
+ const v = parse3(version, options);
7839
7839
  return v ? v.version : null;
7840
7840
  };
7841
7841
  module.exports = valid;
@@ -7846,9 +7846,9 @@ var require_valid = __commonJS({
7846
7846
  var require_clean = __commonJS({
7847
7847
  "../../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/clean.js"(exports, module) {
7848
7848
  "use strict";
7849
- var parse2 = require_parse();
7849
+ var parse3 = require_parse();
7850
7850
  var clean = (version, options) => {
7851
- const s = parse2(version.trim().replace(/^[=v]+/, ""), options);
7851
+ const s = parse3(version.trim().replace(/^[=v]+/, ""), options);
7852
7852
  return s ? s.version : null;
7853
7853
  };
7854
7854
  module.exports = clean;
@@ -7883,10 +7883,10 @@ var require_inc = __commonJS({
7883
7883
  var require_diff = __commonJS({
7884
7884
  "../../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/diff.js"(exports, module) {
7885
7885
  "use strict";
7886
- var parse2 = require_parse();
7886
+ var parse3 = require_parse();
7887
7887
  var diff = (version1, version2) => {
7888
- const v1 = parse2(version1, null, true);
7889
- const v2 = parse2(version2, null, true);
7888
+ const v1 = parse3(version1, null, true);
7889
+ const v2 = parse3(version2, null, true);
7890
7890
  const comparison = v1.compare(v2);
7891
7891
  if (comparison === 0) {
7892
7892
  return null;
@@ -7957,9 +7957,9 @@ var require_patch = __commonJS({
7957
7957
  var require_prerelease = __commonJS({
7958
7958
  "../../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/prerelease.js"(exports, module) {
7959
7959
  "use strict";
7960
- var parse2 = require_parse();
7960
+ var parse3 = require_parse();
7961
7961
  var prerelease = (version, options) => {
7962
- const parsed = parse2(version, options);
7962
+ const parsed = parse3(version, options);
7963
7963
  return parsed && parsed.prerelease.length ? parsed.prerelease : null;
7964
7964
  };
7965
7965
  module.exports = prerelease;
@@ -8145,7 +8145,7 @@ var require_coerce = __commonJS({
8145
8145
  "../../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/coerce.js"(exports, module) {
8146
8146
  "use strict";
8147
8147
  var SemVer = require_semver();
8148
- var parse2 = require_parse();
8148
+ var parse3 = require_parse();
8149
8149
  var { safeRe: re, t } = require_re();
8150
8150
  var coerce = (version, options) => {
8151
8151
  if (version instanceof SemVer) {
@@ -8180,7 +8180,7 @@ var require_coerce = __commonJS({
8180
8180
  const patch = match[4] || "0";
8181
8181
  const prerelease = options.includePrerelease && match[5] ? `-${match[5]}` : "";
8182
8182
  const build = options.includePrerelease && match[6] ? `+${match[6]}` : "";
8183
- return parse2(`${major}.${minor}.${patch}${prerelease}${build}`, options);
8183
+ return parse3(`${major}.${minor}.${patch}${prerelease}${build}`, options);
8184
8184
  };
8185
8185
  module.exports = coerce;
8186
8186
  }
@@ -8190,7 +8190,7 @@ var require_coerce = __commonJS({
8190
8190
  var require_truncate = __commonJS({
8191
8191
  "../../node_modules/.pnpm/semver@7.8.5/node_modules/semver/functions/truncate.js"(exports, module) {
8192
8192
  "use strict";
8193
- var parse2 = require_parse();
8193
+ var parse3 = require_parse();
8194
8194
  var constants = require_constants();
8195
8195
  var SemVer = require_semver();
8196
8196
  var truncate = (version, truncation, options) => {
@@ -8202,7 +8202,7 @@ var require_truncate = __commonJS({
8202
8202
  };
8203
8203
  var cloneInputVersion = (version, options) => {
8204
8204
  const versionStringToParse = version instanceof SemVer ? version.version : version;
8205
- return parse2(versionStringToParse, options);
8205
+ return parse3(versionStringToParse, options);
8206
8206
  };
8207
8207
  var doTruncation = (version, truncation) => {
8208
8208
  if (isPrerelease(truncation)) {
@@ -9244,7 +9244,7 @@ var require_semver2 = __commonJS({
9244
9244
  var constants = require_constants();
9245
9245
  var SemVer = require_semver();
9246
9246
  var identifiers = require_identifiers();
9247
- var parse2 = require_parse();
9247
+ var parse3 = require_parse();
9248
9248
  var valid = require_valid();
9249
9249
  var clean = require_clean();
9250
9250
  var inc = require_inc();
@@ -9283,7 +9283,7 @@ var require_semver2 = __commonJS({
9283
9283
  var simplifyRange = require_simplify();
9284
9284
  var subset = require_subset();
9285
9285
  module.exports = {
9286
- parse: parse2,
9286
+ parse: parse3,
9287
9287
  valid,
9288
9288
  clean,
9289
9289
  inc,
@@ -11670,7 +11670,7 @@ var require_parser2 = __commonJS({
11670
11670
  }
11671
11671
  return buf;
11672
11672
  };
11673
- _proto.parse = function parse2() {
11673
+ _proto.parse = function parse3() {
11674
11674
  return new nodes.NodeList(0, 0, this.parseNodes());
11675
11675
  };
11676
11676
  _proto.parseAsRoot = function parseAsRoot() {
@@ -11679,7 +11679,7 @@ var require_parser2 = __commonJS({
11679
11679
  return Parser2;
11680
11680
  }(Obj);
11681
11681
  module.exports = {
11682
- parse: function parse2(src, extensions, opts) {
11682
+ parse: function parse3(src, extensions, opts) {
11683
11683
  var p = new Parser(lexer.lex(src, opts));
11684
11684
  if (extensions !== void 0) {
11685
11685
  p.extensions = extensions;
@@ -15074,10 +15074,21 @@ async function streamCompletion(client, params, signal, onText) {
15074
15074
  messages: params.messages,
15075
15075
  tools: params.tools,
15076
15076
  tool_choice: params.tool_choice,
15077
+ // Undefined is dropped by the SDK's JSON serialization, so an unset cap
15078
+ // leaves output unbounded (unchanged behavior).
15079
+ max_completion_tokens: params.max_completion_tokens,
15080
+ // Undefined is dropped by the SDK, so an unset temperature leaves the
15081
+ // provider default in place (unchanged behavior).
15082
+ temperature: params.temperature,
15077
15083
  stream: true,
15078
15084
  stream_options: { include_usage: true }
15079
15085
  },
15080
- { signal }
15086
+ // The agent loop (loop.ts) is the SOLE retry authority — it owns the bounded
15087
+ // backoff retry for transient failures. Disable the OpenAI SDK's own retries
15088
+ // (default 2) so they don't MULTIPLY with the loop's: a gateway timeout was
15089
+ // being retried SDK(1+2) x loop(1+3) = ~12 times, turning one 300s timeout
15090
+ // into ~12 minutes of dead attempts.
15091
+ { signal, maxRetries: 0 }
15081
15092
  );
15082
15093
  } catch (e) {
15083
15094
  const err = e;
@@ -15119,7 +15130,7 @@ async function streamCompletion(client, params, signal, onText) {
15119
15130
  alog("stream:end", { chunks: chunkN, contentLen: content.length, toolCalls: toolCalls.length, finishReason, usage });
15120
15131
  return { content, toolCalls, usage, finishReason };
15121
15132
  }
15122
- function createChatClient(auth) {
15133
+ function createChatClient(auth, opts = {}) {
15123
15134
  return new OpenAI({
15124
15135
  baseURL: gatewayBaseURL(),
15125
15136
  // Real key lives server-side; this placeholder is replaced per-request by the
@@ -15132,6 +15143,7 @@ function createChatClient(auth) {
15132
15143
  const token = await auth.getToken().catch(() => null);
15133
15144
  if (token) headers.set("Authorization", `Bearer ${token}`);
15134
15145
  else headers.delete("Authorization");
15146
+ if (opts.origin) headers.set("X-Prompd-Origin", opts.origin);
15135
15147
  alog("gateway:fetch", { url: String(url), hasToken: !!token });
15136
15148
  try {
15137
15149
  const res = await fetch(url, { ...init, headers });
@@ -15163,6 +15175,7 @@ function statusOf(message) {
15163
15175
  }
15164
15176
  function classifyStreamError(message, status) {
15165
15177
  if (/\baborted\b|AbortError/i.test(message)) return "fatal";
15178
+ if (/\b(timed out|request timeout|gateway timeout|deadline exceeded)\b/i.test(message)) return "fatal";
15166
15179
  const code = status ?? statusOf(message);
15167
15180
  if (code != null) {
15168
15181
  if (FATAL_STATUS.has(code)) return "fatal";
@@ -15370,7 +15383,25 @@ var BUILTIN_NAMESPACES = {
15370
15383
  ask_user: "agent",
15371
15384
  spawn_agent: "agent",
15372
15385
  spawn_agents: "agent",
15373
- todo_write: "agent"
15386
+ todo_write: "agent",
15387
+ // Named here as a backstop; the sidecar's own decls also carry `namespace`,
15388
+ // and an explicit field always wins (resolveNamespace). Both say the same
15389
+ // thing so a sidecar that predates this still lands in the right namespace.
15390
+ sidecar_fs_read_file: "sidecar",
15391
+ sidecar_fs_write_file: "sidecar",
15392
+ sidecar_fs_read_bytes: "sidecar",
15393
+ sidecar_fs_list_dir: "sidecar",
15394
+ sidecar_fs_delete: "sidecar",
15395
+ sidecar_shell_exec: "sidecar",
15396
+ // Host-side orchestration tools. They are not in allTools() - they reach the
15397
+ // agent as remote decls (workerFactory) - but they are still governed by the
15398
+ // policy, so they need a canonical address like everything else.
15399
+ create_strategy: "agent",
15400
+ edit_strategy: "agent",
15401
+ view_strategy: "agent",
15402
+ run_strategy: "agent",
15403
+ list_strategies: "agent",
15404
+ list_saved_strategies: "agent"
15374
15405
  };
15375
15406
  function resolveNamespace(tool) {
15376
15407
  if (tool.namespace) return tool.namespace;
@@ -15390,6 +15421,14 @@ function toolAddress(tool) {
15390
15421
  const m = new RegExp(`^${ns}__?`).exec(leaf);
15391
15422
  if (m) leaf = leaf.slice(m[0].length);
15392
15423
  }
15424
+ if (ns === "sidecar") {
15425
+ for (const p of ["sidecar_fs_", "sidecar_"]) {
15426
+ if (leaf.startsWith(p)) {
15427
+ leaf = leaf.slice(p.length);
15428
+ break;
15429
+ }
15430
+ }
15431
+ }
15393
15432
  return `${ns}:${leaf}`;
15394
15433
  }
15395
15434
 
@@ -15408,6 +15447,16 @@ function formatOffloadRef(ref) {
15408
15447
  ${ref.preview}${ref.bytes > ref.preview.length ? "\n\u2026(truncated)" : ""}` : head;
15409
15448
  }
15410
15449
 
15450
+ // ../harness/prompts/toolsMarker.ts
15451
+ var TOOLS_MARKER = "\uE000PROMPD_TOOLS\uE000";
15452
+ function formatToolLines(tools) {
15453
+ return tools.map((t) => `- \`${t.name}\`: ${t.description}`).join("\n");
15454
+ }
15455
+ function fillToolsMarker(system, tools) {
15456
+ if (!system.includes(TOOLS_MARKER)) return system;
15457
+ return system.replaceAll(TOOLS_MARKER, tools.length ? formatToolLines(tools) : "(no tools available this run)");
15458
+ }
15459
+
15411
15460
  // ../harness/core/loop.ts
15412
15461
  async function runAgent(args) {
15413
15462
  const { client, system, tools, toolCtx } = args;
@@ -15423,8 +15472,9 @@ async function runAgent(args) {
15423
15472
  const recovery = resolveRecovery(args.recovery);
15424
15473
  const toolFails = new ToolFailureTracker();
15425
15474
  const messages = [...args.messages];
15426
- if (system && !messages.some((m) => m.role === "system")) {
15427
- messages.unshift({ role: "system", content: system });
15475
+ const filledSystem = system ? fillToolsMarker(system, tools) : system;
15476
+ if (filledSystem && !messages.some((m) => m.role === "system")) {
15477
+ messages.unshift({ role: "system", content: filledSystem });
15428
15478
  }
15429
15479
  const toolDefs = tools.map((t) => ({
15430
15480
  type: "function",
@@ -15462,7 +15512,9 @@ async function runAgent(args) {
15462
15512
  model,
15463
15513
  messages,
15464
15514
  tools: toolDefs.length ? toolDefs : void 0,
15465
- tool_choice: toolDefs.length ? "auto" : void 0
15515
+ tool_choice: toolDefs.length ? "auto" : void 0,
15516
+ max_completion_tokens: args.maxCompletionTokens,
15517
+ temperature: args.temperature
15466
15518
  },
15467
15519
  signal,
15468
15520
  (delta) => emit({ type: "assistant-delta", delta })
@@ -15910,6 +15962,18 @@ var todoWriteTool = {
15910
15962
  name: "todo_write",
15911
15963
  noOffload: true,
15912
15964
  // the task list is what the user wants to see — keep it inline
15965
+ /* DECLARED, not left to the policy's default. It is a write in name only: it
15966
+ * touches no file, no network and no state outside the run's own task panel,
15967
+ * so there is nothing for a person to approve. Without this it inherited
15968
+ * `policy.defaultPermission` — `ask` under any careful policy — and an agent
15969
+ * doing multi-step work stopped to request approval every time it ticked off a
15970
+ * step, which trains people to approve without reading.
15971
+ *
15972
+ * `permission` sits BELOW an explicit `policy.permissions` entry in the
15973
+ * precedence chain (permission-service.ts), so anyone who genuinely wants to
15974
+ * gate it still can; it only replaces the blanket default. And it is not
15975
+ * `mutates`, so a policy's mutating backstop correctly does not catch it. */
15976
+ permission: "allow",
15913
15977
  description: 'Maintain a visible task list for a multi-step job. Pass the COMPLETE list each time (a rewrite). Keep exactly one task "in_progress" while you work it; mark tasks "completed" as you finish; use "blocked" + blockedBy for tasks waiting on others; nest a subtask one level with parentId. Reuse a short stable id per task across updates so its progress and activity persist. Call it when you start a multi-step task and after each meaningful step.',
15914
15978
  parameters: {
15915
15979
  type: "object",
@@ -16103,17 +16167,29 @@ async function prmdCompileError(content, ctx) {
16103
16167
  }
16104
16168
  var proposeEditTool = {
16105
16169
  name: "propose_edit",
16106
- description: "Propose a replacement for the current editor file. The user reviews a diff and applies or rejects it \u2014 this does NOT write the file directly. Provide the COMPLETE new file content (not a patch) plus a short explanation. Use the editor file shown in the system context as the basis. This call WAITS for the review and returns the outcome: applied, partially applied (with the rejected changes), or rejected \u2014 rely on that outcome, not on the proposal, for any follow-up work.",
16170
+ description: "Propose a replacement for the CURRENTLY OPEN editor file. It takes no path and can only ever replace that one file, so never use it to create a new file or to edit a different one \u2014 use create_file or write_file for those. The user reviews a diff and applies or rejects it \u2014 this does NOT write the file directly. Provide the COMPLETE new file content (not a patch) plus a short explanation. Use the editor file shown in the system context as the basis. This call WAITS for the review and returns the outcome: applied, partially applied (with the rejected changes), or rejected \u2014 rely on that outcome, not on the proposal, for any follow-up work.",
16107
16171
  parameters: {
16108
16172
  type: "object",
16109
16173
  properties: {
16110
16174
  explanation: { type: "string", description: "One line describing the change." },
16111
- new_content: { type: "string", description: "The full proposed file content." }
16175
+ new_content: { type: "string", description: "The full proposed file content." },
16176
+ path: { type: "string", description: "The file being replaced. Must be the file currently open in the editor." }
16112
16177
  },
16113
- required: ["explanation", "new_content"]
16178
+ required: ["explanation", "new_content", "path"]
16114
16179
  },
16115
16180
  async run(args, ctx) {
16116
16181
  const explanation = str4(args, "explanation") || "Proposed an edit";
16182
+ if (!ctx.editorFile) {
16183
+ return "Cannot propose an edit: no file is open in the editor, and propose_edit only replaces the open file. To create a new file use create_file, and to overwrite an existing one use write_file \u2014 both take an explicit path.";
16184
+ }
16185
+ const target = str4(args, "path");
16186
+ if (!target) {
16187
+ return `Cannot propose an edit: propose_edit needs a path, and it must be the open file (${ctx.editorFile}). To create a new file use create_file, or to overwrite a different one use write_file.`;
16188
+ }
16189
+ const norm = (p) => p.trim().replace(/\\/g, "/").replace(/^\.\//, "");
16190
+ if (norm(target) !== norm(ctx.editorFile)) {
16191
+ return `Cannot propose an edit: the open file is ${ctx.editorFile}, but this proposal targets ${target}. propose_edit can only replace the open file. Use create_file for a new file, or write_file to overwrite a different one.`;
16192
+ }
16117
16193
  if (!ctx.reviewEdit || !ctx.callId) {
16118
16194
  return `Proposed an edit: ${explanation}. Awaiting the user's review (apply/reject).`;
16119
16195
  }
@@ -16147,21 +16223,23 @@ Respect the rejection \u2014 do not re-propose those changes unless the user ask
16147
16223
  var saveMemoryTool = {
16148
16224
  name: "save_memory",
16149
16225
  mutates: true,
16150
- description: "Persist a durable note to memory for later recall (facts, user preferences, decisions).",
16226
+ description: 'Persist a durable note to memory for later recall (facts, user preferences, decisions). Choose scope: "workspace" for facts specific to the current project, "global" for facts about the user or their preferences that apply everywhere.',
16151
16227
  parameters: {
16152
16228
  type: "object",
16153
16229
  properties: {
16154
16230
  text: { type: "string", description: "The note to remember." },
16231
+ scope: { type: "string", enum: ["workspace", "global"], description: "Where to store it: workspace (this project) or global (everywhere)." },
16155
16232
  tags: { type: "array", items: { type: "string" }, description: "Optional tags." }
16156
16233
  },
16157
- required: ["text"]
16234
+ required: ["text", "scope"]
16158
16235
  },
16159
16236
  // Permission is gated centrally by the loop's policy/permission layer (this
16160
16237
  // tool is `mutates`), so no confirm call here.
16161
16238
  async run(args, ctx) {
16162
16239
  const text = str4(args, "text");
16163
16240
  const tags = Array.isArray(args.tags) ? args.tags.map(String) : [];
16164
- const item = await ctx.memory.save(text, tags);
16241
+ const scope = args.scope === "global" ? "global" : "workspace";
16242
+ const item = await ctx.memory.save(text, tags, scope);
16165
16243
  return `Saved memory ${item.id}.`;
16166
16244
  }
16167
16245
  };
@@ -16208,6 +16286,19 @@ var recallResultTool = {
16208
16286
  name: "recall_result",
16209
16287
  noOffload: true,
16210
16288
  // recalling a large result must never re-offload itself
16289
+ /* DECLARED, like todo_write's and ask_user's. This one grants no new access at
16290
+ * all: it reads back a tool result THIS SESSION ALREADY PRODUCED, which means
16291
+ * the original call already passed the policy check that produced it. The
16292
+ * store is session-scoped (`<root>/<sessionId>/blobs/<id>.json`) and the id
16293
+ * shape is validated, so it cannot reach another session's data or escape the
16294
+ * directory.
16295
+ *
16296
+ * The deciding argument is that offloading is a CONTEXT-MANAGEMENT detail,
16297
+ * triggered by size. Gating recall means the same information costs an
16298
+ * approval prompt depending on how large it happened to be - a performance
16299
+ * heuristic leaking into governance, and an approval the user cannot act on
16300
+ * meaningfully because they already approved the thing that produced it. */
16301
+ permission: "allow",
16211
16302
  description: 'Fetch the full content of a tool result that was offloaded to save context. You saw an "[offloaded tool result \u2026 recall_result({ id })]" reference \u2014 pass that id.',
16212
16303
  parameters: {
16213
16304
  type: "object",
@@ -16223,6 +16314,21 @@ var recallResultTool = {
16223
16314
  var askUserTool = {
16224
16315
  name: "ask_user",
16225
16316
  noOffload: true,
16317
+ /* DECLARED, like todo_write's. Gating this behind an approval prompt is asking
16318
+ * the user for permission to ask the user a question — two dialogs for one
16319
+ * decision, the first of which carries no information.
16320
+ *
16321
+ * It changes nothing outside the run: no file, no network, no state. Its whole
16322
+ * effect is to put a question in front of a person who is already there.
16323
+ *
16324
+ * Nothing depended on its previous `ask` tier. The case that looks like it
16325
+ * might — an unattended run parking forever on a question nobody will answer —
16326
+ * is handled by EXCLUSION rather than by permission: CREW_EXCLUDED_TOOLS drops
16327
+ * ask_user from every crew agent (see @corenel/crew's capabilities.ts), and the
16328
+ * daemon's executor honours the same set. On a surface where it IS offered,
16329
+ * someone is by definition attending. And with no `ask` handler wired, `run`
16330
+ * already returns "unavailable in this environment" rather than hanging. */
16331
+ permission: "allow",
16226
16332
  description: "Ask the user one or more questions when you need a decision you cannot make from context \u2014 to choose between approaches, get a missing requirement, or confirm direction. Prefer this over guessing. Give each question a short `header` (a chip label), the `question` text, and 2\u20134 distinct `options` (each with a label and an optional one-line description of the trade-off). Set `multiSelect: true` when more than one option may apply. The user can also type a custom answer. Returns the chosen answers per question.",
16227
16333
  parameters: {
16228
16334
  type: "object",
@@ -16349,6 +16455,24 @@ Error: ${String(e?.message || e)}`;
16349
16455
  return results.join("\n\n");
16350
16456
  }
16351
16457
  };
16458
+ var listToolsTool = {
16459
+ name: "list_tools",
16460
+ noOffload: true,
16461
+ description: "List the tools available to you this run \u2014 their names, one-line descriptions, and top-level parameters. Use this to discover what you can call (including any MCP or connected-host tools) before deciding how to act.",
16462
+ parameters: { type: "object", properties: {} },
16463
+ async run(_args, ctx) {
16464
+ if (!ctx.listTools) return "Tool discovery is not available here.";
16465
+ const tools = ctx.listTools();
16466
+ if (!tools.length) return "No tools are available this run.";
16467
+ const lines = tools.map((t) => {
16468
+ const props = t.parameters && typeof t.parameters === "object" ? Object.keys(t.parameters.properties ?? {}) : [];
16469
+ const params = props.length ? ` \u2014 params: ${props.join(", ")}` : "";
16470
+ return `- \`${t.name}\`: ${t.description}${params}`;
16471
+ });
16472
+ return `Available tools this run (${tools.length}):
16473
+ ${lines.join("\n")}`;
16474
+ }
16475
+ };
16352
16476
  var compilePrompdTool = {
16353
16477
  name: "compile_prompd",
16354
16478
  noOffload: true,
@@ -16383,6 +16507,7 @@ var defaultTools = [
16383
16507
  askUserTool,
16384
16508
  spawnAgentTool,
16385
16509
  spawnAgentsTool,
16510
+ listToolsTool,
16386
16511
  proposeEditTool,
16387
16512
  saveMemoryTool,
16388
16513
  recallMemoryTool,
@@ -16414,7 +16539,7 @@ function configWorkspace() {
16414
16539
 
16415
16540
  // ../harness/prompts/library/defaults.generated.ts
16416
16541
  var RAW_DEFAULTS = {
16417
- "prompts/help-system.md": '---\nid: help-system\nname: Help assistant\nversion: 1.0.0\ndescription: System prompt for the isolated in-app help chat. Compiled with the site index injected.\nparameters:\n - name: app_index\n type: string\n description: The compiled site index describing the app\'s structure.\n default: ""\n - name: recent_summary\n type: string\n description: A summary of the earlier conversation when a new session was started.\n default: ""\n---\nYou are the **Prompd Help** assistant \u2014 a friendly in-app guide embedded in the Prompd web editor. Your only job is to help the user understand and navigate THIS app. You are not a general chatbot and you do not write or edit their prompts (the editor\'s Assistant does that).\n\nHow to help:\n- Answer from the SITE INDEX below. Tell the user exactly where a thing is and how to get to it ("Settings \u2192 Appearance", "the Build button in the Editor panel header", "the layout presets in the top bar").\n- When pointing at a route, link it in markdown so it\'s clickable: `[Editor](/editor)`, `[Workflows](/workflows)`. For buttons/panels that aren\'t routes, name their location precisely instead of inventing a link.\n- You can DO things, not just describe them. For any action in the "Actions you can trigger" list, write a link as `[label](prompd:<id>)` (e.g. `[create a new file](prompd:new-file)`, `[switch to Chat layout](prompd:layout-chat)`). Clicking it performs the action in the app. Offer an action link whenever the user wants to *do* the thing.\n- For multi-step tasks, follow the matching "How-to flow": give the numbered steps, and turn each step that has an action into a `prompd:<id>` link so the user can jump straight there. Lead with the action link, then the remaining steps.\n- Only use action ids and route paths that appear in the index \u2014 never invent a `prompd:` id.\n- Be concise and concrete. Lead with the answer. Use short steps or a tight list when there are multiple actions.\n- If something isn\'t in the index, say you\'re not sure rather than inventing UI that may not exist.\n- Keep a warm, plain tone. No filler, no preamble.\n\n{% if recent_summary %}\nEarlier in this conversation (summarized, because a new session was started):\n{{ recent_summary }}\n{% endif %}\n\n--- SITE INDEX ---\n{{ app_index }}\n',
16542
+ "systems/help-system.md": '---\nid: help-system\nname: Help assistant\nversion: 1.0.0\ndescription: System prompt for the isolated in-app help chat. Compiled with the site index injected.\nparameters:\n - name: app_index\n type: string\n description: The compiled site index describing the app\'s structure.\n default: ""\n - name: recent_summary\n type: string\n description: A summary of the earlier conversation when a new session was started.\n default: ""\n---\nYou are the **Prompd Help** assistant \u2014 a friendly in-app guide embedded in the Prompd web editor. Your only job is to help the user understand and navigate THIS app. You are not a general chatbot and you do not write or edit their prompts (the editor\'s Assistant does that).\n\nHow to help:\n- Answer from the SITE INDEX below. Tell the user exactly where a thing is and how to get to it ("Settings \u2192 Appearance", "the Build button in the Editor panel header", "the layout presets in the top bar").\n- When pointing at a route, link it in markdown so it\'s clickable: `[Editor](/editor)`, `[Workflows](/workflows)`. For buttons/panels that aren\'t routes, name their location precisely instead of inventing a link.\n- You can DO things, not just describe them. For any action in the "Actions you can trigger" list, write a link as `[label](prompd:<id>)` (e.g. `[create a new file](prompd:new-file)`, `[switch to Chat layout](prompd:layout-chat)`). Clicking it performs the action in the app. Offer an action link whenever the user wants to *do* the thing.\n- For multi-step tasks, follow the matching "How-to flow": give the numbered steps, and turn each step that has an action into a `prompd:<id>` link so the user can jump straight there. Lead with the action link, then the remaining steps.\n- Only use action ids and route paths that appear in the index \u2014 never invent a `prompd:` id.\n- Be concise and concrete. Lead with the answer. Use short steps or a tight list when there are multiple actions.\n- If something isn\'t in the index, say you\'re not sure rather than inventing UI that may not exist.\n- Keep a warm, plain tone. No filler, no preamble.\n\n{% if recent_summary %}\nEarlier in this conversation (summarized, because a new session was started):\n{{ recent_summary }}\n{% endif %}\n\n--- SITE INDEX ---\n{{ app_index }}\n',
16418
16543
  "tools/guidance.md": '<!--\nTool Guidance \u2014 use this to steer tool usage, e.g. "Prefer searching the registry\nfor relevant packages over guessing which tools to use." Anything outside this\ncomment is added to the system prompt; the comment itself is stripped.\n-->\n',
16419
16544
  "editor/inline-assist.prmd": `---
16420
16545
  id: inline-assist
@@ -16496,7 +16621,64 @@ Add a constraint :: Add a constraint or rule to the prompt instructions (what to
16496
16621
  "personas/senior-architect.md": "---\nid: senior-architect\nname: Senior architect\ndescription: Shapes systems and tradeoffs.\n---\nYou are a senior software architect. You shape systems before code is written and keep them coherent as they grow.\n- Understand before designing. Map the existing architecture, data flow, and constraints; reuse and extend established patterns before introducing new ones.\n- Design for the real requirement, not an imagined one. Prefer the simplest structure that meets today's need with a clear seam for tomorrow's. Avoid speculative abstraction (YAGNI).\n- Make boundaries explicit. Define interfaces, contracts, and ownership; keep coupling low and cohesion high; isolate the decisions most likely to change behind stable seams.\n- Name the tradeoffs. For any consequential choice, lay out the options, what each costs, and why you'd pick one. Surface assumptions and risks plainly.\n- Sequence the work into safe, shippable steps with reversible checkpoints; call out what must land first.\n- Stay grounded in the actual codebase and conventions. When uncertain, say so and propose how to de-risk it.\n",
16497
16622
  "personas/senior-dev.md": "---\nid: senior-dev\nname: Senior engineer\ndescription: Senior software engineer.\n---\nYou are a senior software engineer. Bring that craft to every change:\n- Fit the codebase. Study neighboring code and follow its conventions before writing. Don't assume a library is available \u2014 confirm it's already used in the project before depending on it.\n- Write the minimum that solves the task. No speculative abstractions, options, or features for hypothetical futures; three similar lines beat a premature abstraction. No half-finished or stubbed implementations.\n- Don't add error handling, validation, or fallbacks for cases that can't happen. Trust internal guarantees; validate only at real boundaries (user input, external systems). No backwards-compat shims unless asked.\n- Comments are for the non-obvious WHY \u2014 a constraint, an invariant, a workaround. Don't narrate what the code already says; default to none.\n- Verify your work before calling a change done. If you couldn't verify something, say so plainly.\n- Be security-minded: no injection, no leaked secrets, least privilege.\n",
16498
16623
  "personas/terse.md": "---\nid: terse\nname: Terse\ndescription: Extremely concise.\n---\nBe extremely concise. One sentence per update. No preamble. No summary unless asked.\n",
16499
- "systems/system-base.md": '---\nid: system-base\nname: Operational system prompt\nversion: 1.0.0\ndescription: The agent\'s system prompt \u2014 persona, mode, tool definitions and editor context composed by @prompd/core.\nparameters:\n - name: persona\n type: string\n description: The selected persona\'s voice/role text, placed first.\n default: ""\n - name: mode\n type: string\n description: Active mode id (auto / edit / plan / brainstorm).\n default: "auto"\n - name: mode_hint\n type: string\n description: Steering text for the active mode.\n default: ""\n - name: tools\n type: string\n description: Definitions of the tools available this run.\n default: ""\n - name: tool_guidance\n type: string\n description: Optional extra guidance on tool usage.\n default: ""\n - name: file_context\n type: string\n description: The editor file / selection / compiled-output context block.\n default: ""\n---\n{% if persona %}{{ persona }}\n\n{% endif %}You are the Prompd assistant embedded in the in-browser .prmd editor. You help the user author, compile, and improve .prmd/.md prompts (YAML frontmatter + Nunjucks body with typed parameters). When the user asks about the .prmd format itself \u2014 frontmatter fields, parameter types (including enums), date defaults, inheritance, or templating \u2014 the canonical reference is `languages/prmd.md` in the Prompd config workspace; consult it rather than guessing (it is already included in context whenever a .prmd/.md file is open).\n\nTo change the file open in the editor, call propose_edit with the COMPLETE new file content \u2014 the user reviews a diff and applies it. To read or change other files in the workspace, use the file tools (writes ask the user for permission first). Use compile_prompd to preview rendered output and search_packages to find registry packages. For any multi-step job, maintain a visible task list with todo_write: lay out the steps, keep one task in_progress, and mark each completed as you go. Prefer calling a tool over guessing, and be concise.\n\n## Tools available this run\n{{ tools }}\n{% if tool_guidance %}\n\n{{ tool_guidance }}\n{% endif %}\n{% if mode_hint %}\n\n## Mode\n{{ mode_hint }}\n{% endif %}\n{% if file_context %}\n\n{{ file_context }}\n{% endif %}\n',
16624
+ "memory/MEMORY.md": "# Memory index\n\nSaved notes appear here, one line each. The agent adds them with save_memory and\nreads them with recall_memory. This is the global (~/.prompd) memory; a project's\nown notes live in ./.prompd/memory.\n",
16625
+ "systems/system-base.md": `---
16626
+ id: system-base
16627
+ name: Operational system prompt
16628
+ version: 1.0.0
16629
+ description: The agent's system prompt \u2014 persona, mode, tool definitions and editor context composed by @prompd/core.
16630
+ parameters:
16631
+ - name: persona
16632
+ type: string
16633
+ description: The selected persona's voice/role text, placed first.
16634
+ default: ""
16635
+ - name: mode
16636
+ type: string
16637
+ description: Active mode id (auto / edit / plan / brainstorm).
16638
+ default: "auto"
16639
+ - name: mode_hint
16640
+ type: string
16641
+ description: Steering text for the active mode.
16642
+ default: ""
16643
+ - name: tools
16644
+ type: string
16645
+ description: Definitions of the tools available this run.
16646
+ default: ""
16647
+ - name: tool_guidance
16648
+ type: string
16649
+ description: Optional extra guidance on tool usage.
16650
+ default: ""
16651
+ - name: file_context
16652
+ type: string
16653
+ description: The editor file / selection / compiled-output context block.
16654
+ default: ""
16655
+ - name: app
16656
+ type: string
16657
+ description: Product name of the host application, used for the agent's own identity.
16658
+ default: "Prompd"
16659
+ ---
16660
+ {% if persona %}{{ persona }}
16661
+
16662
+ {% endif %}You are the {{ app }} assistant embedded in the in-browser .prmd editor. You help the user author, compile, and improve .prmd/.md prompts (YAML frontmatter + Nunjucks body with typed parameters). When the user asks about the .prmd format itself \u2014 frontmatter fields, parameter types (including enums), date defaults, inheritance, or templating \u2014 the canonical reference is \`languages/prmd.md\` in the {{ app }} config workspace; consult it rather than guessing (it is already included in context whenever a .prmd/.md file is open).
16663
+
16664
+ To change the file open in the editor, call propose_edit with the COMPLETE new file content \u2014 the user reviews a diff and applies it. To read or change other files in the workspace, use the file tools (writes ask the user for permission first). Use compile_prompd to preview rendered output and search_packages to find registry packages. For any multi-step job, maintain a visible task list with todo_write: lay out the steps, keep one task in_progress, and mark each completed as you go. Prefer calling a tool over guessing, and be concise.
16665
+
16666
+ ## Tools available this run
16667
+ {{ tools }}
16668
+ {% if tool_guidance %}
16669
+
16670
+ {{ tool_guidance }}
16671
+ {% endif %}
16672
+ {% if mode_hint %}
16673
+
16674
+ ## Mode
16675
+ {{ mode_hint }}
16676
+ {% endif %}
16677
+ {% if file_context %}
16678
+
16679
+ {{ file_context }}
16680
+ {% endif %}
16681
+ `,
16500
16682
  "systems/system-pdflow.md": '---\nid: system-pdflow\nname: Workflow file system prompt\nversion: 1.0.0\ndescription: Adds .pdflow format knowledge (node taxonomy, edges, parameters) on top of the base system prompt.\ninherits: system-base.md\n---\n\n## Active file format \u2014 Prompd workflow (.pdflow)\n\nThe open file is a **Prompd workflow**: JSON with `{ version, metadata, parameters[], nodes[], edges[] }`. The web editor renders it as a linear chain \u2014 a fixed Start, ordered middle nodes, a fixed End \u2014 connected one edge each.\n\nEach node is `{ id, type, position, data }`. Node types and their key `data`:\n\n- `trigger` \u2014 Start (entry point). Exactly one.\n- `prompt` \u2014 runs a `.prmd`/`.md` prompt. `data.sourceType` is `\'file\'` (`data.source` = a WORKSPACE-RELATIVE path) or `\'raw\'` (`data.rawPrompt`). Optional `data.model` / `data.providerNodeId`.\n- `user-input` \u2014 pauses to collect input. `data.prompt`, `data.inputType` (`text` / `textarea` / `choice` / `confirm` / `number`).\n- `guardrail` \u2014 validates/gates. `data.systemPrompt`, `data.passExpression` or `data.scoreThreshold`.\n- `agent` \u2014 composite (input guardrail \u2192 prompt \u2192 output guardrail). `data.systemPrompt`, `data.userPrompt`, `data.maxIterations`; internal guardrails in `data.inputGuardrail` / `data.outputGuardrail` (each `{ enabled, preset|systemPrompt, passExpression|scoreThreshold }`).\n- `transformer` \u2014 JSON\u2192JSON map. `data.mode: \'template\'` with `data.template` (`{{ input }}` substitution).\n- `callback` / `checkpoint` \u2014 debug/observation point (logs node I/O).\n- `output` \u2014 End (workflow result). Exactly one.\n- `provider` \u2014 one off-chain node holding the canvas-wide provider/model; nodes reference it via `data.providerNodeId`.\n\nEdges thread each node\'s output to the next; the runner exposes `{{ input }}` / `{{ previous_output }}` to downstream nodes.\n\n### Example (minimal chain)\n\n```\n{\n "version": "1.0",\n "metadata": { "name": "summarize" },\n "parameters": [],\n "nodes": [\n { "id": "start", "type": "trigger", "position": { "x": 0, "y": 0 }, "data": {} },\n { "id": "p1", "type": "prompt", "position": { "x": 0, "y": 120 }, "data": { "sourceType": "file", "source": "prompts/summarize.prmd" } },\n { "id": "end", "type": "output", "position": { "x": 0, "y": 240 }, "data": {} }\n ],\n "edges": [\n { "id": "e1", "source": "start", "target": "p1" },\n { "id": "e2", "source": "p1", "target": "end" }\n ]\n}\n```\n\nTo edit the workflow, modify this JSON with the file tools (paths are workspace-relative). Keep exactly one `trigger` and one `output`.\n',
16501
16683
  "systems/system-prmd-md.md": '---\nid: system-prmd-md\nname: Prompt file system prompt (.prmd / .md)\nversion: 1.0.0\ninherits: ./system-base.md\nparameters:\n - name: persona\n type: string\n description: The selected persona\'s voice/role text, placed first.\n default: ""\n - name: mode\n type: string\n description: Active mode id (auto / edit / plan / brainstorm).\n default: "auto"\n - name: mode_hint\n type: string\n description: Steering text for the active mode.\n default: ""\n - name: tools\n type: string\n description: Definitions of the tools available this run.\n default: ""\n - name: tool_guidance\n type: string\n description: Optional extra guidance on tool usage.\n default: ""\n - name: file_context\n type: string\n description: The editor file / selection / compiled-output context block.\n default: ""\n---\n\n{% include "../languages/prmd.md" %}\n\nWhen you edit this file, call `propose_edit` with the COMPLETE new file content. Keep the frontmatter valid YAML and only reference declared parameters in the body.\n',
16502
16684
  "templates/index.json": '[\n {\n "_comment": "EXAMPLE of a data-driven New File type. Copy this entry, remove \\"disabled\\", give it a unique \\"id\\", and create the seed file at \\"template\\" (in this templates/ folder). Tokens __ID__ / __NAME__ are substituted on create. Icon is a ui-kit name (e.g. IcFileCode, IcUser, IcFlow) or \\"svg:<markup>\\". See docs/file-types.md.",\n "disabled": true,\n "id": "example-agent-prompt",\n "label": "Agent prompt",\n "description": "A .prmd tuned for tool-using agents.",\n "ext": "prmd",\n "category": "Prompd",\n "icon": "IcUser",\n "defaultName": "agent",\n "template": "templates/new-agent-prmd.prmd",\n "accent": true\n }\n]\n',
@@ -16562,9 +16744,9 @@ Add your specific criteria here.
16562
16744
 
16563
16745
  Respond with PASS or FAIL followed by a one-sentence reason.
16564
16746
  `,
16565
- "strategies/debate.md": "---\nid: debate\nname: Debate\ndescription: Several independent attempts at the whole goal, judged and merged. Highest token use; best quality for open-ended or high-stakes goals.\ntier: 4\nknobs:\n attempts: 3\n parallelism: parallel\n---\nDo not split the goal. Instead, have it attempted WHOLE multiple times: create one\nsubagent per attempt (3 attempts), each with a single work item that tackles the\nentire goal from a distinct perspective (for example pragmatic, contrarian,\nfirst-principles). Name each subagent for its perspective. Write the synthesis as\na JUDGE: compare the attempts in {{ joined }}, weigh their strengths, and merge\nthe best elements into one final answer, noting significant disagreements.\n",
16566
- "strategies/fanout.md": "---\nid: fanout\nname: Fan-out\ndescription: Wide parallel decomposition across specialist subagents. Higher token use; broad coverage, fastest wall-clock.\ntier: 3\ndefault: true\nknobs:\n parallelism: parallel\n---\nDecompose the goal into INDEPENDENT workstreams that can run in parallel \u2014 one\nspecialist subagent per theme, each with focused work items. Maximize coverage:\ndistinct angles, no overlapping work. Use dependsOn only where a work item truly\nneeds another item's output.\n",
16567
- "strategies/lean.md": `---
16747
+ "strategies/types/debate.md": "---\nid: debate\nname: Debate\ndescription: Several independent attempts at the whole goal, judged and merged. Highest token use; best quality for open-ended or high-stakes goals.\ntier: 4\nknobs:\n attempts: 3\n parallelism: parallel\n---\nDo not split the goal. Instead, have it attempted WHOLE multiple times: create one\nsubagent per attempt (3 attempts), each with a single work item that tackles the\nentire goal from a distinct perspective (for example pragmatic, contrarian,\nfirst-principles). Name each subagent for its perspective. Write the synthesis as\na JUDGE: compare the attempts in {{ joined }}, weigh their strengths, and merge\nthe best elements into one final answer, noting significant disagreements.\n",
16748
+ "strategies/types/fanout.md": "---\nid: fanout\nname: Fan-out\ndescription: Wide parallel decomposition across specialist subagents. Higher token use; broad coverage, fastest wall-clock.\ntier: 3\ndefault: true\nknobs:\n parallelism: parallel\n---\nDecompose the goal into INDEPENDENT workstreams that can run in parallel \u2014 one\nspecialist subagent per theme, each with focused work items. Maximize coverage:\ndistinct angles, no overlapping work. Use dependsOn only where a work item truly\nneeds another item's output.\n",
16749
+ "strategies/types/lean.md": `---
16568
16750
  id: lean
16569
16751
  name: Lean
16570
16752
  description: One subagent, a few sequential steps. Lowest token use \u2014 best for focused, well-defined goals.
@@ -16582,7 +16764,7 @@ reserve a stronger model only for a work item that is genuinely hard.
16582
16764
  If a single work item covers the goal, set "synthesis" to exactly "{{ joined }}"
16583
16765
  so the runner returns that output directly without a synthesis call.
16584
16766
  `,
16585
- "strategies/pipeline.md": "---\nid: pipeline\nname: Pipeline\ndescription: Staged chain \u2014 each stage's output feeds the next. Moderate token use; best for transform-and-refine goals.\ntier: 2\nknobs:\n parallelism: staged\n---\nDecompose the goal into sequential STAGES (one subagent per stage), each\ntransforming or refining what the previous stage produced \u2014 for example\nresearch, then draft, then refine, then package. Keep it to 2-4 stages. Within a\nstage, work items may run in parallel, but a stage must only need what earlier\nstages produced. Write each stage's work prompts to state what they consume from\nthe previous stage and what they hand to the next.\n",
16767
+ "strategies/types/pipeline.md": "---\nid: pipeline\nname: Pipeline\ndescription: Staged chain \u2014 each stage's output feeds the next. Moderate token use; best for transform-and-refine goals.\ntier: 2\nknobs:\n parallelism: staged\n---\nDecompose the goal into sequential STAGES (one subagent per stage), each\ntransforming or refining what the previous stage produced \u2014 for example\nresearch, then draft, then refine, then package. Keep it to 2-4 stages. Within a\nstage, work items may run in parallel, but a stage must only need what earlier\nstages produced. Write each stage's work prompts to state what they consume from\nthe previous stage and what they hand to the next.\n",
16586
16768
  "skills/strategy-planning/SKILL.prmd": `---
16587
16769
  id: strategy-planning
16588
16770
  name: strategy-planning
@@ -16631,7 +16813,19 @@ the goal's stakes and vagueness:
16631
16813
  or context gaps with edit_strategy. Then tell the user what you gathered, what
16632
16814
  you decided, and where the spec/plan files live \u2014 the canvas graph plus those
16633
16815
  two files ARE the deliverable.
16634
- `
16816
+ `,
16817
+ "roles/document-writer/PERSONA.md": "Clear, plain-spoken technical writer. Values accuracy and brevity over flourish.\n",
16818
+ "roles/document-writer/POLICY.yaml": "version: 1\nname: role:document-writer\ndefaultPermission: ask\nmutatingDefault: ask\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n write_file: ask\n create_file: ask\n",
16819
+ "roles/document-writer/ROLE.md": "---\nlabel: Document Writer\njobTitle: Document Writer\ndescription: Drafts and edits clear documentation from the codebase and context.\nsuggestedSkills: []\n---\nWrite clearly and concisely for the intended reader. Ground every claim in the source. Prefer short sentences and concrete examples.\n",
16820
+ "roles/research-analyst/PERSONA.md": "Rigorous analyst. Sources claims, flags uncertainty, distinguishes fact from inference.\n",
16821
+ "roles/research-analyst/POLICY.yaml": "version: 1\nname: role:research-analyst\ndefaultPermission: ask\nmutatingDefault: deny\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n web_search: allow\n recall_memory: allow\n",
16822
+ "roles/research-analyst/ROLE.md": "---\nlabel: Research Analyst\njobTitle: Research Analyst\ndescription: Gathers and synthesizes information; does not modify the workspace.\nsuggestedSkills: []\n---\nGather from the web and the workspace, then synthesize a sourced, structured answer. Separate findings from assumptions. Do not modify files.\n",
16823
+ "roles/senior-software-engineer/PERSONA.md": "Pragmatic senior engineer. Precise, terse, correctness-first. Prefers the smallest change that fully solves the problem.\n",
16824
+ "roles/senior-software-engineer/POLICY.yaml": "version: 1\nname: role:senior-software-engineer\ndefaultPermission: ask\nmutatingDefault: ask\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n compile_prompd: allow\n write_file: ask\n create_file: ask\n rename_file: ask\n",
16825
+ "roles/senior-software-engineer/ROLE.md": "---\nlabel: Senior Software Engineer\njobTitle: Senior Software Engineer\ndescription: Implements features and fixes with tests; reads before writing.\nsuggestedSkills: []\n---\nImplement changes test-first. Read the surrounding code and match its conventions. Keep changes minimal and focused; explain non-obvious decisions.\n",
16826
+ "roles/senior-test-engineer/PERSONA.md": "Meticulous, adversarial about correctness. Thinks in failure modes and boundary conditions.\n",
16827
+ "roles/senior-test-engineer/POLICY.yaml": "version: 1\nname: role:senior-test-engineer\ndefaultPermission: ask\nmutatingDefault: deny\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n compile_prompd: allow\n",
16828
+ "roles/senior-test-engineer/ROLE.md": "---\nlabel: Senior Test Engineer\njobTitle: Senior Test Engineer\ndescription: Writes and runs tests; hunts edge cases; does not modify source.\nsuggestedSkills: []\n---\nFocus on coverage and edge cases. Write tests that fail before a fix and pass after. Do not modify production source; report gaps you find.\n"
16635
16829
  };
16636
16830
 
16637
16831
  // ../harness/prompts/library/index.ts
@@ -16884,8 +17078,7 @@ async function compileConfigTemplate(path, params) {
16884
17078
  }
16885
17079
  }
16886
17080
  function formatTools(tools) {
16887
- if (!tools || !tools.length) return "";
16888
- return tools.map((t) => `- \`${t.name}\`: ${t.description}`).join("\n");
17081
+ return tools && tools.length ? formatToolLines(tools) : "";
16889
17082
  }
16890
17083
  async function composeAgentSystem(args) {
16891
17084
  const mode = args.mode || "auto";
@@ -16902,21 +17095,41 @@ async function composeAgentSystem(args) {
16902
17095
  ]);
16903
17096
  const tool_guidance = toolGuidanceRaw.replace(/<!--[\s\S]*?-->/g, "").trim();
16904
17097
  const skillsBlock = await enabledSkillInstructions().catch(() => "");
17098
+ const memoryIdx = args.memoryIndex ? await args.memoryIndex().catch(() => "") : "";
17099
+ const memoryBlock = memoryIdx.trim() ? `## Memory
17100
+ Notes you've saved. Call recall_memory to read one in full; save_memory to add one.
17101
+
17102
+ ${memoryIdx.trim()}` : "";
16905
17103
  return compileConfigTemplate(systemPath, {
17104
+ // Who the agent says it IS. Injected rather than written into the prompt,
17105
+ // because the same shipped default is seeded into every host's config
17106
+ // workspace and each one carries a different product name.
17107
+ app: promptHost().appName?.() || "Prompd",
16906
17108
  persona: persona.trim(),
16907
17109
  mode,
16908
17110
  mode_hint,
16909
17111
  tool_guidance,
16910
- tools: formatTools(args.tools),
16911
- file_context: [(args.contextText || "").trim(), skillsBlock].filter(Boolean).join("\n\n"),
17112
+ tools: args.toolsText ?? formatTools(args.tools),
17113
+ file_context: [(args.contextText || "").trim(), memoryBlock, skillsBlock].filter(Boolean).join("\n\n"),
16912
17114
  ...args.params || {}
16913
17115
  });
16914
17116
  }
16915
17117
 
16916
17118
  // ../tools-node/fileService.ts
16917
17119
  import { promises as fs } from "node:fs";
16918
- import { join, resolve, relative, dirname, sep, basename } from "node:path";
17120
+ import { join, resolve, relative, dirname, sep, basename, isAbsolute } from "node:path";
16919
17121
  var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".cache", ".turbo"]);
17122
+ function escapesRoot(rel, platformSep = sep) {
17123
+ if (rel === "") return false;
17124
+ if (rel === ".." || rel.startsWith(".." + platformSep)) return true;
17125
+ if (rel.split(platformSep)[0] === "..") return true;
17126
+ if (isAbsolute(rel)) return true;
17127
+ if (platformSep === "\\") {
17128
+ if (/^[a-zA-Z]:/.test(rel)) return true;
17129
+ if (rel.startsWith("\\\\")) return true;
17130
+ }
17131
+ return false;
17132
+ }
16920
17133
  var NodeFileService = class {
16921
17134
  kind = "directory";
16922
17135
  label;
@@ -16930,8 +17143,7 @@ var NodeFileService = class {
16930
17143
  }
16931
17144
  /** True if `a` is lexically within the root (the root itself counts). */
16932
17145
  within(a) {
16933
- const rel = relative(this.root, a);
16934
- return rel === "" || rel !== ".." && !rel.startsWith(".." + sep) && rel.split(sep)[0] !== "..";
17146
+ return !escapesRoot(relative(this.root, a));
16935
17147
  }
16936
17148
  /** Absolute path for a folder-relative one, rejecting anything that escapes root
16937
17149
  * LEXICALLY (../, absolute). Symlink escapes are caught by assertReal/realAbs. */
@@ -16950,7 +17162,7 @@ var NodeFileService = class {
16950
17162
  try {
16951
17163
  const real = await fs.realpath(probe);
16952
17164
  const rel = relative(realRoot, real);
16953
- if (rel !== "" && (rel === ".." || rel.startsWith(".." + sep) || rel.split(sep)[0] === "..")) {
17165
+ if (escapesRoot(rel)) {
16954
17166
  throw new Error(`path escapes the workspace root via symlink: ${a}`);
16955
17167
  }
16956
17168
  return;
@@ -20165,13 +20377,23 @@ async function deviceLogin(opts) {
20165
20377
  const f = opts.fetchImpl ?? fetch;
20166
20378
  const base = opts.base.replace(/\/$/, "");
20167
20379
  const post = async (path, body) => {
20168
- const res = await f(`${base}${path}`, {
20380
+ const url = `${base}${path}`;
20381
+ const res = await f(url, {
20169
20382
  method: "POST",
20170
20383
  headers: { "Content-Type": "application/json" },
20171
20384
  body: JSON.stringify(body),
20172
20385
  signal: opts.signal
20173
20386
  });
20174
- return await res.json();
20387
+ const text = await res.text();
20388
+ try {
20389
+ return JSON.parse(text);
20390
+ } catch {
20391
+ const looksLikeHtml = /^\s*</.test(text);
20392
+ throw new Error(
20393
+ `${url} answered ${res.status} with ${looksLikeHtml ? "an HTML page" : "a non-JSON body"}, not JSON.` + (res.status === 404 ? `
20394
+ That usually means --base is wrong. The device endpoints live under /api \u2014 try --base ${base.replace(/\/api$/, "")}/api` : "")
20395
+ );
20396
+ }
20175
20397
  };
20176
20398
  const dc = await post("/oauth/device/code", { client_id: "corenel-cli", scope: "agent" });
20177
20399
  if (!dc.device_code) throw new Error("device authorization failed");
@@ -20198,11 +20420,300 @@ async function deviceLogin(opts) {
20198
20420
  }
20199
20421
  }
20200
20422
 
20423
+ // ../crew/frontmatter.ts
20424
+ var import_yaml = __toESM(require_dist(), 1);
20425
+ var FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
20426
+ function parseFrontmatter2(text) {
20427
+ const m = text.match(FRONTMATTER);
20428
+ if (!m) return { meta: {}, body: text };
20429
+ let meta = {};
20430
+ try {
20431
+ const parsed = import_yaml.default.parse(m[1]);
20432
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
20433
+ meta = parsed;
20434
+ }
20435
+ } catch {
20436
+ meta = {};
20437
+ }
20438
+ return { meta, body: m[2] };
20439
+ }
20440
+
20441
+ // ../crew/policy.ts
20442
+ var import_yaml2 = __toESM(require_dist(), 1);
20443
+ var LEVELS = ["allow", "ask", "deny"];
20444
+ var UNATTENDED = ["deny", "park", "allow"];
20445
+ function asLevel(v) {
20446
+ return typeof v === "string" && LEVELS.includes(v) ? v : void 0;
20447
+ }
20448
+ function asStringArray(v) {
20449
+ if (!Array.isArray(v)) return void 0;
20450
+ const out = v.filter((x) => typeof x === "string");
20451
+ return out.length ? out : void 0;
20452
+ }
20453
+ function coercePermissions(v) {
20454
+ const out = {};
20455
+ if (v && typeof v === "object" && !Array.isArray(v)) {
20456
+ for (const [k, val] of Object.entries(v)) {
20457
+ const lvl = asLevel(val);
20458
+ if (lvl) out[k] = lvl;
20459
+ }
20460
+ }
20461
+ return out;
20462
+ }
20463
+ function parsePolicyYaml(text, agentName) {
20464
+ let raw = {};
20465
+ if (text.trim()) {
20466
+ try {
20467
+ const parsed = import_yaml2.default.parse(text);
20468
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
20469
+ raw = parsed;
20470
+ }
20471
+ } catch {
20472
+ raw = {};
20473
+ }
20474
+ }
20475
+ const toolsAllow = raw.tools && typeof raw.tools === "object" ? asStringArray(raw.tools.allow) : void 0;
20476
+ const toolsDeny = raw.tools && typeof raw.tools === "object" ? asStringArray(raw.tools.deny) : void 0;
20477
+ const rawUnattended = raw.unattended;
20478
+ const unattended = typeof rawUnattended === "string" && UNATTENDED.includes(rawUnattended) ? rawUnattended : "deny";
20479
+ const rawPark = raw.parkTimeoutMs;
20480
+ const parkTimeoutMs = typeof rawPark === "number" && Number.isFinite(rawPark) && rawPark > 0 ? rawPark : void 0;
20481
+ const policy = {
20482
+ version: 1,
20483
+ name: typeof raw.name === "string" ? raw.name : `crew:${agentName}`,
20484
+ description: typeof raw.description === "string" ? raw.description : void 0,
20485
+ onViolation: raw.onViolation === "warn" ? "warn" : "stop",
20486
+ defaultPermission: asLevel(raw.defaultPermission) ?? "ask",
20487
+ mutatingDefault: asLevel(raw.mutatingDefault) ?? "deny",
20488
+ permissions: coercePermissions(raw.permissions),
20489
+ maxTurns: typeof raw.maxTurns === "number" ? raw.maxTurns : void 0,
20490
+ maxToolCalls: typeof raw.maxToolCalls === "number" ? raw.maxToolCalls : void 0,
20491
+ unattended,
20492
+ parkTimeoutMs
20493
+ };
20494
+ if (toolsAllow || toolsDeny) {
20495
+ policy.tools = { ...toolsAllow ? { allow: toolsAllow } : {}, ...toolsDeny ? { deny: toolsDeny } : {} };
20496
+ }
20497
+ return policy;
20498
+ }
20499
+
20500
+ // ../crew/paths.ts
20501
+ var CREW_SEG = "crew";
20502
+ var NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
20503
+ function isValidAgentName(name) {
20504
+ return NAME_RE.test(name);
20505
+ }
20506
+
20507
+ // ../crew/store.ts
20508
+ var import_yaml3 = __toESM(require_dist(), 1);
20509
+ var TRIGGER_TYPES = [
20510
+ "manual",
20511
+ "file-change",
20512
+ "run-complete",
20513
+ "interval-while-open",
20514
+ "app-init",
20515
+ "workspace-init",
20516
+ "schedule",
20517
+ "continuous",
20518
+ "self-directed",
20519
+ "external-webhook"
20520
+ ];
20521
+ async function readOr(files, path, fallback = "") {
20522
+ try {
20523
+ return await files.read(path);
20524
+ } catch {
20525
+ return fallback;
20526
+ }
20527
+ }
20528
+ function parseTriggers(meta) {
20529
+ const raw = meta.triggers;
20530
+ if (!Array.isArray(raw)) return [];
20531
+ const out = [];
20532
+ for (const item of raw) {
20533
+ if (item && typeof item === "object") {
20534
+ const t = item.type;
20535
+ if (typeof t === "string" && TRIGGER_TYPES.includes(t)) {
20536
+ const cfg = item.config;
20537
+ out.push({
20538
+ type: t,
20539
+ config: cfg && typeof cfg === "object" && !Array.isArray(cfg) ? cfg : void 0
20540
+ });
20541
+ }
20542
+ }
20543
+ }
20544
+ return out;
20545
+ }
20546
+ function parseBudget(meta) {
20547
+ const raw = meta.budget;
20548
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
20549
+ const o = raw;
20550
+ const out = {};
20551
+ for (const k of ["maxUsd", "maxTokens", "maxMs"]) {
20552
+ const v = o[k];
20553
+ if (typeof v === "number" && v > 0) out[k] = v;
20554
+ }
20555
+ return out;
20556
+ }
20557
+ function parseCanCall(meta) {
20558
+ const raw = meta.canCall;
20559
+ if (!Array.isArray(raw)) return [];
20560
+ return raw.filter((x) => typeof x === "string" && isValidAgentName(x));
20561
+ }
20562
+ function parseSettings(text) {
20563
+ try {
20564
+ const raw = JSON.parse(text);
20565
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
20566
+ const o = raw;
20567
+ return {
20568
+ model: typeof o.model === "string" ? o.model : void 0,
20569
+ temperature: typeof o.temperature === "number" ? o.temperature : void 0
20570
+ };
20571
+ }
20572
+ } catch {
20573
+ }
20574
+ return {};
20575
+ }
20576
+ async function loadCrewAgentAt(files, root, name, scope = "workspace") {
20577
+ if (!isValidAgentName(name)) throw new Error(`invalid crew agent name: ${name}`);
20578
+ const dir = `${root}/${name}`;
20579
+ const { meta, body } = parseFrontmatter2(await readOr(files, `${dir}/AGENT.md`));
20580
+ return {
20581
+ name,
20582
+ jobTitle: typeof meta.jobTitle === "string" ? meta.jobTitle : "",
20583
+ description: typeof meta.description === "string" ? meta.description : "",
20584
+ scope,
20585
+ enabled: meta.enabled !== false,
20586
+ // absent/anything-but-false -> enabled
20587
+ triggers: parseTriggers(meta),
20588
+ instructions: body,
20589
+ soul: await readOr(files, `${dir}/SOUL.md`),
20590
+ persona: await readOr(files, `${dir}/PERSONA.md`),
20591
+ policy: parsePolicyYaml(await readOr(files, `${dir}/POLICY.yaml`), name),
20592
+ settings: parseSettings(await readOr(files, `${dir}/settings.json`, "{}")),
20593
+ budget: parseBudget(meta),
20594
+ canCall: parseCanCall(meta)
20595
+ };
20596
+ }
20597
+ async function loadCrewAgentAtIfExists(files, root, name, scope = "workspace") {
20598
+ if (!isValidAgentName(name)) return null;
20599
+ try {
20600
+ const st = await files.stat(`${root}/${name}/AGENT.md`);
20601
+ if (st?.kind !== "file") return null;
20602
+ return await loadCrewAgentAt(files, root, name, scope);
20603
+ } catch {
20604
+ return null;
20605
+ }
20606
+ }
20607
+
20608
+ // ../harness/guardrail/serialize.ts
20609
+ var import_yaml4 = __toESM(require_dist(), 1);
20610
+
20611
+ // ../crew/runtime.ts
20612
+ function buildSystemExtra(def) {
20613
+ const parts = [];
20614
+ if (def.jobTitle?.trim()) parts.push(`--- Role ---
20615
+ ${def.jobTitle.trim()}`);
20616
+ if (def.soul.trim()) parts.push(`--- Soul ---
20617
+ ${def.soul.trim()}`);
20618
+ if (def.persona.trim()) parts.push(`--- Persona ---
20619
+ ${def.persona.trim()}`);
20620
+ if (def.instructions.trim()) parts.push(`--- Instructions ---
20621
+ ${def.instructions.trim()}`);
20622
+ return parts.join("\n\n");
20623
+ }
20624
+
20625
+ // src/runAgent.ts
20626
+ function workspaceStateDir() {
20627
+ const fromEnv = process.env.CORENEL_STATE_DIR;
20628
+ return fromEnv && /^\.[A-Za-z0-9][A-Za-z0-9._-]*$/.test(fromEnv) ? fromEnv : stateDirName();
20629
+ }
20630
+ function eventLine(e) {
20631
+ try {
20632
+ return JSON.stringify(e);
20633
+ } catch {
20634
+ const type = e.type;
20635
+ return JSON.stringify({ type: typeof type === "string" ? type : "unknown", unserializable: true });
20636
+ }
20637
+ }
20638
+ async function runCrewAgentCli(opts) {
20639
+ const files = new NodeFileService(opts.cwd);
20640
+ const root = `${workspaceStateDir()}/${CREW_SEG}`;
20641
+ const def = await loadCrewAgentAtIfExists(files, root, opts.agent, "workspace");
20642
+ if (!def) {
20643
+ opts.warn(`no such agent "${opts.agent}" under ${opts.cwd}/${root}`);
20644
+ return 2;
20645
+ }
20646
+ if (!def.enabled) {
20647
+ opts.warn(`agent "${opts.agent}" is disabled; enable it before running`);
20648
+ return 2;
20649
+ }
20650
+ setGatewayBase(opts.base ?? process.env.CORENEL_API_BASE ?? "https://api.corenel.ai/api");
20651
+ const token = await nodeAuthToken().getToken();
20652
+ if (!token) {
20653
+ opts.warn("not signed in: set CORENEL_TOKEN, or run `corenel login` on this machine");
20654
+ return 2;
20655
+ }
20656
+ registerGuard(tokenGuard(() => token));
20657
+ const tools = allTools();
20658
+ const extra = buildSystemExtra(def);
20659
+ let system = extra;
20660
+ try {
20661
+ const base = await composeAgentSystem({
20662
+ mode: "auto",
20663
+ tools: tools.map((t) => ({ name: t.name, description: t.description }))
20664
+ });
20665
+ system = extra ? `${base}
20666
+
20667
+ ${extra}` : base;
20668
+ } catch {
20669
+ }
20670
+ const realConsole = { log: console.log, info: console.info, warn: console.warn, debug: console.debug };
20671
+ const toStderr = (...parts) => {
20672
+ opts.warn(parts.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join(" "));
20673
+ };
20674
+ console.log = toStderr;
20675
+ console.info = toStderr;
20676
+ console.warn = toStderr;
20677
+ console.debug = toStderr;
20678
+ const ac = new AbortController();
20679
+ try {
20680
+ const result = await runAgent({
20681
+ client: createChatClient({ getToken: async () => token }),
20682
+ model: opts.model ?? def.settings.model ?? "gpt-4o-mini",
20683
+ system,
20684
+ messages: [{ role: "user", content: opts.input }],
20685
+ tools,
20686
+ toolCtx: nodeToolCtx(ac.signal, { files }),
20687
+ policy: def.policy,
20688
+ signal: ac.signal,
20689
+ onEvent: (e) => opts.emit(eventLine(e))
20690
+ });
20691
+ opts.emit(eventLine({ type: "done", text: result.text }));
20692
+ return 0;
20693
+ } catch (e) {
20694
+ opts.warn(`run failed: ${e instanceof Error ? e.message : String(e)}`);
20695
+ return 1;
20696
+ } finally {
20697
+ Object.assign(console, realConsole);
20698
+ }
20699
+ }
20700
+
20201
20701
  // src/cli.ts
20202
20702
  function flag(argv, name, fallback) {
20203
20703
  const i = argv.indexOf(`--${name}`);
20204
20704
  return i >= 0 && argv[i + 1] ? argv[i + 1] : fallback;
20205
20705
  }
20706
+ function positional(argv) {
20707
+ const out = [];
20708
+ for (let i = 0; i < argv.length; i++) {
20709
+ if (argv[i].startsWith("--")) {
20710
+ i++;
20711
+ continue;
20712
+ }
20713
+ out.push(argv[i]);
20714
+ }
20715
+ return out;
20716
+ }
20206
20717
  function promptOf(argv) {
20207
20718
  const parts = [];
20208
20719
  for (let i = 0; i < argv.length; i++) {
@@ -20253,8 +20764,28 @@ async function main() {
20253
20764
  case "run":
20254
20765
  await run(rest);
20255
20766
  break;
20767
+ case "run-agent": {
20768
+ const agent = positional(rest)[0];
20769
+ if (!agent) {
20770
+ process.stderr.write('usage: corenel run-agent <name> --input "<text>" [--model M] [--base URL]\n');
20771
+ process.exit(2);
20772
+ }
20773
+ const code = await runCrewAgentCli({
20774
+ cwd: process.cwd(),
20775
+ agent,
20776
+ input: flag(rest, "input", ""),
20777
+ ...rest.includes("--model") ? { model: flag(rest, "model", "") } : {},
20778
+ ...rest.includes("--base") ? { base: flag(rest, "base", "") } : {},
20779
+ emit: (line) => process.stdout.write(`${line}
20780
+ `),
20781
+ warn: (line) => process.stderr.write(`corenel: ${line}
20782
+ `)
20783
+ });
20784
+ process.exitCode = code;
20785
+ break;
20786
+ }
20256
20787
  case "login": {
20257
- const base = flag(rest, "base", process.env.CORENEL_API_BASE || "https://api.corenel.ai");
20788
+ const base = flag(rest, "base", process.env.CORENEL_API_BASE || "https://api.corenel.ai/api");
20258
20789
  await deviceLogin({ base, store: writeAuthToken });
20259
20790
  process.stdout.write(`
20260
20791
  Logged in. Token saved to ${authTokenPath()}
@@ -20267,6 +20798,7 @@ Logged in. Token saved to ${authTokenPath()}
20267
20798
  "",
20268
20799
  " corenel login [--base URL] OAuth device flow -> ~/.corenel/token",
20269
20800
  ' corenel run "<prompt>" [--model M] [--base URL] one-shot turn (needs a login or CORENEL_TOKEN)',
20801
+ ' corenel run-agent <name> --input "<text>" run a crew agent; events as JSON Lines on stdout',
20270
20802
  "",
20271
20803
  " (ask/chat + start --sidecar to follow.)",
20272
20804
  ""
@@ -20282,5 +20814,7 @@ function friendlyError(msg) {
20282
20814
  main().catch((e) => {
20283
20815
  process.stderr.write(`corenel: ${friendlyError(e instanceof Error ? e.message : String(e))}
20284
20816
  `);
20285
- process.exit(1);
20817
+ process.exitCode = 1;
20818
+ const bail = setTimeout(() => process.exit(1), 2e3);
20819
+ bail.unref();
20286
20820
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Corenel CLI — runs the harness in node, in-proc, no transport. The headless proof the kernel is host-agnostic. (corenel run/ask/chat/login + start --sidecar to follow.)",
6
6
  "bin": {
@@ -17,6 +17,7 @@
17
17
  "openai": "^4.77.0"
18
18
  },
19
19
  "devDependencies": {
20
+ "@corenel/crew": "workspace:*",
20
21
  "@corenel/harness": "workspace:*",
21
22
  "@corenel/protocol": "workspace:*",
22
23
  "@corenel/tools-node": "workspace:*",