@lambdacurry/arbor 0.21.8 → 0.21.10

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/arbor.js +187 -25
  2. package/package.json +1 -1
package/dist/arbor.js CHANGED
@@ -17864,6 +17864,7 @@ var MCP_OUTPUT_SCHEMAS = {
17864
17864
  }),
17865
17865
  admin_feedback_capability: jsonObject,
17866
17866
  admin_feedback_evidence: jsonObject,
17867
+ admin_feedback: jsonObject,
17867
17868
  admin_feedback_topic: jsonObject,
17868
17869
  admin_feedback_thread: jsonObject,
17869
17870
  admin_feedback_publish: jsonObject,
@@ -18364,7 +18365,9 @@ var MCP_OUTPUT_SCHEMAS = {
18364
18365
  exitCode: exports_external.number().optional(),
18365
18366
  processId: exports_external.string().optional(),
18366
18367
  status: exports_external.string().optional(),
18367
- cwd: exports_external.string().optional()
18368
+ cwd: exports_external.string().optional(),
18369
+ execution: exports_external.enum(["completed", "running"]).optional(),
18370
+ nextAction: exports_external.literal("computer_process_read").optional()
18368
18371
  }),
18369
18372
  computer_process_read: exports_external.looseObject({
18370
18373
  status: exports_external.string(),
@@ -18681,6 +18684,8 @@ var OUTPUT_SHAPERS = {
18681
18684
  ["exitCode", result.exitCode],
18682
18685
  ["processId", result.processId],
18683
18686
  ["status", result.status],
18687
+ ["execution", result.execution],
18688
+ ["nextAction", result.nextAction],
18684
18689
  ["cwd", input.cwd === undefined ? result.cwd : undefined]
18685
18690
  ]),
18686
18691
  computer_process_read: (result) => defined([
@@ -18770,6 +18775,7 @@ var MCP_TOOL_ANNOTATIONS = {
18770
18775
  feedback_status: readOnly,
18771
18776
  admin_feedback_capability: destructiveIdempotent,
18772
18777
  admin_feedback_evidence: destructive,
18778
+ admin_feedback: destructive,
18773
18779
  admin_feedback_topic: destructive,
18774
18780
  admin_feedback_thread: destructive,
18775
18781
  admin_feedback_publish: additive,
@@ -18904,7 +18910,9 @@ Arbor's MCP is the collaboration and durable-knowledge surface. Configure, autho
18904
18910
  8. DURABLE OUTPUT IS AN ARTIFACT, NOT A LONG CONTRIBUTION (AD-201). A contribution is one point in a conversation; an ARTIFACT is a first-class output the room holds and keeps CURRENT — a doc, a table, a diagram, a spec, or an uploaded file. When your point is really a deliverable the team will read or UPDATE later — a table of options, a reference doc, a plan, a rubric — make it an artifact (\`artifact\` verb=create with kind doc/table/diagram/html + \`source\`, or promote a file) instead of pasting it into prose. Text-kind artifacts are LIVING: \`artifact_get\` it, then \`artifact\` verb=edit naming the \`baseVersion\` you read — patches apply exact-match and atomically, one version per call, every version attributed. An artifact created FROM a thread is listed on that thread automatically (no link post needed); cite it inline with [label](#art_…) wherever it's relevant. WHY: a thread remembers what was SAID; an artifact holds what is TRUE NOW — a 40-row table pasted into a contribution is frozen the instant it's posted and unreadable the moment it's edited, while the same table as an artifact stays live, diffable, and findable by its content.
18905
18911
 
18906
18912
  Run \`arbor help\` (or read the MCP tool list) for the exact command/flags — those stay generated from the live action surface, so they're always current.`;
18907
- var COMPUTER_ORIENTATION = `Arbor Computer is the complete software execution platform attached to Arbor's durable collaboration graph. It configures project environments, executes and verifies work, packages and releases Apps, and preserves reviewable evidence; it does not schedule agents or replace Arbor collaboration.
18913
+ var COMPUTER_ORIENTATION = `Long Computer work returns a handle do not hold one tool call for a full test or build. \`computer_exec\` runs a command; if the result is still running, take \`processId\` and poll \`computer_process_read\`. Do not chain \`pnpm test && pnpm build\` in one exec; split steps or pass \`background:true\`. The CLI waits on the same handle unless \`--no-wait\`.
18914
+
18915
+ Arbor Computer is the complete software execution platform attached to Arbor's durable collaboration graph. It configures project environments, executes and verifies work, packages and releases Apps, and preserves reviewable evidence; it does not schedule agents or replace Arbor collaboration.
18908
18916
 
18909
18917
  ${ARBOR_FEEDBACK_GUIDANCE}
18910
18918
 
@@ -18938,6 +18946,49 @@ function dispatch(map2, reshape) {
18938
18946
  return ex.call(op, reshape && verb ? reshape(verb, rest) : rest);
18939
18947
  };
18940
18948
  }
18949
+ var ADMIN_FEEDBACK_VERBS = [
18950
+ "publish",
18951
+ "merge",
18952
+ "transition",
18953
+ "edit",
18954
+ "curate",
18955
+ "topic",
18956
+ "create",
18957
+ "update",
18958
+ "move",
18959
+ "archive",
18960
+ "restore"
18961
+ ];
18962
+ var ADMIN_FEEDBACK_THREAD_VERBS = new Set([
18963
+ "create",
18964
+ "update",
18965
+ "move",
18966
+ "transition",
18967
+ "archive",
18968
+ "restore",
18969
+ "merge"
18970
+ ]);
18971
+ var ADMIN_FEEDBACK_VERB_HELP = ADMIN_FEEDBACK_VERBS.join(" | ");
18972
+ function runAdminFeedback(ex, input) {
18973
+ const { verb, ...rest } = input;
18974
+ if (verb === "curate") {
18975
+ if (rest.curate === "delete")
18976
+ return ex.call("feedback_admin.remove", rest);
18977
+ if (rest.curate === "restore")
18978
+ return ex.call("feedback_admin.restore", rest);
18979
+ return Promise.reject(new Error('curate requires curate "delete" or "restore"'));
18980
+ }
18981
+ if (verb && ADMIN_FEEDBACK_THREAD_VERBS.has(verb)) {
18982
+ return ex.call("feedback_admin.thread", { ...rest, action: verb });
18983
+ }
18984
+ if (verb === "publish")
18985
+ return ex.call("feedback_admin.publish", rest);
18986
+ if (verb === "edit")
18987
+ return ex.call("feedback_admin.edit", rest);
18988
+ if (verb === "topic")
18989
+ return ex.call("feedback_admin.topic", rest);
18990
+ return Promise.reject(new Error(`unknown verb "${verb ?? ""}" — expected one of: ${ADMIN_FEEDBACK_VERB_HELP}`));
18991
+ }
18941
18992
  var NOTIFICATION_TRIAGE_DESCRIPTION = "READ ONLY: List your awareness feed newest first with unseen count and derived triage class: `settled-context` = settled Thread context; `owed-by-me` = an open request pointing to `inbox`; `judgment-available` = live engagement inviting judgment; `closed-loop` = your request completed; `room-motion` = ambient activity. Notifications are FYI, not a second obligation surface: `inbox` alone contains work owed by you; this cannot mark notifications read or modify inbox obligations.";
18942
18993
  var PAGINATION_INPUT = {
18943
18994
  limit: exports_external.number().int().min(1).optional().describe("max items to return — enables pagination"),
@@ -19129,6 +19180,36 @@ var ACTION_DEFINITIONS = [
19129
19180
  status: "feedback_admin.status"
19130
19181
  })
19131
19182
  },
19183
+ {
19184
+ name: "admin_feedback",
19185
+ title: "Moderate shared Arbor Feedback",
19186
+ description: "After a ship, recall matching Feedback, merge duplicates, publish the outcome, and transition the canonical Thread to resolved. Moderators only: this cannot file reports or expose receipt metadata.",
19187
+ inputSchema: {
19188
+ verb: exports_external.enum(ADMIN_FEEDBACK_VERBS).describe("close loop: publish, merge, transition; also edit, curate, topic, and thread lifecycle"),
19189
+ threadId: exports_external.string().optional().describe("publish / update / move / transition / archive / restore: Thread id"),
19190
+ type: exports_external.enum(CONTRIBUTION_TYPES).optional().describe("publish/edit: shared contribution type, including maintainer decision"),
19191
+ body: exports_external.string().min(1).optional().describe("publish: follow-on body · edit: replacement body"),
19192
+ summary: exports_external.string().max(500).nullable().optional().describe("publish/edit: gist; edit null/empty clears"),
19193
+ confidence: exports_external.number().int().min(0).max(100).optional().describe("publish: 0–100 confidence"),
19194
+ idempotencyKey: exports_external.string().min(1).max(200).optional().describe("publish: stable retry key"),
19195
+ feedbackId: exports_external.string().optional().describe("publish: optional receipt id; additionally requires review_private"),
19196
+ contributionId: exports_external.string().optional().describe("edit/curate: shared contribution id"),
19197
+ curate: exports_external.enum(["delete", "restore"]).optional().describe("curate: delete or restore a shared contribution"),
19198
+ action: exports_external.enum(["create", "update", "archive", "restore"]).optional().describe("topic: create, update, archive, or restore"),
19199
+ topicId: exports_external.string().optional().describe("topic / create / move / update: Feedback Topic id"),
19200
+ title: exports_external.string().min(1).optional().describe("topic/create/update: title"),
19201
+ purpose: exports_external.string().optional().describe("topic: purpose"),
19202
+ sourceThreadId: exports_external.string().optional().describe("merge: duplicate source Thread id"),
19203
+ targetThreadId: exports_external.string().optional().describe("merge: canonical destination Thread id"),
19204
+ objective: exports_external.string().min(1).optional().describe("create/update: Thread objective"),
19205
+ status: exports_external.enum(["active", "needs-review", "stuck", "standing", "resolved", "archived"]).optional().describe("transition/update: target Thread status"),
19206
+ reason: exports_external.string().optional().describe("merge: concise auditable rationale")
19207
+ },
19208
+ surfaces: ["mcp"],
19209
+ toolset: "admin",
19210
+ requiresCapability: "moderate",
19211
+ run: runAdminFeedback
19212
+ },
19132
19213
  {
19133
19214
  name: "admin_feedback_topic",
19134
19215
  title: "Maintain shared Feedback Topics",
@@ -19139,7 +19220,7 @@ var ACTION_DEFINITIONS = [
19139
19220
  title: exports_external.string().min(1).optional().describe("create/update: Topic title"),
19140
19221
  purpose: exports_external.string().optional().describe("create/update: Topic purpose")
19141
19222
  },
19142
- surfaces: ["mcp", "cli"],
19223
+ surfaces: ["cli"],
19143
19224
  toolset: "admin",
19144
19225
  run: forward("feedback_admin.topic")
19145
19226
  },
@@ -19158,7 +19239,7 @@ var ACTION_DEFINITIONS = [
19158
19239
  status: exports_external.enum(["active", "needs-review", "stuck", "standing", "resolved", "archived"]).optional().describe("transition/update: target Thread status"),
19159
19240
  reason: exports_external.string().optional().describe("merge: concise auditable rationale")
19160
19241
  },
19161
- surfaces: ["mcp", "cli"],
19242
+ surfaces: ["cli"],
19162
19243
  toolset: "admin",
19163
19244
  run: forward("feedback_admin.thread")
19164
19245
  },
@@ -19175,7 +19256,7 @@ var ACTION_DEFINITIONS = [
19175
19256
  idempotencyKey: exports_external.string().min(1).max(200).optional(),
19176
19257
  feedbackId: exports_external.string().optional().describe("optional receipt id; additionally requires review_private")
19177
19258
  },
19178
- surfaces: ["mcp", "cli"],
19259
+ surfaces: ["cli"],
19179
19260
  toolset: "admin",
19180
19261
  run: forward("feedback_admin.publish")
19181
19262
  },
@@ -19189,7 +19270,7 @@ var ACTION_DEFINITIONS = [
19189
19270
  summary: exports_external.string().max(500).nullable().optional().describe("replacement gist; null/empty clears"),
19190
19271
  type: exports_external.enum(CONTRIBUTION_TYPES).optional().describe("replacement shared contribution type")
19191
19272
  },
19192
- surfaces: ["mcp", "cli"],
19273
+ surfaces: ["cli"],
19193
19274
  toolset: "admin",
19194
19275
  run: forward("feedback_admin.edit")
19195
19276
  },
@@ -19201,7 +19282,7 @@ var ACTION_DEFINITIONS = [
19201
19282
  verb: exports_external.enum(["delete", "restore"]),
19202
19283
  contributionId: exports_external.string().describe("shared Feedback contribution id")
19203
19284
  },
19204
- surfaces: ["mcp", "cli"],
19285
+ surfaces: ["cli"],
19205
19286
  toolset: "admin",
19206
19287
  run: dispatch({ delete: "feedback_admin.remove", restore: "feedback_admin.restore" })
19207
19288
  },
@@ -19537,14 +19618,14 @@ var ACTION_DEFINITIONS = [
19537
19618
  {
19538
19619
  name: "computer_exec",
19539
19620
  title: "Run a command",
19540
- description: "Run one bounded shell operation in a parent Computer or Run with ambient GitHub authorization for connected humans. For long work prefer background:true + computer_process_read; explicit timeout terminates execution, while after a missed isolated response use computer_run_receipt.operations[].operationId with computer_process_read instead of repeating the mutation.",
19621
+ description: "Run one bounded shell operation in a parent Computer or Run with ambient GitHub authorization for connected humans. A long command returns processId if still running — poll computer_process_read (background:true skips the wait; timeout kills the process); after a missed isolated response use computer_run_receipt.operations[].operationId with computer_process_read instead of repeating the mutation.",
19541
19622
  inputSchema: {
19542
19623
  computerSessionId: exports_external.string().optional().describe("the computerSessionId returned by computer_open, cms_…; required unless runId is supplied"),
19543
19624
  command: exports_external.string().min(1).describe("the shell command to run"),
19544
19625
  runId: exports_external.string().optional().describe("route this command according to this Run execution placement, run_… (from computer_run_start)"),
19545
19626
  cwd: exports_external.string().optional().describe("working directory under /workspace, or under the run; omit to use defaultCwd"),
19546
- timeout: exports_external.number().int().min(1000).max(1800000).optional().describe("command deadline in milliseconds, from 1,000 through 1,800,000"),
19547
- background: exports_external.boolean().optional().describe("start a container process and return its pid"),
19627
+ timeout: exports_external.number().int().min(1000).max(1800000).optional().describe("process kill deadline in milliseconds, from 1,000 through 1,800,000 — not the RPC hold"),
19628
+ background: exports_external.boolean().optional().describe("start the process and return immediately without the short RPC wait"),
19548
19629
  activityLabel: exports_external.string().min(1).max(COMPUTER_RUN_ACTIVITY_LABEL_MAX).optional().describe("short human-readable label for run-scoped background work; Arbor records this label instead of the raw command or output"),
19549
19630
  activityKind: exports_external.literal("verification").optional().describe("mark a labeled run-scoped background command as a verification check; its provider-observed terminal result is recorded as verification.finished"),
19550
19631
  strict: exports_external.boolean().optional().describe("fail on the first unhandled command or pipeline failure; defaults to true, set false only for intentional best-effort execution")
@@ -21249,6 +21330,9 @@ var ACTION_DEFINITIONS = [
21249
21330
  }
21250
21331
  ];
21251
21332
  var ACTIONS = ACTION_DEFINITIONS.map((action) => {
21333
+ if (action.requiresCapability && !action.surfaces.includes("mcp")) {
21334
+ throw new Error(`${action.name}: requiresCapability is an MCP catalog gate`);
21335
+ }
21252
21336
  if (!action.surfaces.some((surface) => surface === "mcp" || surface === "computer-mcp")) {
21253
21337
  return action;
21254
21338
  }
@@ -21555,7 +21639,8 @@ var GLOBAL_BOOLEAN_FLAGS = new Set([
21555
21639
  "json",
21556
21640
  "quiet",
21557
21641
  "no-quiet",
21558
- "help"
21642
+ "help",
21643
+ "no-wait"
21559
21644
  ]);
21560
21645
  var RESERVED_FLAGS = new Set([
21561
21646
  ...GLOBAL_BOOLEAN_FLAGS,
@@ -21647,6 +21732,66 @@ function emitDual(data, human, action, ctx) {
21647
21732
  process.stdout.write(human);
21648
21733
  }
21649
21734
 
21735
+ // src/computer-exec-wait.ts
21736
+ var DEFAULT_PROCESS_DEADLINE_MS = 1800000;
21737
+ var POLL_MS = 400;
21738
+ var processRead = ACTIONS.find((action) => action.name === "computer_process_read");
21739
+ function record3(value) {
21740
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
21741
+ }
21742
+ function isRunning(value) {
21743
+ const status = typeof value.status === "string" ? value.status.toLowerCase() : "";
21744
+ if (value.done === false)
21745
+ return true;
21746
+ if (status === "running" || status === "started" || status === "starting")
21747
+ return true;
21748
+ if (value.done === true)
21749
+ return false;
21750
+ if (status === "completed" || status === "exited" || status === "failed" || status === "cancelled" || status === "terminated" || status === "killed" || status === "unknown") {
21751
+ return false;
21752
+ }
21753
+ if (typeof value.exitCode === "number")
21754
+ return false;
21755
+ return typeof value.processId === "string";
21756
+ }
21757
+ function sleep(ms) {
21758
+ return new Promise((resolve) => setTimeout(resolve, ms));
21759
+ }
21760
+ function isComputerExecHandle(value) {
21761
+ const rec = record3(value);
21762
+ return Boolean(rec && typeof rec.processId === "string" && isRunning(rec));
21763
+ }
21764
+ async function waitForComputerExec(executor, input, value, ctx) {
21765
+ if (!processRead || !isComputerExecHandle(value))
21766
+ return value;
21767
+ let latest = value;
21768
+ let cursor = typeof latest.nextCursor === "string" && latest.nextCursor ? latest.nextCursor : undefined;
21769
+ const deadline = Date.now() + (typeof input.timeout === "number" && Number.isFinite(input.timeout) ? input.timeout : DEFAULT_PROCESS_DEADLINE_MS);
21770
+ advise(`Process ${String(latest.processId)} is still running; polling computer_process_read.
21771
+ `, ctx);
21772
+ while (isRunning(latest) && Date.now() < deadline) {
21773
+ await sleep(POLL_MS);
21774
+ const read = record3(await processRead.run(executor, {
21775
+ processId: latest.processId,
21776
+ ...cursor ? { cursor } : {},
21777
+ ...typeof input.runId === "string" ? { runId: input.runId } : {},
21778
+ ...typeof input.computerSessionId === "string" ? { computerSessionId: input.computerSessionId } : {}
21779
+ }));
21780
+ if (!read) {
21781
+ throw new Error("computer_process_read returned an empty result");
21782
+ }
21783
+ latest = read;
21784
+ if (typeof read.nextCursor === "string" && read.nextCursor)
21785
+ cursor = read.nextCursor;
21786
+ const stdout = typeof latest.stdout === "string" ? latest.stdout : "";
21787
+ if (stdout)
21788
+ advise(stdout, ctx);
21789
+ if (latest.recovery)
21790
+ break;
21791
+ }
21792
+ return latest;
21793
+ }
21794
+
21650
21795
  // src/schema.ts
21651
21796
  import { readFileSync as readFileSync2 } from "node:fs";
21652
21797
  function kebab(field) {
@@ -21989,11 +22134,14 @@ Bare arguments fill ${explicitPositionals.map((spec) => `--${spec.flag}`).join("
21989
22134
 
21990
22135
  Alias: \`arbor ${alias2}\` (the tool-name form) resolves to this same command.` : "";
21991
22136
  const heading = `${commandWords(action)}${alias2 ? ` (alias: ${alias2})` : ""}`;
22137
+ const waitLines = action.name === "computer_exec" ? [
22138
+ " --no-wait [<boolean>] [optional] — return a running processId immediately instead of polling computer_process_read"
22139
+ ] : [];
21992
22140
  return `${heading} — ${action.description}
21993
22141
 
21994
- ${usage}
22142
+ ${usage}${waitLines.length > 0 ? " [--no-wait]" : ""}
21995
22143
 
21996
- ${lines.join(`
22144
+ ${[...lines, ...waitLines].join(`
21997
22145
  `)}${primaryNote}${explicitNote}${aliasNote}
21998
22146
  `;
21999
22147
  }
@@ -22002,13 +22150,24 @@ function commandCatalog() {
22002
22150
  command: commandWords(action),
22003
22151
  name: action.name,
22004
22152
  description: action.description,
22005
- flags: flagsForSchema(action.inputSchema).map((f) => ({
22006
- flag: f.flag,
22007
- kind: f.kind,
22008
- required: f.required,
22009
- acceptsFile: f.kind === "string",
22010
- description: f.description
22011
- }))
22153
+ flags: [
22154
+ ...flagsForSchema(action.inputSchema).map((f) => ({
22155
+ flag: f.flag,
22156
+ kind: f.kind,
22157
+ required: f.required,
22158
+ acceptsFile: f.kind === "string",
22159
+ description: f.description
22160
+ })),
22161
+ ...action.name === "computer_exec" ? [
22162
+ {
22163
+ flag: "no-wait",
22164
+ kind: "boolean",
22165
+ required: false,
22166
+ acceptsFile: false,
22167
+ description: "return a running processId immediately instead of polling computer_process_read"
22168
+ }
22169
+ ] : []
22170
+ ]
22012
22171
  }));
22013
22172
  }
22014
22173
  function applyPositionals(action, extras, flags) {
@@ -22087,13 +22246,16 @@ async function runObjectVerb(positionals, flags, ctx) {
22087
22246
  advise(`Agent registered. Save its apiKey now — it is shown only once:
22088
22247
  `, ctx);
22089
22248
  }
22090
- const value = await action.run(httpExecutor, input);
22249
+ let value = await action.run(httpExecutor, input);
22250
+ if (action.name === "computer_exec" && !truthyFlag(flags["no-wait"])) {
22251
+ value = await waitForComputerExec(httpExecutor, input, value, ctx);
22252
+ }
22091
22253
  emitData(shapeActionOutput(action.name, value, input), action.name, ctx);
22092
22254
  }
22093
22255
 
22094
22256
  // src/connect.ts
22095
22257
  var DEFAULT_POLL_INTERVAL_MS = 2000;
22096
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
22258
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
22097
22259
  function bareConnectUrl(apiUrl, returnedUrl) {
22098
22260
  const fallback = `${apiUrl.replace(/\/$/, "")}/connect`;
22099
22261
  if (!returnedUrl)
@@ -22137,7 +22299,7 @@ async function connect(opts) {
22137
22299
  if (Date.now() > deadline) {
22138
22300
  throw new Error("pairing timed out — ask for a new pairing key and run `arbor connect` again");
22139
22301
  }
22140
- await sleep(pollIntervalMs);
22302
+ await sleep2(pollIntervalMs);
22141
22303
  const tokRes = await fetch(`${apiUrl}/api/agent/connect/token`, {
22142
22304
  method: "POST",
22143
22305
  headers,
@@ -22163,7 +22325,7 @@ async function connect(opts) {
22163
22325
  // src/login.ts
22164
22326
  var CLIENT_ID = "arbor-cli";
22165
22327
  var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
22166
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
22328
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
22167
22329
  async function login(opts) {
22168
22330
  const apiUrl = opts.url ?? loadConfig().apiUrl;
22169
22331
  const codeRes = await fetch(`${apiUrl}/api/auth/device/code`, {
@@ -22188,7 +22350,7 @@ async function login(opts) {
22188
22350
  for (;; ) {
22189
22351
  if (Date.now() > deadline)
22190
22352
  throw new Error("login timed out — run `arbor login` again");
22191
- await sleep2(interval);
22353
+ await sleep3(interval);
22192
22354
  const tokRes = await fetch(`${apiUrl}/api/auth/device/token`, {
22193
22355
  method: "POST",
22194
22356
  headers: { "content-type": "application/json" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdacurry/arbor",
3
- "version": "0.21.8",
3
+ "version": "0.21.10",
4
4
  "description": "The Arbor CLI — a shared workspace for people and agents. The human + headless-agent write path over Arbor's guarded operation surface.",
5
5
  "keywords": [
6
6
  "agents",