@aiaiai-pt/martha-cli 0.27.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/index.js +330 -37
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ All notable changes to `@aiaiai-pt/martha-cli`. Format: [Keep a Changelog](https
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.30.0] — 2026-07-03
8
+
9
+ ### Added — #761 runner heartbeat + poll-based cancel
10
+ - `martha runner` now runs each task's goal **asynchronously with a heartbeat loop** (every `min(lease_timeout_s/3, 60)`s). Heartbeats renew the lease, so a task longer than its lease is no longer reclaimed by the lease reaper while still executing on the host (the Slice-0 double-execution hazard).
11
+ - Cancelling a claimed task (`martha tasks cancel <id>` or the admin route) now actually stops it: the heartbeat reply carries `signal: "cancel"`, the runner kills the goal's **entire process tree** (SIGTERM the group, SIGKILL after 5s), and reports the partial output with `status: "cancelled"` — accepted idempotently server-side. Worst-case kill latency is one heartbeat interval (≤60s).
12
+ - Heartbeat failures (network blips, 409s) never affect the running job — only an explicit `signal: "cancel"` kills.
13
+
14
+ ### Changed
15
+ - The runner no longer "backgrounds" goals after 20s (a Slice-0 artifact that reported long tasks as `completed` while they were still running) — it now waits for real completion, kept alive by the heartbeat loop.
16
+
17
+ <!-- 0.29.0 shipped 2026-07-02 (#751 asks + trigger-tasking) without a CHANGELOG section — see git history. -->
18
+
19
+ ## [0.28.0] — 2026-06-30
20
+
21
+ ### Added — #714 Slice 1 external-agent `event.emit` binding surfaces
22
+ - `martha agents grant-trigger <agent> <trigger>` binds an agent's emitted events to a trigger (so an agent-emitted event may fire it); `martha agents revoke-trigger <agent> <trigger>` removes the binding; `martha agents triggers <agent>` lists the triggers an agent may fire.
23
+ - `martha triggers allow-agent <trigger> <agentId>` accepts an agent as a source for a trigger; `martha triggers disallow-agent <trigger> <agentId>` removes it; `martha triggers agents <trigger>` lists the accepted agent sources.
24
+ - Both directions mutate the same `agent_trigger_access` binding (#714) and reflect each other. All support `--json`. The trigger is resolved by name → id; the agent side keys on the agent's stored (server-normalized) name and the agent id, respectively. Cross-tenant agent/trigger → 404 (IDOR guard).
25
+
7
26
  ## [0.20.0] — 2026-06-21
8
27
 
9
28
  ### Added — #649 agent→agent delegation surfaces
package/dist/index.js CHANGED
@@ -5,43 +5,25 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- function __accessProp(key) {
9
- return this[key];
10
- }
11
- var __toESMCache_node;
12
- var __toESMCache_esm;
13
8
  var __toESM = (mod, isNodeMode, target) => {
14
- var canCache = mod != null && typeof mod === "object";
15
- if (canCache) {
16
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
- var cached = cache.get(mod);
18
- if (cached)
19
- return cached;
20
- }
21
9
  target = mod != null ? __create(__getProtoOf(mod)) : {};
22
10
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
11
  for (let key of __getOwnPropNames(mod))
24
12
  if (!__hasOwnProp.call(to, key))
25
13
  __defProp(to, key, {
26
- get: __accessProp.bind(mod, key),
14
+ get: () => mod[key],
27
15
  enumerable: true
28
16
  });
29
- if (canCache)
30
- cache.set(mod, to);
31
17
  return to;
32
18
  };
33
19
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
- var __returnValue = (v) => v;
35
- function __exportSetter(name, newValue) {
36
- this[name] = __returnValue.bind(null, newValue);
37
- }
38
20
  var __export = (target, all) => {
39
21
  for (var name in all)
40
22
  __defProp(target, name, {
41
23
  get: all[name],
42
24
  enumerable: true,
43
25
  configurable: true,
44
- set: __exportSetter.bind(all, name)
26
+ set: (newValue) => all[name] = () => newValue
45
27
  });
46
28
  };
47
29
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -1019,7 +1001,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1019
1001
  this._exitCallback = (err) => {
1020
1002
  if (err.code !== "commander.executeSubCommandAsync") {
1021
1003
  throw err;
1022
- }
1004
+ } else {}
1023
1005
  };
1024
1006
  }
1025
1007
  return this;
@@ -11081,7 +11063,7 @@ init_config();
11081
11063
  init_errors();
11082
11064
 
11083
11065
  // src/version.ts
11084
- var CLI_VERSION = "0.27.1";
11066
+ var CLI_VERSION = "0.30.0";
11085
11067
 
11086
11068
  // src/commands/sessions.ts
11087
11069
  function relativeTime(iso) {
@@ -11439,6 +11421,68 @@ async function runHostCommand(command, toolCallId, opts) {
11439
11421
  function safeName(s) {
11440
11422
  return s.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 120) || "task";
11441
11423
  }
11424
+ async function startHostCommand(command, toolCallId, opts) {
11425
+ const dir = runnerDir(opts.cwd);
11426
+ await fs5.mkdir(dir, { recursive: true });
11427
+ const logPath = path4.join(dir, `${safeName(toolCallId)}.log`);
11428
+ await fs5.writeFile(logPath, "");
11429
+ const fd = openSync(logPath, "a");
11430
+ const cap = opts.maxOutputBytes ?? MAX_OUTPUT;
11431
+ const readLog = async () => {
11432
+ try {
11433
+ const buf = await fs5.readFile(logPath);
11434
+ return buf.subarray(-cap).toString("utf8");
11435
+ } catch {
11436
+ return "";
11437
+ }
11438
+ };
11439
+ const child = spawn(command, {
11440
+ shell: true,
11441
+ cwd: opts.cwd,
11442
+ detached: true,
11443
+ stdio: ["ignore", fd, fd]
11444
+ });
11445
+ closeSync(fd);
11446
+ let exited = false;
11447
+ const done = new Promise((resolve, reject) => {
11448
+ let settled = false;
11449
+ child.on("error", (e) => {
11450
+ if (settled)
11451
+ return;
11452
+ settled = true;
11453
+ exited = true;
11454
+ reject(e);
11455
+ });
11456
+ child.on("close", async (code) => {
11457
+ if (settled)
11458
+ return;
11459
+ settled = true;
11460
+ exited = true;
11461
+ resolve({
11462
+ stdout: await readLog(),
11463
+ stderr: "",
11464
+ exit_code: code ?? -1,
11465
+ log_path: logPath
11466
+ });
11467
+ });
11468
+ });
11469
+ const kill = (graceMs = 5000) => {
11470
+ if (exited || !child.pid)
11471
+ return;
11472
+ try {
11473
+ process.kill(-child.pid, "SIGTERM");
11474
+ } catch {}
11475
+ const escalate = setTimeout(() => {
11476
+ if (exited || !child.pid)
11477
+ return;
11478
+ try {
11479
+ process.kill(-child.pid, "SIGKILL");
11480
+ } catch {}
11481
+ }, graceMs);
11482
+ escalate.unref?.();
11483
+ };
11484
+ return { pid: child.pid, done, kill };
11485
+ }
11442
11486
  function mintExecSecret() {
11443
11487
  return randomBytes2(32).toString("hex");
11444
11488
  }
@@ -13512,7 +13556,7 @@ function registerConfigCommand(program2) {
13512
13556
  config.current_profile = name;
13513
13557
  }
13514
13558
  saveConfig(config);
13515
- if (!!program2.opts().json) {
13559
+ if (program2.opts().json) {
13516
13560
  console.log(JSON.stringify({
13517
13561
  name,
13518
13562
  profile,
@@ -13556,7 +13600,7 @@ function registerConfigCommand(program2) {
13556
13600
  config.current_profile = remaining[0] ?? "default";
13557
13601
  }
13558
13602
  saveConfig(config);
13559
- if (!!program2.opts().json) {
13603
+ if (program2.opts().json) {
13560
13604
  console.log(JSON.stringify({ name, deleted: true }));
13561
13605
  return;
13562
13606
  }
@@ -15066,6 +15110,50 @@ var agentsConfig = {
15066
15110
  }
15067
15111
  console.log(`Removed function '${functionName}' from agent '${agent}'`);
15068
15112
  });
15113
+ parentCmd.command("grant-trigger <agent> <trigger>").description("Allow an agent's emitted events to fire a trigger").action(async (agent, trigger) => {
15114
+ const ctx = getCtx();
15115
+ const t = await ctx.api.get(`/api/admin/triggers/${encodeURIComponent(trigger)}`);
15116
+ const result = await ctx.api.post(`${API_PATH}/${encodeURIComponent(agent)}/triggers`, { trigger_id: t.id });
15117
+ if (isJson()) {
15118
+ console.log(JSON.stringify(result, null, 2));
15119
+ return;
15120
+ }
15121
+ console.log(`Granted trigger '${trigger}' to agent '${agent}'`);
15122
+ });
15123
+ parentCmd.command("revoke-trigger <agent> <trigger>").description("Revoke an agent's binding to a trigger").action(async (agent, trigger) => {
15124
+ const ctx = getCtx();
15125
+ const t = await ctx.api.get(`/api/admin/triggers/${encodeURIComponent(trigger)}`);
15126
+ await ctx.api.del(`${API_PATH}/${encodeURIComponent(agent)}/triggers/${encodeURIComponent(String(t.id))}`);
15127
+ if (isJson()) {
15128
+ console.log(JSON.stringify({ agent, trigger, removed: true }));
15129
+ return;
15130
+ }
15131
+ console.log(`Revoked trigger '${trigger}' from agent '${agent}'`);
15132
+ });
15133
+ parentCmd.command("triggers <agent>").description("List triggers an agent may fire").action(async (agent) => {
15134
+ const ctx = getCtx();
15135
+ const items = await ctx.api.get(`${API_PATH}/${encodeURIComponent(agent)}/triggers`);
15136
+ if (isJson()) {
15137
+ console.log(JSON.stringify(items, null, 2));
15138
+ return;
15139
+ }
15140
+ if (items.length === 0) {
15141
+ console.log(source_default.dim("No triggers bound to this agent."));
15142
+ return;
15143
+ }
15144
+ const nameW = Math.max(7, ...items.map((b) => String(b.trigger_name ?? "").length));
15145
+ const header = ["TRIGGER", "GRANTED BY", "GRANTED AT"].map((h, i) => h.padEnd([nameW, 16, 20][i])).join(" ");
15146
+ console.log(source_default.bold(header));
15147
+ console.log(source_default.dim("-".repeat(header.length)));
15148
+ for (const b of items) {
15149
+ const date = b.granted_at ? new Date(b.granted_at).toISOString().slice(0, 16).replace("T", " ") : "-";
15150
+ console.log([
15151
+ String(b.trigger_name ?? "").padEnd(nameW),
15152
+ String(b.granted_by ?? "-").padEnd(16),
15153
+ date.padEnd(20)
15154
+ ].join(" "));
15155
+ }
15156
+ });
15069
15157
  parentCmd.command("provision-auth <agent>").description("Provision, switch, or rotate agent auth credentials").requiredOption("--method <method>", "Auth method: service-account or api-key").action(async (agent, opts) => {
15070
15158
  const ctx = getCtx();
15071
15159
  const methodMap = {
@@ -15495,6 +15583,51 @@ var triggersConfig = {
15495
15583
  }
15496
15584
  }
15497
15585
  });
15586
+ parentCmd.command("allow-agent <trigger> <agentId>").description("Accept an agent's emitted events as a source for a trigger").action(async (trigger, agentId) => {
15587
+ const ctx = getCtx();
15588
+ const t = await ctx.api.get(`/api/admin/triggers/${encodeURIComponent(trigger)}`);
15589
+ const result = await ctx.api.post(`/api/admin/triggers/${encodeURIComponent(String(t.id))}/agents`, { agent_id: agentId });
15590
+ if (isJson()) {
15591
+ console.log(JSON.stringify(result, null, 2));
15592
+ return;
15593
+ }
15594
+ console.log(`Accepted agent '${agentId}' as a source for '${trigger}'`);
15595
+ });
15596
+ parentCmd.command("disallow-agent <trigger> <agentId>").description("Remove an agent as a source for a trigger").action(async (trigger, agentId) => {
15597
+ const ctx = getCtx();
15598
+ const t = await ctx.api.get(`/api/admin/triggers/${encodeURIComponent(trigger)}`);
15599
+ await ctx.api.del(`/api/admin/triggers/${encodeURIComponent(String(t.id))}/agents/${encodeURIComponent(agentId)}`);
15600
+ if (isJson()) {
15601
+ console.log(JSON.stringify({ trigger, agent_id: agentId, removed: true }));
15602
+ return;
15603
+ }
15604
+ console.log(`Removed agent '${agentId}' as a source for '${trigger}'`);
15605
+ });
15606
+ parentCmd.command("agents <trigger>").description("List the agents accepted as sources for a trigger").action(async (trigger) => {
15607
+ const ctx = getCtx();
15608
+ const t = await ctx.api.get(`/api/admin/triggers/${encodeURIComponent(trigger)}`);
15609
+ const items = await ctx.api.get(`/api/admin/triggers/${encodeURIComponent(String(t.id))}/agents`);
15610
+ if (isJson()) {
15611
+ console.log(JSON.stringify(items, null, 2));
15612
+ return;
15613
+ }
15614
+ if (items.length === 0) {
15615
+ console.log(source_default.dim("No agent sources accepted for this trigger."));
15616
+ return;
15617
+ }
15618
+ const nameW = Math.max(5, ...items.map((b) => String(b.agent_name ?? "").length));
15619
+ const header = ["AGENT", "AGENT ID", "GRANTED AT"].map((h, i) => h.padEnd([nameW, 36, 20][i])).join(" ");
15620
+ console.log(source_default.bold(header));
15621
+ console.log(source_default.dim("-".repeat(header.length)));
15622
+ for (const b of items) {
15623
+ const date = b.granted_at ? new Date(b.granted_at).toISOString().slice(0, 16).replace("T", " ") : "-";
15624
+ console.log([
15625
+ String(b.agent_name ?? "-").padEnd(nameW),
15626
+ String(b.agent_id ?? "-").padEnd(36),
15627
+ date.padEnd(20)
15628
+ ].join(" "));
15629
+ }
15630
+ });
15498
15631
  }
15499
15632
  };
15500
15633
 
@@ -16488,6 +16621,119 @@ function registerApprovalCommands(program2) {
16488
16621
  });
16489
16622
  }
16490
16623
 
16624
+ // src/commands/asks.ts
16625
+ init_errors();
16626
+ var KINDS = ["agent_question", "agent_approval_request"];
16627
+ function statusColor(status) {
16628
+ if (status === "approved")
16629
+ return source_default.green(status);
16630
+ if (status === "rejected")
16631
+ return source_default.red(status);
16632
+ if (status === "expired")
16633
+ return source_default.dim(status);
16634
+ return source_default.yellow(status);
16635
+ }
16636
+ function renderAsk(a) {
16637
+ const lines = [
16638
+ `${source_default.bold(a.case_id ?? "?")} ${statusColor(a.status ?? "?")} ${source_default.dim(a.kind ?? "")}`,
16639
+ ` ${a.reason ?? ""}`
16640
+ ];
16641
+ if (a.context_summary)
16642
+ lines.push(` ${source_default.dim(a.context_summary)}`);
16643
+ if (a.decision) {
16644
+ lines.push(` ${source_default.bold("decision:")} ${statusColor(a.decision)}`);
16645
+ if (a.comment)
16646
+ lines.push(` ${source_default.bold("answer:")} ${a.comment}`);
16647
+ }
16648
+ if (a.expires_at)
16649
+ lines.push(` ${source_default.dim(`expires ${a.expires_at}`)}`);
16650
+ return lines.join(`
16651
+ `);
16652
+ }
16653
+ function registerAskCommands(program2) {
16654
+ const cmd = program2.command("asks").description("Agent asks — questions/approval requests to the human queue (agent credentials)");
16655
+ function getCtx() {
16656
+ const ctx = createContext({
16657
+ profileOverride: program2.opts().profile,
16658
+ verbose: program2.opts().verbose
16659
+ });
16660
+ if (program2.opts().apiUrl)
16661
+ ctx.profile.api_url = program2.opts().apiUrl;
16662
+ return ctx;
16663
+ }
16664
+ function isJson() {
16665
+ return !!program2.opts().json;
16666
+ }
16667
+ cmd.command("create").description("File an ask (the server attributes it to the token's agent)").requiredOption("--reason <text>", "The question / what approval is requested").option("--kind <kind>", `${KINDS.join(" | ")} (default agent_question)`, "agent_question").option("--context <text>", "Context summary for the reviewer").option("--questions <json>", "Structured questions JSON array (ask_user shape: question/header/options/multiSelect)").option("--expires-in <seconds>", "Auto-expire the ask after N seconds").option("--idempotency-key <key>", "Dedup key — retries with the same key do not double-file").action(async (opts) => {
16668
+ if (!KINDS.includes(opts.kind)) {
16669
+ throw new CLIError(`--kind must be one of: ${KINDS.join(", ")}`, 4 /* Validation */);
16670
+ }
16671
+ let questions;
16672
+ if (opts.questions) {
16673
+ try {
16674
+ questions = JSON.parse(opts.questions);
16675
+ } catch {
16676
+ throw new CLIError("--questions must be valid JSON", 4 /* Validation */);
16677
+ }
16678
+ }
16679
+ let expiresIn;
16680
+ if (opts.expiresIn) {
16681
+ expiresIn = parseInt(opts.expiresIn, 10);
16682
+ if (isNaN(expiresIn) || expiresIn < 60) {
16683
+ throw new CLIError("--expires-in must be an integer >= 60 (seconds)", 4 /* Validation */);
16684
+ }
16685
+ }
16686
+ const ctx = getCtx();
16687
+ const ask = await ctx.api.post("/api/agent/asks", {
16688
+ kind: opts.kind,
16689
+ reason: opts.reason,
16690
+ context_summary: opts.context,
16691
+ questions,
16692
+ expires_in_s: expiresIn,
16693
+ idempotency_key: opts.idempotencyKey
16694
+ });
16695
+ if (isJson()) {
16696
+ console.log(JSON.stringify(ask, null, 2));
16697
+ return;
16698
+ }
16699
+ console.log(`Filed ask ${source_default.bold(ask.case_id)} (${ask.status})`);
16700
+ console.log(source_default.dim(`Poll the answer with: martha asks get ${ask.case_id}`));
16701
+ });
16702
+ cmd.command("list").description("List this agent's own asks (newest first)").option("--status <status>", "Filter by status (pending/approved/rejected/expired)").option("--limit <n>", "Max results", "50").action(async (opts) => {
16703
+ const limitN = parseInt(opts.limit, 10);
16704
+ if (isNaN(limitN) || limitN < 1) {
16705
+ throw new CLIError("--limit must be a positive integer", 4 /* Validation */);
16706
+ }
16707
+ const ctx = getCtx();
16708
+ const params = { limit: String(limitN) };
16709
+ if (opts.status)
16710
+ params.status = opts.status;
16711
+ const items = await ctx.api.get("/api/agent/asks", {
16712
+ params
16713
+ });
16714
+ if (isJson()) {
16715
+ console.log(JSON.stringify(items, null, 2));
16716
+ return;
16717
+ }
16718
+ if (items.length === 0) {
16719
+ console.log(source_default.dim("No asks."));
16720
+ return;
16721
+ }
16722
+ console.log(items.map(renderAsk).join(`
16723
+
16724
+ `));
16725
+ });
16726
+ cmd.command("get <caseId>").description("Show one own ask — status, and decision+answer once resolved").action(async (caseId) => {
16727
+ const ctx = getCtx();
16728
+ const ask = await ctx.api.get(`/api/agent/asks/${caseId}`);
16729
+ if (isJson()) {
16730
+ console.log(JSON.stringify(ask, null, 2));
16731
+ return;
16732
+ }
16733
+ console.log(renderAsk(ask));
16734
+ });
16735
+ }
16736
+
16491
16737
  // src/commands/tasks.ts
16492
16738
  import { readFileSync as readFileSync2 } from "node:fs";
16493
16739
  init_errors();
@@ -16508,7 +16754,7 @@ var sleep4 = (ms, signal) => new Promise((resolve2) => {
16508
16754
  signal?.addEventListener("abort", onAbort, { once: true });
16509
16755
  });
16510
16756
  var fmtDate2 = (v) => typeof v === "string" ? new Date(v).toISOString().slice(0, 16).replace("T", " ") : "-";
16511
- var statusColor = (status) => {
16757
+ var statusColor2 = (status) => {
16512
16758
  switch (status) {
16513
16759
  case "open":
16514
16760
  return source_default.yellow(status);
@@ -16611,7 +16857,7 @@ function registerTaskCommands(program2) {
16611
16857
  const row = columns.map((col, i) => {
16612
16858
  let val = col.accessor(item);
16613
16859
  if (col.header === "STATUS")
16614
- val = statusColor(val);
16860
+ val = statusColor2(val);
16615
16861
  if (col.header === "PRIORITY")
16616
16862
  val = priorityColor(val);
16617
16863
  return col.header === "STATUS" || col.header === "PRIORITY" ? val.padEnd(widths[i] + (val.length - col.accessor(item).length)) : val.padEnd(widths[i]);
@@ -16660,7 +16906,7 @@ ${items.length} task(s)`));
16660
16906
  return;
16661
16907
  }
16662
16908
  console.log(`Created task ${source_default.cyan(String(result.id ?? ""))}`);
16663
- console.log(` Status: ${statusColor(String(result.status ?? "-"))}`);
16909
+ console.log(` Status: ${statusColor2(String(result.status ?? "-"))}`);
16664
16910
  console.log(` Priority: ${priorityColor(String(result.priority ?? "-"))}`);
16665
16911
  console.log(` Goal: ${truncate2(String(result.goal ?? "-"), 60)}`);
16666
16912
  });
@@ -16674,7 +16920,7 @@ ${items.length} task(s)`));
16674
16920
  console.log(source_default.bold("Task Details"));
16675
16921
  console.log(source_default.dim("-".repeat(50)));
16676
16922
  console.log(`${source_default.cyan("ID:")} ${item.id ?? "-"}`);
16677
- console.log(`${source_default.cyan("Status:")} ${statusColor(String(item.status ?? "-"))}`);
16923
+ console.log(`${source_default.cyan("Status:")} ${statusColor2(String(item.status ?? "-"))}`);
16678
16924
  console.log(`${source_default.cyan("Priority:")} ${priorityColor(String(item.priority ?? "-"))}`);
16679
16925
  console.log(`${source_default.cyan("Title:")} ${item.title ?? "-"}`);
16680
16926
  console.log(`${source_default.cyan("Agent:")} ${item.agent_definition_id ?? "-"}`);
@@ -16785,7 +17031,7 @@ ${items.length} task(s) available`));
16785
17031
  return;
16786
17032
  }
16787
17033
  console.log(`Claimed task ${source_default.cyan(String(result.id ?? "").slice(0, 8))}`);
16788
- console.log(` Status: ${statusColor(String(result.status ?? "-"))}`);
17034
+ console.log(` Status: ${statusColor2(String(result.status ?? "-"))}`);
16789
17035
  console.log(` Executor: ${result.executor_id ?? "-"}`);
16790
17036
  console.log(` Lease: expires ${fmtDate2(result.lease_expires_at)}`);
16791
17037
  });
@@ -16958,7 +17204,7 @@ ${items.length} task(s) available`));
16958
17204
  return;
16959
17205
  }
16960
17206
  console.log(`Completed task ${source_default.cyan(String(result.id ?? "").slice(0, 8))}`);
16961
- console.log(` Status: ${statusColor(String(result.status ?? "-"))}`);
17207
+ console.log(` Status: ${statusColor2(String(result.status ?? "-"))}`);
16962
17208
  console.log(` Completed at: ${fmtDate2(result.completed_at)}`);
16963
17209
  });
16964
17210
  }
@@ -16968,6 +17214,17 @@ init_errors();
16968
17214
  function sleep5(ms) {
16969
17215
  return new Promise((r) => setTimeout(r, ms));
16970
17216
  }
17217
+ function sleepUnref(ms) {
17218
+ return new Promise((r) => {
17219
+ const t = setTimeout(r, ms);
17220
+ t.unref?.();
17221
+ });
17222
+ }
17223
+ var DEFAULT_LEASE_S = 300;
17224
+ function heartbeatIntervalMs(leaseTimeoutS) {
17225
+ const lease = typeof leaseTimeoutS === "number" && leaseTimeoutS > 0 ? leaseTimeoutS : DEFAULT_LEASE_S;
17226
+ return Math.min(lease / 3, 60) * 1000;
17227
+ }
16971
17228
  async function runOnce(ctx, opts, handled) {
16972
17229
  const tasks = await ctx.api.get("/api/tasks/poll", {
16973
17230
  params: { agent_id: opts.agent, limit: String(opts.pollLimit ?? 5) }
@@ -16985,20 +17242,55 @@ async function runOnce(ctx, opts, handled) {
16985
17242
  handled?.add(task.id);
16986
17243
  const claimed = await ctx.api.post(`/api/tasks/${encodeURIComponent(task.id)}/claim`, {});
16987
17244
  const goal = claimed.goal ?? task.goal ?? "";
17245
+ const intervalMs = heartbeatIntervalMs(claimed.lease_timeout_s ?? DEFAULT_LEASE_S);
16988
17246
  process.stderr.write(source_default.dim(`
16989
- [runner] claimed ${task.id} — running: ${goal}
17247
+ [runner] claimed ${task.id} — running: ${goal} (heartbeat every ${Math.round(intervalMs / 1000)}s)
16990
17248
  `));
16991
17249
  let result;
17250
+ let cancelled = false;
16992
17251
  try {
16993
- result = await runHostCommand(goal, `task-${task.id}`, { cwd: opts.cwd });
17252
+ const handle = await startHostCommand(goal, `task-${task.id}`, {
17253
+ cwd: opts.cwd
17254
+ });
17255
+ const done = handle.done.catch((e) => ({
17256
+ stdout: "",
17257
+ stderr: String(e),
17258
+ exit_code: -1
17259
+ }));
17260
+ for (;; ) {
17261
+ const raced = await Promise.race([
17262
+ done,
17263
+ sleepUnref(intervalMs).then(() => null)
17264
+ ]);
17265
+ if (raced !== null) {
17266
+ result = raced;
17267
+ break;
17268
+ }
17269
+ let reply;
17270
+ try {
17271
+ reply = await ctx.api.post(`/api/tasks/${encodeURIComponent(task.id)}/heartbeat`, {});
17272
+ } catch (e) {
17273
+ process.stderr.write(source_default.yellow(`[runner] heartbeat failed for ${task.id} (job unaffected): ${String(e)}
17274
+ `));
17275
+ continue;
17276
+ }
17277
+ if (reply && reply.signal === "cancel") {
17278
+ process.stderr.write(source_default.yellow(`[runner] cancel signal for ${task.id} — killing process tree
17279
+ `));
17280
+ handle.kill();
17281
+ result = await done;
17282
+ cancelled = true;
17283
+ break;
17284
+ }
17285
+ }
16994
17286
  } catch (e) {
16995
17287
  result = { stdout: "", stderr: String(e), exit_code: -1 };
16996
17288
  }
16997
17289
  const exit_code = result.exit_code;
16998
17290
  const stdout = result.stdout || result.stderr || "";
16999
- const status = exit_code === 0 ? "completed" : "failed";
17291
+ const status = cancelled ? "cancelled" : exit_code === 0 ? "completed" : "failed";
17000
17292
  await ctx.api.post(`/api/tasks/${encodeURIComponent(task.id)}/complete`, {
17001
- outcome: { stdout, exit_code },
17293
+ outcome: cancelled ? { stdout, exit_code, cancelled: true } : { stdout, exit_code },
17002
17294
  status
17003
17295
  });
17004
17296
  process.stderr.write(source_default.dim(`[runner] completed ${task.id} — ${status} (exit ${exit_code})
@@ -19970,7 +20262,7 @@ async function resolveCredentialFlag(value) {
19970
20262
  // src/commands/policy.ts
19971
20263
  init_errors();
19972
20264
  var ACTIONS = ["allow", "require_approval", "block"];
19973
- var KINDS = ["risk_tag", "risk_level", "capability"];
20265
+ var KINDS2 = ["risk_tag", "risk_level", "capability"];
19974
20266
  var SCOPES = ["any", "agent", "client"];
19975
20267
  var truncate4 = (s, n) => s.length > n ? s.slice(0, n - 1) + "…" : s;
19976
20268
  function assertOneOf(value, allowed, flag) {
@@ -20066,8 +20358,8 @@ function registerPolicyCommands(program2) {
20066
20358
  console.log(`Safe-default bundle ${enabled ? source_default.yellow("enabled") : source_default.dim("disabled")}.`);
20067
20359
  });
20068
20360
  const rule = cmd.command("rule").description("Manage explicit policy rules");
20069
- rule.command("add").description("Create or update a rule").requiredOption(`--kind <kind>`, `Match kind (${KINDS.join("|")})`).requiredOption("--value <value>", "Match value (a risk tag, level, or capability ref)").requiredOption(`--action <action>`, `Action (${ACTIONS.join("|")})`).option(`--scope <scope>`, `Principal scope (${SCOPES.join("|")})`, "any").option("--note <text>", "Optional human note").action(async (opts) => {
20070
- assertOneOf(opts.kind, KINDS, "--kind");
20361
+ rule.command("add").description("Create or update a rule").requiredOption(`--kind <kind>`, `Match kind (${KINDS2.join("|")})`).requiredOption("--value <value>", "Match value (a risk tag, level, or capability ref)").requiredOption(`--action <action>`, `Action (${ACTIONS.join("|")})`).option(`--scope <scope>`, `Principal scope (${SCOPES.join("|")})`, "any").option("--note <text>", "Optional human note").action(async (opts) => {
20362
+ assertOneOf(opts.kind, KINDS2, "--kind");
20071
20363
  assertOneOf(opts.action, ACTIONS, "--action");
20072
20364
  assertOneOf(opts.scope, SCOPES, "--scope");
20073
20365
  const ctx = getCtx();
@@ -21363,6 +21655,7 @@ registerDocumentCommands(program2);
21363
21655
  registerDocumentSyncCommands(program2);
21364
21656
  registerWikiCommands(program2);
21365
21657
  registerApprovalCommands(program2);
21658
+ registerAskCommands(program2);
21366
21659
  registerTaskCommands(program2);
21367
21660
  registerRunnerCommand(program2);
21368
21661
  registerTeamCommands(program2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiaiai-pt/martha-cli",
3
- "version": "0.27.1",
3
+ "version": "0.30.0",
4
4
  "description": "Terminal-first client for the Martha AI platform",
5
5
  "homepage": "https://docs.martha.nomadriver.co",
6
6
  "repository": {