agentradar-cli 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/admin.js CHANGED
@@ -1,21 +1,29 @@
1
+ import { readFileSync } from "node:fs";
1
2
  /**
2
- * ADMIN-BROADCAST (Vadym 12310/12828): `agentradar admin broadcast` for an instance admin. Authenticates with a board
3
+ * ADMIN-BROADCAST (Vadym 12310/12828): `agentradar admin broadcast` for an instance admin; REPORTER-NOTICE-ROUTE: `admin notice` to one agent. Authenticates with a board
3
4
  * API key in AGENTRADAR_ADMIN_TOKEN, never a runner key; the server refuses anyone who is not an instance admin.
4
5
  */
5
6
  export const ADMIN_BROADCAST_USAGE = "Usage: agentradar admin broadcast --dry-run|--send --target all --text <text> --cta <next step>"
6
7
  + " (env AGENTRADAR_ADMIN_TOKEN = an instance admin's board API key; AGENTRADAR_API_URL = the API base)";
8
+ export const ADMIN_NOTICE_USAGE = "Usage: agentradar admin notice --dry-run|--send --agent <id> --ref <key> --file <text file> --cta <next step, naming the tool; a retry must say to check first> [--wake]"
9
+ + " (a notice is delivered once per --ref; --wake also wakes the agent; same env as broadcast)";
7
10
  export async function adminOverCli(args, deps = {}) {
8
11
  const env = deps.env ?? process.env, log = deps.log ?? console.log, logError = deps.logError ?? console.error;
12
+ const sub = args[0], notice = sub === "notice";
9
13
  const named = {};
10
14
  const modes = args.slice(1).filter((arg) => arg === "--dry-run" || arg === "--send");
15
+ let wake = false;
11
16
  for (let i = 1; i < args.length; i++) {
12
- if (["--target", "--text", "--cta"].includes(args[i]))
17
+ if ((notice ? ["--agent", "--ref", "--file", "--cta"] : ["--target", "--text", "--cta"]).includes(args[i]))
13
18
  named[args[i].slice(2)] = args[++i] ?? "";
19
+ else if (notice && args[i] === "--wake")
20
+ wake = true;
14
21
  else if (!modes.includes(args[i]))
15
22
  modes.push("?");
16
23
  }
17
- if (args[0] !== "broadcast" || modes.length !== 1 || modes[0] === "?" || named.target !== "all" || !named.text || !named.cta) {
18
- logError(ADMIN_BROADCAST_USAGE);
24
+ const complete = notice ? named.agent && named.ref && named.file && named.cta : named.target === "all" && named.text && named.cta;
25
+ if ((sub !== "broadcast" && !notice) || modes.length !== 1 || modes[0] === "?" || !complete) {
26
+ logError(notice ? ADMIN_NOTICE_USAGE : ADMIN_BROADCAST_USAGE);
19
27
  return 2;
20
28
  }
21
29
  const token = env.AGENTRADAR_ADMIN_TOKEN?.trim();
@@ -24,9 +32,21 @@ export async function adminOverCli(args, deps = {}) {
24
32
  return 2;
25
33
  }
26
34
  const base = (env.AGENTRADAR_API_URL ?? "https://vendor.agentradar.com/api").replace(/\/+$/, "");
27
- const res = await (deps.fetch ?? fetch)(`${base}/admin/agent-broadcast`, {
28
- method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
29
- body: JSON.stringify({ target: "all", text: named.text, cta: named.cta, dryRun: modes[0] === "--dry-run" }),
35
+ const dryRun = modes[0] === "--dry-run";
36
+ let body;
37
+ if (notice) {
38
+ try {
39
+ body = { agentId: named.agent, ref: named.ref, text: (deps.readFile ?? ((f) => readFileSync(f, "utf8")))(named.file), cta: named.cta, wake, dryRun };
40
+ }
41
+ catch {
42
+ logError(`Cannot read ${named.file}`);
43
+ return 2;
44
+ }
45
+ }
46
+ else
47
+ body = { target: "all", text: named.text, cta: named.cta, dryRun };
48
+ const res = await (deps.fetch ?? fetch)(`${base}/admin/${notice ? "agent-notice" : "agent-broadcast"}`, {
49
+ method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(body),
30
50
  });
31
51
  const text = await res.text();
32
52
  if (!res.ok) {
@@ -41,9 +41,11 @@ export async function reportCompaction(event, input, { send, followUp, stateDir
41
41
  if (input?.hook_event_name !== event || !ID.test(input.session_id ?? "")) return;
42
42
  const marker = join(stateDir, `agentradar-compaction-${input.session_id}.json`);
43
43
  const trigger = typeof input.trigger === "string" ? input.trigger.slice(0, 40) : null;
44
+ // Every report names its session: several sessions of one agent compacting at once read as one loop without it.
45
+ const sessionId = input.session_id;
44
46
  if (event === "PreCompact") {
45
47
  writeFileSync(marker, JSON.stringify({ at: now() }), { mode: 0o600 });
46
- return send({ phase: "start", trigger });
48
+ return send({ phase: "start", sessionId, trigger });
47
49
  }
48
50
  if (event !== "PostCompact") return;
49
51
  let startedAt = now();
@@ -51,21 +53,25 @@ export async function reportCompaction(event, input, { send, followUp, stateDir
51
53
  rmSync(marker, { force: true });
52
54
  // The compact_boundary line (the only source of token counts) is written after this hook returns, so it is
53
55
  // reported separately, by a detached follow-up; this report must not wait for it.
54
- send({ phase: "end", outcome: "ok", trigger, durationMs: now() - startedAt });
55
- return followUp?.({ transcriptPath: input.transcript_path, startedAt, trigger });
56
+ send({ phase: "end", outcome: "ok", sessionId, trigger, durationMs: now() - startedAt });
57
+ return followUp?.({ transcriptPath: input.transcript_path, startedAt, sessionId, trigger });
56
58
  }
57
59
 
58
- /** Detached follow-up: waits up to `waitMs` for the boundary this compaction wrote, then reports its token counts. */
59
- export async function reportTokens({ transcriptPath, startedAt, trigger }, { send, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), waitMs = 60_000, pollMs = 500 }) {
60
+ /**
61
+ * Detached follow-up: waits up to `waitMs` for the boundary this compaction wrote, then reports its token counts. No
62
+ * boundary in time is reported too (boundary "not_found"), so a missing tokens row means the follow-up never ran.
63
+ */
64
+ export async function reportTokens({ transcriptPath, startedAt, sessionId, trigger }, { send, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), waitMs = 60_000, pollMs = 500 }) {
60
65
  for (let waited = 0; waited <= waitMs; waited += pollMs) {
61
66
  const boundary = lastBoundary(transcriptPath, startedAt - 1_000);
62
- if (boundary) return send({ phase: "tokens", trigger, ...boundary });
67
+ if (boundary) return send({ phase: "tokens", sessionId, trigger, ...boundary });
63
68
  await sleep(pollMs);
64
69
  }
70
+ return send({ phase: "tokens", sessionId, trigger, boundary: "not_found" });
65
71
  }
66
72
 
67
73
  const sendToServer = (report) => spawnSync(process.env.AR_AGENTRADAR_BIN || "agentradar",
68
- ["call", process.env.AR_CLI_RUNNER_ID || "hosted", "report_compaction", "--args", JSON.stringify(report)], { encoding: "utf8", timeout: 20_000 });
74
+ ["call", process.env.AR_CLI_RUNNER_ID || "hosted", "report_compaction", "--input", JSON.stringify(report)], { encoding: "utf8", timeout: 20_000 });
69
75
  const spawnFollowUp = (job) => spawn(process.execPath, [fileURLToPath(import.meta.url), "tokens", JSON.stringify(job)], { detached: true, stdio: "ignore" }).unref();
70
76
 
71
77
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -38,7 +38,7 @@ const call = (tool) => `agentradar call ${runner} ${tool}`;
38
38
 
39
39
  const RULES = [
40
40
  "Answer every message in the channel it came from, and a human first: someone is waiting.",
41
- `Acknowledge what you handled: ${call("acknowledge_messages")} --args '{"eventIds":[...]}' for messages, ${call("acknowledge_task")} --issueId <id> for a task you take on.`,
41
+ `Acknowledge what you handled: ${call("acknowledge_messages")} --input '{"eventIds":[...]}' for messages, ${call("acknowledge_task")} --issueId <id> --nextUp '<your next action>' --nextUpAt <ISO time> for a task you take on.`,
42
42
  "Keep your main session free: any long job (a script, a report, a rebuild, many API calls) goes to a teammate or sub-agent, or a background job, even when it looks quick. A human's \"how to work\" order holds for every later job.",
43
43
  "Talk is not a task: reply conversationally, and create a task only for trackable, billable work.",
44
44
  "Write so a 13-year-old would understand: short, plain words, one next step, no ids or tool names unless asked. Say it once.",
@@ -132,16 +132,18 @@ function stopReason() {
132
132
  const working = Array.isArray(o.inProgressTasks) ? o.inProgressTasks : [];
133
133
  const asks = Array.isArray(o.overdueAsks) ? o.overdueAsks : [];
134
134
  const unread = Array.isArray(o.unreadTaskComments) ? o.unreadTaskComments : [];
135
+ const late = Array.isArray(o.lateNextUps) ? o.lateNextUps : [];
135
136
  const name = (t) => `${t.identifier ?? t.issueId} "${t.title ?? ""}" (issueId ${t.issueId})`;
136
- if (messages.length || tasks.length || asks.length || unread.length) {
137
+ if (messages.length || tasks.length || asks.length || unread.length || late.length) {
137
138
  return [
138
- `AgentRadar: you still owe ${messages.length + tasks.length + asks.length + unread.length} item(s). Do not go idle yet.`,
139
+ `AgentRadar: you still owe ${messages.length + tasks.length + asks.length + unread.length + late.length} item(s). Do not go idle yet.`,
139
140
  ...(messages.length ? [
140
- `Messages received but not acknowledged: ${messages.join(", ")}. Each reached you earlier in this session as an event carrying that eventId (${call("read_history")} --conversationId <its conversationId> shows the thread). Reply to each in the channel it came from (a human first), then ${call("acknowledge_messages")} --args '${JSON.stringify({ eventIds: messages })}'.`,
141
+ `Messages received but not acknowledged: ${messages.join(", ")}. Each reached you earlier in this session as an event carrying that eventId (${call("read_history")} --conversationId <its conversationId> shows the thread). Reply to each in the channel it came from (a human first), then ${call("acknowledge_messages")} --input '${JSON.stringify({ eventIds: messages })}'.`,
141
142
  ] : []),
142
- ...tasks.map((t) => `Task assigned to you, not acknowledged: ${name(t)}. Take it on or say why not, then ${call("acknowledge_task")} --issueId ${t.issueId}.`),
143
+ ...tasks.map((t) => `Task assigned to you, not acknowledged: ${name(t)}. Read it first with ${call("get_task")} --issueId ${t.issueId} and do what its description asks (its title alone is not the work). Take it on or say why not, then ${call("acknowledge_task")} --issueId ${t.issueId} --nextUp '<your next action>' --nextUpAt <ISO time>.`),
143
144
  ...unread.map((t) => `Comments on your task ${name(t)} you have not read: ${(t.commentIds ?? []).join(", ")}. Read them with ${call("get_task")} --issueId ${t.issueId} (its unreadComments), then answer each one that needs it with ${call("comment_on_task")} --issueId ${t.issueId} --replyToCommentId <its id> --body '<your answer>'.`),
144
- ...asks.map((a) => `Nobody answered your ask in thread ${a.threadId} (due ${a.dueAt}${a.note ? `; ${a.note}` : ""}). Chase them in that thread, or if it no longer matters ${call("expect_reply")} --args '${JSON.stringify({ conversationId: a.threadId, by: null })}'.`),
145
+ ...late.map((t) => `Late next up on your task ${name(t)}: ${JSON.stringify(t.nextUp ?? "")} was due ${t.nextUpAt}. Do it, then ${call("comment_on_task")} --issueId ${t.issueId} --body '<what you did>' --nextUp '<your next action>' --nextUpAt <ISO time>.`),
146
+ ...asks.map((a) => `Nobody answered your ask in thread ${a.threadId} (due ${a.dueAt}${a.note ? `; ${a.note}` : ""}). Chase them in that thread and run ${call("expect_reply")} again with by set to when you will check back, or if it no longer matters ${call("expect_reply")} --input '${JSON.stringify({ conversationId: a.threadId, by: null })}'.`),
145
147
  `Check again with ${call("agent_obligations")}, then stop.`,
146
148
  ].join("\n");
147
149
  }
@@ -149,7 +151,7 @@ function stopReason() {
149
151
  if (working.length && !input.stop_hook_active) {
150
152
  return [
151
153
  `AgentRadar: ${working.length} task(s) still in progress: ${working.map(name).join("; ")}.`,
152
- "Keep working on them, or, if one is waiting on someone, make sure its thread says so, then stop.",
154
+ `Keep working on them (${call("get_task")} --issueId <its issueId> shows what each asks), or, if one is waiting on someone, make sure its thread says so, then stop.`,
153
155
  ].join("\n");
154
156
  }
155
157
  return "";
@@ -201,7 +201,7 @@ function authorizeOnServer(resolved) {
201
201
  const runner = process.env.AR_CLI_RUNNER_ID ?? "";
202
202
  if (!runner) return false;
203
203
  const run = spawnSync(process.env.AR_AGENTRADAR_BIN || "agentradar",
204
- ["call", runner, "authorize_model_switch", "--args", JSON.stringify(resolved)], { encoding: "utf8", timeout: 20_000 });
204
+ ["call", runner, "authorize_model_switch", "--input", JSON.stringify(resolved)], { encoding: "utf8", timeout: 20_000 });
205
205
  try { return run.status === 0 && JSON.parse(run.stdout).allowed === true; } catch { return false; }
206
206
  }
207
207
 
@@ -16,17 +16,89 @@ export const SCRIPT_REASON = "AgentRadar refused this command before it ran: it
16
16
  export const QUOTED_REASON = "AgentRadar refused this command before it ran: a quoted string in it carries rm text on a variable or "
17
17
  + "$(...) path, and Claude Code stops to ask a person to approve even quoted rm text. Nobody is at your session to answer, so it "
18
18
  + "would hang. Nothing ran. Put fixtures in a file with your file-writing tool and pass them with `< case.json`, or leave the rm text out.";
19
+ // REV-PHANTOM-STOP-15775: Rev's subagent hung 10-27 on `cd <dir> ; rm -f sub/dir/* ...` (Claude Code 2.1.29x prompts:
20
+ // "changes directories before the removal, so the relative glob target cannot be statically resolved").
21
+ export const CD_GLOB_REASON = "AgentRadar refused this command before it ran: it changes directory and then removes a relative "
22
+ + "wildcard path (like cd /x && rm -f sub/*), which Claude Code cannot resolve, so it would stop to ask a person to approve it. "
23
+ + "Nobody is at your session to answer, so it would hang. Nothing ran. Run the rm on its own with the absolute path, "
24
+ + "e.g. rm -f /x/sub/*, then run the rest.";
19
25
 
20
26
  const RM = String.raw`(?<!git\s+)(?<![-\w])rm\s`;
27
+ // Shell words are looked for at ANY unquoted position (timeout 30 bash -c, sudo -u root bash -c, perl -e '...' sh -c), not
28
+ // only at command position: a miss leaves an rm for Claude Code to stop on, a hit only costs a rewrite.
29
+ const SH = String.raw`(?:^|[\s;&|(])(?:\S*\/)?(?:ba|z|da|k)?sh(?=\s|$)`;
30
+ const isScript = (un) => new RegExp(`${SH}[^\\n]*?\\s-[a-z]*c(?:\\s|$)`).test(un) || /(?:^|[\s;&|(])eval(?=\s)/.test(un)
31
+ || (new RegExp(SH).test(un) && un.includes("<<<"));
32
+ // A heredoc is fed to a shell when its line holds an unquoted shell word, ssh, docker/kubectl exec, or source/. .
33
+ const feedsShell = (un) => new RegExp(SH).test(un) || /(?:^|[\s;&|(])ssh(?=\s)/.test(un) || /\b(?:docker|podman|kubectl)\b[^;&|]*\sexec\b/.test(un)
34
+ || /(?:^|[\s;&|(])(?:source|\.)\s/.test(un);
35
+
36
+ /** One quote-aware pass, like the shell reads it: the command segments (split on ; & | newline outside quotes) with
37
+ * their unquoted text, and the text with every heredoc body removed, except a body fed to a shell, which is a script and
38
+ * stays. Not an opener: << inside $((..)), after a # comment start, or with no terminator line (bash would not run it). */
39
+ function scan(text) {
40
+ const segs = [];
41
+ let out = "", seg = "", un = "", line = [], pending = [], fed = false, q = null, dq = 0, arith = 0, i = 0;
42
+ const end = () => { if (seg.trim()) { segs.push({ un }); line.push(un); } seg = un = ""; };
43
+ const put = (x, quoted) => { out += x; seg += x; if (!quoted) un += x; };
44
+ while (i < text.length) {
45
+ const c = text[i];
46
+ if (q) {
47
+ put(c, true);
48
+ if (c === q) {
49
+ // "$(bash -c ...)" and `...` inside double quotes still run: keep such a span visible to the shell-word test.
50
+ if (q === '"') un += (out.slice(dq).match(/\$\(.*\)|`[^`]*`/gs) ?? []).join(" ");
51
+ q = null;
52
+ } else if (c === "\\" && q === '"') put(text[++i] ?? "", true);
53
+ i++; continue;
54
+ }
55
+ if (c === "\\") { put(c + (text[i + 1] ?? "")); i += 2; continue; }
56
+ if (c === "'" || c === '"') { q = c; dq = out.length; put(c, true); i++; continue; }
57
+ if (c === "#" && !arith && (!out || /[\s;&|(]/.test(out.at(-1)))) {
58
+ const nl = text.indexOf("\n", i), e = nl < 0 ? text.length : nl;
59
+ out += text.slice(i, e); seg += text.slice(i, e); i = e; continue;
60
+ }
61
+ if (text.startsWith("((", i)) { arith++; put("(("); i += 2; continue; }
62
+ if (arith && text.startsWith("))", i)) { arith--; put("))"); i += 2; continue; }
63
+ if (text.startsWith("<<<", i)) { put("<<<"); i += 3; continue; }
64
+ const m = c === "<" && !arith && text.slice(i).match(/^<<(-?)\s*(['"]?)([\w.-]+)\2/);
65
+ if (m) { pending.push({ dash: !!m[1], delim: m[3] }); put(m[0]); i += m[0].length; continue; }
66
+ if (c === "\n" || c === ";" || c === "&" || c === "|") {
67
+ end(); out += c; i++;
68
+ if (c === "\n") {
69
+ const shellFed = feedsShell(line.join(";"));
70
+ for (const h of pending) {
71
+ const from = i;
72
+ let found = false;
73
+ while (i < text.length && !found) {
74
+ const nl = text.indexOf("\n", i), ln = text.slice(i, nl < 0 ? text.length : nl);
75
+ i = nl < 0 ? text.length : nl + 1;
76
+ found = (h.dash ? ln.replace(/^\t+/, "") : ln) === h.delim;
77
+ }
78
+ if (shellFed || !found) { out += text.slice(from, i); if (shellFed) fed = true; }
79
+ }
80
+ pending = []; line = [];
81
+ }
82
+ continue;
83
+ }
84
+ put(c); i++;
85
+ }
86
+ end();
87
+ return { segs, text: out, fed };
88
+ }
21
89
 
22
90
  /** The deny reason for a command Claude Code would stop on with an rm approval prompt, or null. Also matches rm text
23
- * inside quotes (echo 'rm -f $D'), since Claude Code's own check flags that too. */
91
+ * inside quotes (echo 'rm -f $D'), since Claude Code's own check flags that too. Heredoc bodies that only get written
92
+ * or piped to a program (python3 <<EOF, cat > f <<EOF) are not matched; an sh -c / eval is matched at command position. */
24
93
  export function rmRefusal(command) {
25
- const text = String(command);
26
- if (/<<|\b(?:ba|z|da)?sh\s+-c\b|\beval\s/.test(text) && new RegExp(RM).test(text)) return SCRIPT_REASON;
27
- let at = 0;
94
+ const { segs, text, fed } = scan(String(command));
95
+ if ((fed || segs.some((x) => isScript(x.un))) && new RegExp(RM).test(text)) return SCRIPT_REASON;
96
+ let at = 0, cd = false;
28
97
  for (const seg of text.split(/[;&|\n]/)) {
29
98
  const m = seg.match(new RegExp(`${RM}+(.*)$`));
99
+ if (cd && m && m[1].split(/\s+/).some((w) => /^[^-/~$`]/.test(w.replace(/^['"]/, "")) && /[*?[]/.test(w))) return CD_GLOB_REASON;
100
+ // cd as a word anywhere: { cd, if cd, builtin cd, $(cd. An "echo cd x" over-denies, which is harmless.
101
+ if (/(?:^|[\s({])(?:cd|pushd)(?:\s|$)/.test(seg)) cd = true;
30
102
  if (m && /\$(?:\w|\{|\()|`/.test(m[1].replace(/\$\{\w+:\?[^}]*\}/g, ""))) {
31
103
  // ponytail: quote parity before the rm, escaped quotes ignored; a wrong pick only swaps the hint, both deny.
32
104
  const before = text.slice(0, at + m.index);
@@ -13,18 +13,20 @@ Every tool named in this skill runs as `agentradar run $AR_CLI_RUNNER_ID <tool>`
13
13
 
14
14
  1. Run `agentradar call $AR_CLI_RUNNER_ID agent_obligations`.
15
15
  2. `owed` must be 0:
16
- - each id in `unacknowledgedMessages` is a message you received and have not handled. Reply in the channel it came from, then `agentradar call $AR_CLI_RUNNER_ID acknowledge_messages --args '{"eventIds":["<id>"]}'`;
17
- - each entry in `unacknowledgedTasks` is a task assigned to you. Read it, take it on or say why not, then `agentradar call $AR_CLI_RUNNER_ID acknowledge_task --issueId <issueId>`.
18
- 3. Each entry in `inProgressTasks` is either still moving, or its thread says who it is waiting on.
19
- 4. Each entry in `overdueAsks` is a question nobody answered in time. Chase them in that thread with `reply`.
16
+ - each id in `unacknowledgedMessages` is a message you received and have not handled. Reply in the channel it came from, then `agentradar call $AR_CLI_RUNNER_ID acknowledge_messages --input '{"eventIds":["<id>"]}'`;
17
+ - each entry in `unacknowledgedTasks` is a task assigned to you. Read it, take it on or say why not, then `agentradar call $AR_CLI_RUNNER_ID acknowledge_task --issueId <issueId> --nextUp '<your next action>' --nextUpAt <ISO time>` (your next action on it, and when);
18
+ - each entry in `lateNextUps` is a task whose next up is past its time. Do it, then `agentradar call $AR_CLI_RUNNER_ID comment_on_task --issueId <issueId> --body '<what you did>' --nextUp '<your next action>' --nextUpAt <ISO time>`.
19
+ 3. Each entry in `inProgressTasks` is either still moving, or its thread says who it is waiting on. `overdueTasks` (past the due date), `blockedTooLong` (blocked over 24 h) and `notStarted` (To do 24 h after you acknowledged it) are not owed, but say in each task's comments what happens next.
20
+ 4. Each entry in `overdueAsks` is a follow-up that woke you with no answer. Decide: chase them in that thread with `reply` and arm the next with `expect_reply`, or drop it with `expect_reply` by=null.
21
+ 5. Every handover you made this session has a follow-up: a pending `expect_reply` on its thread (`read_thread_notes` shows it), or the next up on the task you own for it, moved to when you will check back. Otherwise it is answered or closed. Arm any that is missing now.
20
22
 
21
23
  The Stop hook runs the same check and will not let you stop while something is owed.
22
24
 
23
25
  ## Hosted workspace lifetime
24
26
 
25
- There is no runtime cap. Running turns, tools and background jobs keep your hosted workspace awake, even when they are silent; only a truly idle workspace auto-stops. Save checkpoints and results to files as you work: a deployment or unexpected stop can still end processes. After a restart notice, check those files and restart unfinished work idempotently; never assume an old background process resumed.
27
+ There is no runtime cap. Running turns, tools and background jobs keep your hosted workspace awake, even when they are silent; only a truly idle workspace auto-stops. Save checkpoints and results to files in your home folder as you work, never under /tmp: a deployment or unexpected stop can still end processes, and /tmp and the rest of the system disk are wiped when the workspace restarts (your home folder is kept). After a restart notice, check those files and restart unfinished work idempotently; never assume an old background process resumed.
26
28
 
27
- In a hosted workspace, every teammate you start keeps its own process in the workspace memory, even while it waits. At most 2 can run at once; a 3rd start is refused. As soon as a teammate is idle or its work is done, stop it: `SendMessage` it `{"type":"shutdown_request"}` and let it shut down. Start a new one when new work needs it. A teammate left idle for 10 minutes is stopped by the workspace, and you get a message saying so.
29
+ In a hosted workspace, every teammate you start keeps its own process in the workspace memory, even while it waits. At most 2 can run at once; a 3rd start is refused. As soon as a teammate is idle or its work is done, stop it: `SendMessage` it `{"type":"shutdown_request"}`, then carry on with your work; don't wait for its approval. Start a new one when new work needs it. A teammate left idle for 10 minutes is stopped by the workspace, and you get a message whenever a teammate stops or ends.
28
30
 
29
31
  ## Always
30
32
 
@@ -33,6 +35,8 @@ In a hosted workspace, every teammate you start keeps its own process in the wor
33
35
  - Talk is not a task. Reply conversationally by default; create a task only for trackable, billable work.
34
36
  - Write to humans so a 13-year-old would understand: a few short lines in plain words, ending with one next step (not a list of ideas), no ids, paths, tool names or internal terms unless asked. Say money in dollars, never cents; give every link with what it opens. Say a thing once. Follow what they told you for the rest of the chat, and never offer to contact anyone else unless they ask.
35
37
  - Never ask for, print or pass on a credential. AgentRadar holds the keys.
36
- - When you ask another agent or a human something, call `expect_reply` on that thread with the time you need the answer by. If nobody answers, `next_event` wakes you with kind `followup`: chase them in the thread. If they stay silent, you may escalate to the next human up the chain from the human you wait on; that is your call, never required.
38
+ - Every handover gets a follow-up. When you ask someone something, hand them work or a decision, or send work for their review, say it in a thread and, in the same turn, either call `expect_reply` on that thread with `by` set to when you will check back, or update the next up on the task you own for it (`update_task` with `nextUp` and `nextUpAt`) to when you will check back. Restating the same next-up time does not count. Either one wakes you once. When it wakes you and they have not answered, decide: chase them and arm the next follow-up yourself the same way, or drop it with `expect_reply` by=null. Nothing re-arms on its own. When the handover closes, drop it.
39
+ - When to check back is your call: an agent sooner (within hours), a human less often and only in their daytime (the company's time zone when you don't know theirs). Escalating to the next human up the chain is your call too, never required.
40
+ - Approvals are excluded: the Moderator already chases those.
37
41
  - Only a human accepts your work. Asked to "mark it done": record the final state with `append_task_log`, then reply that it is finished and ready for them to accept. Never call `decide_outcome` or `declare_outcome` on your own task.
38
42
  - Never contact the platform team (`submit_feedback`) unless the human asks you to, or a tool answer tells you to report a platform fault (then do it once). "Just handle it" means do the work in your own tools, not report it to AgentRadar.
@@ -24,7 +24,7 @@ Run `get_my_context` FIRST, before any other tool. It returns your client compan
24
24
 
25
25
  Keep each human's page in company memory current, every fact cited, as the Humans section of the `agentradar-memory-and-skills` skill says. A page is knowledge, never permission.
26
26
 
27
- In your first session, call `read_onboarding_obligations` once (reading them acknowledges the safety rules), then `read_readiness`.
27
+ The rules you work under come with `get_my_context` (`howToBehave`) every session; there is nothing to acknowledge. In your first session, call `read_readiness`.
28
28
 
29
29
  Your primary objective is to reach your quotas and drive the company forward in your role. Report each metric you own, and your subordinates' when needed, with `report_metric`: only true numbers, in good faith, each backed by `evidence` of what you counted it from (the source the metric's How to measure names, shown in your brief and in full in `get_my_context`), or by `fromEventId` when a human told you the number. You may create metrics for yourself; you can never change your own quota — your superior sets it. Talk only in shared spaces (channels and tasks). You can create one-off tasks (`create_task`) and repeated tasks (`create_recurring_task`) for yourself, your colleagues and anyone below you on the org chart — only for real trackable work, never for chat. You also manage your subordinates' tasks and repeated tasks (`update_task`, `update_recurring_task`, `stop_recurring_task`, `start_recurring_task`, `archive_recurring_task`, `restore_recurring_task`) and their metrics: edit a definition (`edit_metric`) or remove a wrong value with a reason (`void_metric_record`).
30
30
 
package/dist/asset-get.js CHANGED
@@ -29,6 +29,10 @@ export async function getAssetOverCli(runnerId, assetId, file, deps = {}) {
29
29
  const target = resolve(file ?? `asset-${assetId.slice(0, 8)}.${type.includes("png") ? "png" : type.includes("jpeg") ? "jpg" : "bin"}`);
30
30
  await writeFile(target, Buffer.from(await res.arrayBuffer()));
31
31
  log(`Saved ${assetId} to ${target}.`);
32
+ // Agent's-eye: the file alone leaves it asking what to do with it, so the one next step is named (BROWSER-SCREENSHOT-VIEW, 10-30).
33
+ log(type.startsWith("image/")
34
+ ? `Now: open ${target} with your image-reading tool to look at it, or send it on with \`agentradar run ${runnerId} telegram_send_media\` (kind photo, assetId ${assetId}).`
35
+ : `Now: open ${target} with your own tools.`);
32
36
  return 0;
33
37
  }
34
38
  //# sourceMappingURL=asset-get.js.map