@saadnvd1/agent-os 0.3.694 → 0.3.697

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.
@@ -9,7 +9,7 @@ import { useApprovalsModeQuery, useSetApprovalsMode } from "@/data/approvals";
9
9
  export const NO_APPROVALS_WARNING = [
10
10
  "Every ask is approved the moment it is raised, hard lines included: public posts and messages, money, irreversible or destructive changes, credentials.",
11
11
  "A PR stuck failing its gates, with no CI, or too big to review still merges.",
12
- "Brakes stop holding new starts, and plan-only tasks go on to implement without waiting for a plan approval.",
12
+ "Brakes stop holding new starts. Plan-only tasks still wait for approve_plan.",
13
13
  "Every agent AgentOS starts runs with its permission prompts off, and prompts already waiting are allowed.",
14
14
  ];
15
15
 
@@ -2,9 +2,7 @@
2
2
  // brings what already exists in line with it.
3
3
 
4
4
  import { db } from "../db";
5
- import { isRemoteHost } from "../hosts";
6
5
  import { reapplyChatModes } from "../chat/settings";
7
- import { approvePlanNow } from "../models/plan-step";
8
6
  import { autoApproveAsk, type AskRow } from "../orchestrator/asks";
9
7
  import { addNote } from "../orchestrator/notes";
10
8
  import { listOrchestrators } from "../orchestrator/home";
@@ -15,7 +13,7 @@ import {
15
13
  } from "./no-approvals";
16
14
 
17
15
  // Everything already waiting when the mode goes on is approved: open asks,
18
- // plan-only tasks at their plan, tool cards in live chats.
16
+ // tool cards in live chats.
19
17
  async function sweep(): Promise<void> {
20
18
  const asks = db
21
19
  .prepare(`SELECT * FROM orchestrator_asks WHERE status = 'open'`)
@@ -27,24 +25,6 @@ async function sweep(): Promise<void> {
27
25
  console.error(`auto-approving ask ${ask.id} failed:`, e);
28
26
  }
29
27
  }
30
- const plans = db
31
- .prepare(
32
- `SELECT id, host_id, plan_waits_for_caller FROM session_rows WHERE plan_phase = 'awaiting' AND task_status = 'running'`
33
- )
34
- .all() as {
35
- id: string;
36
- host_id: string;
37
- plan_waits_for_caller: number | null;
38
- }[];
39
- for (const p of plans) {
40
- // A plan placed here by another machine is that machine's to decide.
41
- if (p.plan_waits_for_caller && !isRemoteHost(p.host_id)) continue;
42
- try {
43
- await approvePlanNow(p.id, undefined, "no-approvals");
44
- } catch (e) {
45
- console.error(`auto-approving plan of ${p.id} failed:`, e);
46
- }
47
- }
48
28
  }
49
29
 
50
30
  export async function switchNoApprovals(on: boolean): Promise<void> {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * No approvals mode: while it's on, nothing AgentOS starts or runs waits on
3
3
  * a person. Asks are approved as they're raised, brakes let starts through,
4
- * plan-only tasks go on to implement, and agents launch with their harness's
4
+ * and agents launch with their harness's
5
5
  * permission prompts off. Off by default; app-wide; every point that honours
6
6
  * it reads it here and records what it did with recordAutoApproval.
7
7
  */
@@ -19,7 +19,7 @@ import {
19
19
  } from "./resolve";
20
20
  import { ANTHROPIC } from "./roles";
21
21
  import { queueEvent } from "../orchestrator/events";
22
- import { noApprovalsOn, recordAutoApproval } from "../approvals/no-approvals";
22
+ import { recordAutoApproval } from "../approvals/no-approvals";
23
23
  import { addNote } from "../orchestrator/notes";
24
24
  import { amendBrief, scopeChangesOf } from "../orchestrator/amendments";
25
25
  import { PIECES_PROMPT, foldedBrief, pieceBrief } from "../plans/pieces";
@@ -451,15 +451,15 @@ export async function handoffAfterPlan(
451
451
  deleteQueued(sessionId, planRetryMessageId(sessionId));
452
452
  const retried = waiting.get(sessionId)?.retried ?? false;
453
453
  forget(sessionId);
454
- // No approvals mode doesn't stop at the plan to wait for one. Pieces that
455
- // couldn't be registered always do: building on a guess is the bug.
456
- const auto = !!s.plan_only && noApprovalsOn() && !s.plan_waits_for_caller;
454
+ // A plan-only task never goes on by itself, in No approvals mode too: it
455
+ // was asked to stop and show the plan. Pieces that couldn't be registered
456
+ // always hold a plan-first one: building on a guess is the bug.
457
457
  const planText = readPlan(s);
458
458
  const pieceErrors =
459
459
  (planText && !s.plan_waits_for_caller
460
460
  ? splitOf(s, planText)?.errors
461
461
  : null) ?? [];
462
- if ((!s.plan_only || auto) && !pieceErrors.length) {
462
+ if (!s.plan_only && !pieceErrors.length) {
463
463
  const ref = await startImplementing(
464
464
  s,
465
465
  "planning",
@@ -467,14 +467,6 @@ export async function handoffAfterPlan(
467
467
  undefined,
468
468
  retried
469
469
  );
470
- if (ref && auto)
471
- recordAutoApproval({
472
- source: "plan",
473
- workspaceId: sessionWorkspace(s),
474
- sessionId,
475
- subject: `plan for ${s.name}`,
476
- detail: "implementing without waiting for approve_plan",
477
- });
478
470
  return ref !== null;
479
471
  }
480
472
  if (!advancePlan(db, sessionId, "planning", "awaiting")) return false;
@@ -16,10 +16,6 @@ import { isRemoteHost } from "../../hosts";
16
16
  import { peerSource } from "../../hosts/peer-load";
17
17
  import { hostLink, requireHostLink } from "../../hosts/remote-api";
18
18
  import { LOCAL, machineCapabilities } from "../../load/machines";
19
- import {
20
- noApprovalsOn,
21
- recordAutoApproval,
22
- } from "../../approvals/no-approvals";
23
19
  import { implementArrived, planNoteFor } from "../../models/plan-step";
24
20
  import { sessionWorkspace } from "../../models/resolve";
25
21
  import { queueEvent } from "../../orchestrator/events";
@@ -112,8 +108,7 @@ export async function planWritten(
112
108
  // As for a task planned here: a plan-first task goes on by itself, a
113
109
  // plan-only one waits unless no-approvals is on, and pieces that couldn't
114
110
  // be registered always hold it.
115
- const auto = !!fresh.plan_only && noApprovalsOn();
116
- const goOn = (!fresh.plan_only || auto) && !errors.length;
111
+ const goOn = !fresh.plan_only && !errors.length;
117
112
  const target = goOn ? implementTarget(fresh, split) : null;
118
113
  const next =
119
114
  target === "there"
@@ -148,14 +143,6 @@ export async function planWritten(
148
143
  return;
149
144
  }
150
145
  await continuePlanThere(fresh);
151
- if (auto)
152
- recordAutoApproval({
153
- source: "plan",
154
- workspaceId,
155
- sessionId: s.id,
156
- subject: `plan for ${s.name}`,
157
- detail: "implementing without waiting for approve_plan",
158
- });
159
146
  }
160
147
 
161
148
  /**
@@ -37,6 +37,7 @@ export const TEST_TEMP_PREFIXES: readonly string[] = [
37
37
  "aos-kind-",
38
38
  "aos-launch-plain-",
39
39
  "aos-launch-repo-",
40
+ "aos-machine-",
40
41
  "aos-limits-",
41
42
  "aos-mcp-",
42
43
  "aos-merge-policy-",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.694",
3
+ "version": "0.3.697",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
@@ -13,7 +13,12 @@ source scripts/lib/pnpm.sh
13
13
  if [ -s "$HOME/.nvm/nvm.sh" ]; then
14
14
  # shellcheck disable=SC1091
15
15
  source "$HOME/.nvm/nvm.sh" >/dev/null
16
- nvm use >/dev/null
16
+ # A box with nvm but without .nvmrc's version (a CI runner) keeps the node on PATH.
17
+ nvm use >/dev/null 2>&1 || true
18
+ fi
19
+ if [ "$(node -p 'process.versions.node.split(".")[0]')" -lt 22 ]; then
20
+ echo "Node 22 or newer is required (found $(node -v)); run: nvm install" >&2
21
+ exit 1
17
22
  fi
18
23
  use_pnpm
19
24
 
@@ -189,7 +189,7 @@ cmd_stop() {
189
189
  while ps -p "$pid" &> /dev/null && [[ $count -lt 10 ]]; do
190
190
  printf "."
191
191
  sleep 1
192
- ((count++))
192
+ count=$((count + 1))
193
193
  done
194
194
  echo ""
195
195
 
@@ -33,6 +33,12 @@ const TOOLS = [
33
33
  "id",
34
34
  ];
35
35
 
36
+ // Bash 4+ (Linux, CI) fails `set -e` on `((n++))` at 0; macOS's 3.2 doesn't.
37
+ const BASH =
38
+ ["/opt/homebrew/bin/bash", "/usr/local/bin/bash", "/bin/bash"].find(
39
+ existsSync
40
+ ) ?? "/bin/bash";
41
+
36
42
  let dir: string;
37
43
 
38
44
  function fake(name: string, body: string) {
@@ -48,7 +54,7 @@ function run(
48
54
  ) {
49
55
  writeFileSync(join(dir, "osrelease"), osrelease);
50
56
  return spawnSync(
51
- "/bin/bash",
57
+ BASH,
52
58
  [
53
59
  "-euo",
54
60
  "pipefail",
@@ -313,3 +319,47 @@ describe("installing prerequisites", () => {
313
319
  expect(r.stdout).not.toContain("Prerequisites installed");
314
320
  });
315
321
  });
322
+
323
+ describe("cmd_stop", () => {
324
+ // `ps` reports the server alive for the first `alive` checks, then gone.
325
+ // A server that takes a moment to exit used to trip `((count++))` (status 1
326
+ // at 0) under `set -e`, so stop exited 1 after the server had stopped.
327
+ const stop = (alive: number) => {
328
+ mkdirSync(join(dir, "home"), { recursive: true });
329
+ fake(
330
+ "ps",
331
+ `n=$(cat "${dir}/ps-count" 2>/dev/null || echo 0); echo $((n + 1)) > "${dir}/ps-count"; [ "$n" -lt ${alive} ]`
332
+ );
333
+ fake("sleep", "exit 0");
334
+ return run(
335
+ `AGENT_OS_HOME="${dir}/home"; PID_FILE="$AGENT_OS_HOME/agent-os.pid"; echo $$ > "$PID_FILE"; kill() { return 0; }; cmd_stop`,
336
+ ""
337
+ );
338
+ };
339
+
340
+ it("returns 0 when the server exits after a few checks", () => {
341
+ const r = stop(3);
342
+ expect(r.status).toBe(0);
343
+ expect(r.stdout).toContain("AgentOS stopped");
344
+ expect(existsSync(join(dir, "home", "agent-os.pid"))).toBe(false);
345
+ });
346
+
347
+ it("force kills and still returns 0 when it outlives the timeout", () => {
348
+ const r = stop(1000);
349
+ expect(r.status).toBe(0);
350
+ expect(r.stdout).toContain("Force killing");
351
+ });
352
+
353
+ it("has no bare ((x++)) in scripts/lib: it returns 1 at 0 under set -e", () => {
354
+ for (const f of [
355
+ "commands.sh",
356
+ "common.sh",
357
+ "machine.sh",
358
+ "prerequisites.sh",
359
+ ]) {
360
+ expect(readFileSync(join(LIB, f), "utf8"), f).not.toMatch(
361
+ /^\s*\(\(\s*\w+(\+\+|--)\s*\)\)\s*$/m
362
+ );
363
+ }
364
+ });
365
+ });