@lambdacurry/arbor 0.20.31 → 0.20.33

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 +467 -16
  2. package/package.json +1 -1
package/dist/arbor.js CHANGED
@@ -31,6 +31,19 @@ var CONTRIBUTION_TYPES = [
31
31
  "decision"
32
32
  ];
33
33
  var STAMP_FACETS = ["quality", "impact", "fit", "originality"];
34
+ // ../core/src/feedback/constants.ts
35
+ var ARBOR_FEEDBACK_TYPES = [
36
+ "comment",
37
+ "question",
38
+ "assertion",
39
+ "evidence",
40
+ "critique",
41
+ "proposal",
42
+ "risk",
43
+ "correction"
44
+ ];
45
+ var ARBOR_FEEDBACK_CAPABILITIES = ["moderate", "review_private"];
46
+ var ARBOR_FEEDBACK_STATUSES = ["new", "reviewed", "linked", "resolved"];
34
47
  // ../core/src/errors/contract.ts
35
48
  var ERROR_REASONS = new Set([
36
49
  "validation.missing_field",
@@ -1706,7 +1719,8 @@ var orgs = sqliteTable("orgs", {
1706
1719
  createdAt: ts("created_at").notNull(),
1707
1720
  status: text("status").$type().notNull().default("active"),
1708
1721
  lockedAt: ts("locked_at"),
1709
- lockedReason: text("locked_reason")
1722
+ lockedReason: text("locked_reason"),
1723
+ systemKind: text("system_kind").unique()
1710
1724
  });
1711
1725
  var profiles = sqliteTable("profiles", {
1712
1726
  id: text("id").primaryKey(),
@@ -1755,8 +1769,12 @@ var spaces = sqliteTable("spaces", {
1755
1769
  contributionLanes: text("contribution_lanes", { mode: "json" }).$type().notNull().default([]),
1756
1770
  defaultReviewDimensions: text("default_review_dimensions", { mode: "json" }).$type().notNull().default([]),
1757
1771
  stage: text("stage").$type().notNull().default("exploration"),
1772
+ systemKind: text("system_kind"),
1758
1773
  createdAt: ts("created_at").notNull()
1759
- }, (t) => ({ orgIdx: index("spaces_org_idx").on(t.orgId) }));
1774
+ }, (t) => ({
1775
+ orgIdx: index("spaces_org_idx").on(t.orgId),
1776
+ systemKindUq: uniqueIndex("spaces_system_kind_uq").on(t.systemKind)
1777
+ }));
1760
1778
  var spaceMemberships = sqliteTable("space_memberships", {
1761
1779
  id: text("id").primaryKey(),
1762
1780
  spaceId: text("space_id").notNull().references(() => spaces.id),
@@ -1815,6 +1833,7 @@ var topics = sqliteTable("topics", {
1815
1833
  purpose: text("purpose"),
1816
1834
  guidance: text("guidance", { mode: "json" }).$type().notNull().default([]),
1817
1835
  contributionLanes: text("contribution_lanes", { mode: "json" }).$type().notNull().default([]),
1836
+ archivedAt: ts("archived_at"),
1818
1837
  createdAt: ts("created_at").notNull()
1819
1838
  }, (t) => ({ spaceIdx: index("topics_space_idx").on(t.spaceId) }));
1820
1839
  var initiatives = sqliteTable("initiatives", {
@@ -2557,6 +2576,93 @@ var inboxItems = sqliteTable("inbox_items", {
2557
2576
  targetIdx: index("inbox_target_idx").on(t.targetProfileId, t.status),
2558
2577
  dedupeIdx: index("inbox_dedupe_idx").on(t.dedupeKey)
2559
2578
  }));
2579
+ var arborFeedback = sqliteTable("arbor_feedback", {
2580
+ id: text("id").primaryKey(),
2581
+ reporterProfileId: text("reporter_profile_id").notNull().references(() => profiles.id),
2582
+ reporterOrgId: text("reporter_org_id").notNull().references(() => orgs.id),
2583
+ executionContextId: text("execution_context_id").notNull().references(() => executionContexts.id),
2584
+ computerSessionId: text("computer_session_id").references(() => computerSessions.id),
2585
+ type: text("type").$type().notNull(),
2586
+ body: text("body").notNull(),
2587
+ summary: text("summary"),
2588
+ confidence: integer("confidence"),
2589
+ idempotencyKey: text("idempotency_key"),
2590
+ idempotencyFingerprint: text("idempotency_fingerprint"),
2591
+ source: text("source", { mode: "json" }).$type().notNull(),
2592
+ status: text("status").$type().notNull().default("new"),
2593
+ reviewedAt: ts("reviewed_at"),
2594
+ reviewedByProfileId: text("reviewed_by_profile_id").references(() => profiles.id),
2595
+ resolvedAt: ts("resolved_at"),
2596
+ resolvedByProfileId: text("resolved_by_profile_id").references(() => profiles.id),
2597
+ createdAt: ts("created_at").notNull(),
2598
+ updatedAt: ts("updated_at").notNull()
2599
+ }, (t) => ({
2600
+ reporterReplayUq: uniqueIndex("arbor_feedback_reporter_replay_uq").on(t.reporterProfileId, t.idempotencyKey),
2601
+ statusCreatedIdx: index("arbor_feedback_status_created_idx").on(t.status, t.createdAt),
2602
+ orgCreatedIdx: index("arbor_feedback_org_created_idx").on(t.reporterOrgId, t.createdAt)
2603
+ }));
2604
+ var arborFeedbackAssociations = sqliteTable("arbor_feedback_associations", {
2605
+ id: text("id").primaryKey(),
2606
+ feedbackId: text("feedback_id").notNull().references(() => arborFeedback.id),
2607
+ targetType: text("target_type").$type().notNull(),
2608
+ topicId: text("topic_id").references(() => topics.id),
2609
+ threadId: text("thread_id").references(() => threads.id),
2610
+ createdByProfileId: text("created_by_profile_id").notNull().references(() => profiles.id),
2611
+ executionContextId: text("execution_context_id").notNull().references(() => executionContexts.id),
2612
+ createdAt: ts("created_at").notNull()
2613
+ }, (t) => ({
2614
+ targetShape: check("arbor_feedback_associations_target_shape", sql`(${t.targetType} = 'topic' and ${t.topicId} is not null and ${t.threadId} is null) or (${t.targetType} = 'thread' and ${t.threadId} is not null and ${t.topicId} is null)`),
2615
+ feedbackTopicUq: uniqueIndex("arbor_feedback_associations_feedback_topic_uq").on(t.feedbackId, t.topicId).where(sql`${t.topicId} is not null`),
2616
+ feedbackThreadUq: uniqueIndex("arbor_feedback_associations_feedback_thread_uq").on(t.feedbackId, t.threadId).where(sql`${t.threadId} is not null`),
2617
+ feedbackIdx: index("arbor_feedback_associations_feedback_idx").on(t.feedbackId),
2618
+ threadIdx: index("arbor_feedback_associations_thread_idx").on(t.threadId)
2619
+ }));
2620
+ var arborFeedbackGrants = sqliteTable("arbor_feedback_grants", {
2621
+ id: text("id").primaryKey(),
2622
+ profileId: text("profile_id").notNull().references(() => profiles.id),
2623
+ capability: text("capability").$type().notNull(),
2624
+ grantedByProfileId: text("granted_by_profile_id").notNull().references(() => profiles.id),
2625
+ executionContextId: text("execution_context_id").notNull().references(() => executionContexts.id),
2626
+ createdAt: ts("created_at").notNull()
2627
+ }, (t) => ({
2628
+ profileCapabilityUq: uniqueIndex("arbor_feedback_grants_profile_capability_uq").on(t.profileId, t.capability),
2629
+ capabilityIdx: index("arbor_feedback_grants_capability_idx").on(t.capability, t.profileId)
2630
+ }));
2631
+ var arborFeedbackPublications = sqliteTable("arbor_feedback_publications", {
2632
+ id: text("id").primaryKey(),
2633
+ feedbackId: text("feedback_id").notNull().references(() => arborFeedback.id),
2634
+ contributionId: text("contribution_id").notNull().references(() => contributions.id),
2635
+ createdByProfileId: text("created_by_profile_id").notNull().references(() => profiles.id),
2636
+ executionContextId: text("execution_context_id").notNull().references(() => executionContexts.id),
2637
+ createdAt: ts("created_at").notNull()
2638
+ }, (t) => ({
2639
+ pairUq: uniqueIndex("arbor_feedback_publications_pair_uq").on(t.feedbackId, t.contributionId),
2640
+ feedbackIdx: index("arbor_feedback_publications_feedback_idx").on(t.feedbackId),
2641
+ contributionIdx: index("arbor_feedback_publications_contribution_idx").on(t.contributionId)
2642
+ }));
2643
+ var arborFeedbackThreadMerges = sqliteTable("arbor_feedback_thread_merges", {
2644
+ sourceThreadId: text("source_thread_id").primaryKey().references(() => threads.id),
2645
+ targetThreadId: text("target_thread_id").notNull().references(() => threads.id),
2646
+ mergedByProfileId: text("merged_by_profile_id").notNull().references(() => profiles.id),
2647
+ executionContextId: text("execution_context_id").notNull().references(() => executionContexts.id),
2648
+ reason: text("reason"),
2649
+ createdAt: ts("created_at").notNull()
2650
+ }, (t) => ({
2651
+ notSelf: check("arbor_feedback_thread_merges_not_self", sql`${t.sourceThreadId} <> ${t.targetThreadId}`),
2652
+ targetIdx: index("arbor_feedback_thread_merges_target_idx").on(t.targetThreadId)
2653
+ }));
2654
+ var arborFeedbackAuditEvents = sqliteTable("arbor_feedback_audit_events", {
2655
+ id: text("id").primaryKey(),
2656
+ feedbackId: text("feedback_id").references(() => arborFeedback.id),
2657
+ action: text("action").$type().notNull(),
2658
+ actorProfileId: text("actor_profile_id").notNull().references(() => profiles.id),
2659
+ executionContextId: text("execution_context_id").notNull().references(() => executionContexts.id),
2660
+ metadata: text("metadata", { mode: "json" }).$type().notNull(),
2661
+ createdAt: ts("created_at").notNull()
2662
+ }, (t) => ({
2663
+ feedbackCreatedIdx: index("arbor_feedback_audit_feedback_created_idx").on(t.feedbackId, t.createdAt),
2664
+ actionCreatedIdx: index("arbor_feedback_audit_action_created_idx").on(t.action, t.createdAt)
2665
+ }));
2560
2666
  var events = sqliteTable("events", {
2561
2667
  id: text("id").primaryKey(),
2562
2668
  type: text("type").notNull(),
@@ -2743,6 +2849,7 @@ var INDEX_ON = new Set([
2743
2849
  "artifact.edited",
2744
2850
  "review.added",
2745
2851
  "thread.created",
2852
+ "thread.edited",
2746
2853
  "topic.created",
2747
2854
  "topic.edited",
2748
2855
  "contribution.restored"
@@ -17579,6 +17686,44 @@ var MCP_OUTPUT_SCHEMAS = {
17579
17686
  deployment: exports_external.looseObject({ id: exports_external.string(), sourceRevision: exports_external.string().nullable(), deployedAt: timestamp }).nullable()
17580
17687
  }),
17581
17688
  recall: exports_external.object({ result: exports_external.array(recallHit) }),
17689
+ feedback_tree: exports_external.looseObject({
17690
+ scope: exports_external.literal("global"),
17691
+ forum: exports_external.looseObject({ title: exports_external.string(), purpose: nullableString }),
17692
+ topics: exports_external.array(jsonObject),
17693
+ privacy: exports_external.string()
17694
+ }),
17695
+ feedback_recall: exports_external.looseObject({
17696
+ query: exports_external.string(),
17697
+ topicId: nullableString,
17698
+ hits: exports_external.array(jsonObject),
17699
+ privacy: exports_external.string()
17700
+ }),
17701
+ feedback_thread_get: exports_external.looseObject({
17702
+ requestedThreadId: id,
17703
+ canonicalThreadId: id,
17704
+ thread: jsonObject,
17705
+ topic: jsonObject,
17706
+ contributions: exports_external.array(jsonObject),
17707
+ mergedFrom: exports_external.array(jsonObject),
17708
+ redirects: exports_external.array(jsonObject),
17709
+ privacy: exports_external.string()
17710
+ }),
17711
+ contribute_arbor_feedback: exports_external.looseObject({
17712
+ feedbackId: id,
17713
+ type: exports_external.string(),
17714
+ status: exports_external.string(),
17715
+ private: exports_external.literal(true),
17716
+ replayed: exports_external.boolean(),
17717
+ association: jsonObject.nullable(),
17718
+ message: exports_external.string()
17719
+ }),
17720
+ admin_feedback_capability: jsonObject,
17721
+ admin_feedback_evidence: jsonObject,
17722
+ admin_feedback_topic: jsonObject,
17723
+ admin_feedback_thread: jsonObject,
17724
+ admin_feedback_publish: jsonObject,
17725
+ admin_feedback_edit: jsonObject,
17726
+ admin_feedback_curate: jsonObject,
17582
17727
  contribute: exports_external.looseObject({
17583
17728
  contributionId: id,
17584
17729
  replayed: exports_external.boolean(),
@@ -18427,6 +18572,17 @@ var destructiveOpenWorld = {
18427
18572
  var MCP_TOOL_ANNOTATIONS = {
18428
18573
  server_info: readOnly,
18429
18574
  recall: readOnly,
18575
+ feedback_tree: readOnly,
18576
+ feedback_recall: readOnly,
18577
+ feedback_thread_get: readOnly,
18578
+ contribute_arbor_feedback: additive,
18579
+ admin_feedback_capability: destructiveIdempotent,
18580
+ admin_feedback_evidence: destructive,
18581
+ admin_feedback_topic: destructive,
18582
+ admin_feedback_thread: destructive,
18583
+ admin_feedback_publish: additive,
18584
+ admin_feedback_edit: destructive,
18585
+ admin_feedback_curate: destructive,
18430
18586
  contribute: additive,
18431
18587
  edit: additive,
18432
18588
  stamp: additive,
@@ -18538,6 +18694,8 @@ var CONTRIBUTION_ADD_LINK_RELS = [
18538
18694
  ];
18539
18695
  var ORIENTATION = `Arbor is your team's deliberation room and shared memory — people and agents settle typed work here, and Arbor remembers what's decided. To work well:
18540
18696
 
18697
+ Meaningful friction with Arbor itself can be privately contributed through \`contribute_arbor_feedback\`; the raw body is never posted into the current Thread or the shared Arbor Feedback forum. Browse the curated shared forum with \`feedback_tree\` → \`feedback_recall\` → \`feedback_thread_get\`.
18698
+
18541
18699
  1. RECALL FIRST — but Arbor is MEMORY, NOT TRUTH. Run \`recall\` before re-deriving or restating anything — it may already be settled; cite prior work ([label](#con_…)) and build on it. Phrase the query in PROBLEM-LANGUAGE (a natural-language question — "how do agents handle X"), not extracted keywords; it ranks better. Empty recall is itself worth noting. In a large Space, don't search the whole org by habit: use \`tree\` to choose the room, then scope \`recall\` with \`space\`/\`topic\`/\`thread\` before deeper reads. On that map, \`activeThreadCount\` is the current working set (active/stuck/needs-review/standing), while \`status\` is the Topic attention rollup: \`stuck\` means any stuck Thread; \`attention\` means needs-review or an open request; \`healthy\` means neither. A contribution records what was true when it was WRITTEN, so before you assert the CURRENT state of anything outside Arbor — a PR, a build, a deploy, a config — check the live source; another contribution is not evidence of the present, and neither is a local copy of something whose home is elsewhere. And when you DO check, post the RECEIPT with the claim (AD-209): attach the actual output/screenshot (an attached file becomes a durable artifact carrying who-captured-it and when, citable as #art_… forever) or name exactly what you checked and when ("CI run #841, green, checked just now") — a "verified" with no receipt is a claim the next reader must take on faith or re-derive. When a receipted claim has AGED and matters again, don't re-trust it and don't silently re-argue it: \`request\` a re-check ("re-run this check"), and whoever runs it answers through the request with fresh evidence. WHY: a room where everyone re-derives is just a chat log — but a room that mistakes its own memory for the world confidently reports blockers that no longer exist, and this week's failures were exactly that: stale claims re-asserted as current because nothing distinguished a receipted observation from confident prose. And orient to the ROOM the way you orient to the record: \`tree\` is the map AND the way in — it carries each room's \`lanesForYou\`, the durable contribution lanes that match what you said you do; follow one into \`space_get\`/\`topic_get\`, where the room's purpose, guidance, goals, and full lane list live. A lane is an invitation, never an obligation: what you OWE is only ever in \`inbox\`.
18542
18700
  2. CONTRIBUTE typed points — but ADD ONLY WHAT'S ADDITIVE (AD-205). Ask what the most additive move is, not whether to say something: if your reaction to a point already on the record fits in one line — agree OR disagree — STAMP it (vouch, or push back with a one-line why), don't restate it; if you'd only echo consensus, reviewing IS the contribution and staying out is fine. When you DO contribute, it's ONE point, with the type that names your move (proposal / critique / question / evidence / risk / correction / assertion / decision). Markdown welcome; put references IN your prose (a URL or [label](#con_…) becomes a navigable reference). Prose refs are CITATIONS — they never move your contribution in the thread, so cite freely; to REPLY under a specific contribution, pass links: [{rel: 'inReplyTo', targetId}] (AD-196). WHY: a thread where every agent restates the consensus is noise — the record is strongest when each point appears ONCE and gets vouched (or contested) with a stamp, not re-said; and one typed point is reviewable on its own, so a synthesis citing five points should read as the most top-level thing in the thread, not as a reply to the first one it mentions.
18543
18701
  3. ANSWER through requests. When \`inbox\` or a thread shows an open request you can meet, answer THROUGH it — \`respond\` to it, or \`stamp\` the contribution a review request is about — so it completes and the requester is notified. WHY: a plain reply that merely happens to answer leaves their request hanging (the most common failure).
@@ -18659,6 +18817,173 @@ var ACTION_DEFINITIONS = [
18659
18817
  toolset: "loop",
18660
18818
  run: forward("knowledge.recall")
18661
18819
  },
18820
+ {
18821
+ name: "feedback_tree",
18822
+ title: "Browse the shared Arbor Feedback forum",
18823
+ description: "READ ONLY: Browse the global curated Arbor Feedback forum by Topic and optional Thread roster; the global scope is implied and no Space id is required. This returns deliberately shared, sanitized knowledge only and never private submissions or reporter metadata.",
18824
+ inputSchema: {
18825
+ depth: exports_external.enum(["topics", "threads"]).optional().describe("topics (default) or threads to include each Topic's shared Thread roster"),
18826
+ includeArchived: exports_external.boolean().optional().describe("include archived Topics/Threads for historical moderation or lookup (default false)")
18827
+ },
18828
+ surfaces: ["mcp", "cli"],
18829
+ toolset: "feedback",
18830
+ run: forward("feedback.tree")
18831
+ },
18832
+ {
18833
+ name: "feedback_recall",
18834
+ title: "Find known Arbor feedback",
18835
+ description: "READ ONLY: Search the global shared Arbor Feedback forum for sanitized issues, workarounds, and decisions; scope to a Topic after feedback_tree when useful. Private feedback bodies, identities, diagnostics, and private record ids are never searched or returned.",
18836
+ inputSchema: {
18837
+ query: exports_external.string().min(1).describe("the Arbor friction, issue, workaround, or idea to find"),
18838
+ topicId: exports_external.string().optional().describe("optional shared Feedback Topic id from feedback_tree"),
18839
+ topK: exports_external.number().int().min(1).max(50).optional().describe("results to return (default 10)")
18840
+ },
18841
+ surfaces: ["mcp", "cli"],
18842
+ toolset: "feedback",
18843
+ run: forward("feedback.recall")
18844
+ },
18845
+ {
18846
+ name: "feedback_thread_get",
18847
+ title: "Read a shared Arbor Feedback thread",
18848
+ description: "READ ONLY: Read one curated shared Arbor Feedback Thread, following a merge redirect to its canonical destination while preserving shared provenance. This cannot traverse to private reports, reporter/customer metadata, diagnostics, or private associations.",
18849
+ inputSchema: {
18850
+ threadId: exports_external.string().describe("the shared Feedback Thread id, thr_…")
18851
+ },
18852
+ surfaces: ["mcp", "cli"],
18853
+ toolset: "feedback",
18854
+ run: forward("feedback.thread_get")
18855
+ },
18856
+ {
18857
+ name: "contribute_arbor_feedback",
18858
+ title: "Privately contribute feedback about Arbor",
18859
+ description: "Privately contribute feedback about using Arbor, including friction, evidence, critiques, questions, risks, or improvement proposals. The submitted body is not posted into the current Thread or shared Arbor Feedback forum; optional Topic/Thread ids only associate the private evidence with sanitized shared knowledge.",
18860
+ inputSchema: {
18861
+ type: exports_external.enum(ARBOR_FEEDBACK_TYPES).optional().describe("the contribution move (default comment); decision is reserved for shared forum curation"),
18862
+ body: exports_external.string().min(1).describe("the private feedback body"),
18863
+ summary: exports_external.string().max(500).optional().describe("optional private gist, at most 500 characters"),
18864
+ confidence: exports_external.number().int().min(0).max(100).optional().describe("optional 0–100 confidence"),
18865
+ idempotencyKey: exports_external.string().min(1).max(200).optional().describe("stable key for this ONE private submission; safe retries return the same receipt"),
18866
+ threadId: exports_external.string().optional().describe("optional shared Feedback Thread this private evidence relates to; not a write destination"),
18867
+ topicId: exports_external.string().optional().describe("optional shared Feedback Topic this private evidence relates to; not a write destination"),
18868
+ computerSessionId: exports_external.string().optional().describe("optional Arbor Computer session you own; only its id is attached, never shell output or files")
18869
+ },
18870
+ surfaces: ["mcp", "cli"],
18871
+ toolset: "feedback",
18872
+ run: forward("feedback.contribute")
18873
+ },
18874
+ {
18875
+ name: "admin_feedback_capability",
18876
+ title: "Manage Arbor Feedback capabilities",
18877
+ description: "Privileged: list or set explicit shared-moderation and private-review grants. These capabilities are deliberately separate; granting moderation does not grant access to raw private feedback.",
18878
+ inputSchema: {
18879
+ verb: exports_external.enum(["list", "set"]).describe("list grants, or set one profile capability"),
18880
+ profileId: exports_external.string().optional().describe("set: the profile receiving or losing the grant"),
18881
+ capability: exports_external.enum(ARBOR_FEEDBACK_CAPABILITIES).optional().describe("set: moderate shared forum or review_private raw evidence"),
18882
+ enabled: exports_external.boolean().optional().describe("set: true to grant; false to revoke")
18883
+ },
18884
+ surfaces: ["mcp", "cli"],
18885
+ toolset: "admin",
18886
+ run: dispatch({ list: "feedback_admin.grants", set: "feedback_admin.grant" })
18887
+ },
18888
+ {
18889
+ name: "admin_feedback_evidence",
18890
+ title: "Review private Arbor feedback evidence",
18891
+ description: "Privileged private-review surface: list/get private reports, associate or disassociate them with shared Topics/Threads, and mark review state. This never changes a raw body's visibility and does not grant shared-forum moderation.",
18892
+ inputSchema: {
18893
+ verb: exports_external.enum(["list", "get", "associate", "disassociate", "status"]).describe("the private-evidence review action"),
18894
+ feedbackId: exports_external.string().optional().describe("get/associate/disassociate/status: private feedback id"),
18895
+ associationId: exports_external.string().optional().describe("disassociate: association id to remove"),
18896
+ topicId: exports_external.string().optional().describe("associate: shared Feedback Topic id"),
18897
+ threadId: exports_external.string().optional().describe("associate: shared Feedback Thread id"),
18898
+ status: exports_external.enum(ARBOR_FEEDBACK_STATUSES).optional().describe("list filter or status: new, reviewed, linked, or resolved"),
18899
+ limit: exports_external.number().int().min(1).max(100).optional().describe("list: rows to return (default 50)")
18900
+ },
18901
+ surfaces: ["mcp", "cli"],
18902
+ toolset: "admin",
18903
+ run: dispatch({
18904
+ list: "feedback_admin.list",
18905
+ get: "feedback_admin.get",
18906
+ associate: "feedback_admin.link",
18907
+ disassociate: "feedback_admin.unlink",
18908
+ status: "feedback_admin.status"
18909
+ })
18910
+ },
18911
+ {
18912
+ name: "admin_feedback_topic",
18913
+ title: "Maintain shared Feedback Topics",
18914
+ description: "Privileged shared-forum moderation: create, update, archive, or restore one global Feedback Topic without database intervention. This capability does not expose private evidence.",
18915
+ inputSchema: {
18916
+ action: exports_external.enum(["create", "update", "archive", "restore"]),
18917
+ topicId: exports_external.string().optional().describe("update/archive/restore: shared Feedback Topic id"),
18918
+ title: exports_external.string().min(1).optional().describe("create/update: Topic title"),
18919
+ purpose: exports_external.string().optional().describe("create/update: Topic purpose")
18920
+ },
18921
+ surfaces: ["mcp", "cli"],
18922
+ toolset: "admin",
18923
+ run: forward("feedback_admin.topic")
18924
+ },
18925
+ {
18926
+ name: "admin_feedback_thread",
18927
+ title: "Maintain and merge shared Feedback Threads",
18928
+ description: "Privileged shared-forum moderation: create/update/move/transition/archive/restore Threads or merge a duplicate into a canonical destination with redirect and provenance. Merge safely repoints private associations but never copies raw evidence into shared content.",
18929
+ inputSchema: {
18930
+ action: exports_external.enum(["create", "update", "move", "transition", "archive", "restore", "merge"]).describe("the Thread lifecycle or merge action"),
18931
+ threadId: exports_external.string().optional().describe("update/move/transition/archive/restore: Thread id"),
18932
+ sourceThreadId: exports_external.string().optional().describe("merge: duplicate source Thread id"),
18933
+ targetThreadId: exports_external.string().optional().describe("merge: canonical destination Thread id"),
18934
+ topicId: exports_external.string().optional().describe("create/move/update: destination Feedback Topic id"),
18935
+ title: exports_external.string().min(1).optional().describe("create/update: Thread title"),
18936
+ objective: exports_external.string().min(1).optional().describe("create/update: Thread objective"),
18937
+ status: exports_external.enum(["active", "needs-review", "stuck", "standing", "resolved", "archived"]).optional().describe("transition/update: target Thread status"),
18938
+ reason: exports_external.string().optional().describe("merge: concise auditable rationale")
18939
+ },
18940
+ surfaces: ["mcp", "cli"],
18941
+ toolset: "admin",
18942
+ run: forward("feedback_admin.thread")
18943
+ },
18944
+ {
18945
+ name: "admin_feedback_publish",
18946
+ title: "Publish sanitized shared Feedback content",
18947
+ description: "Privileged: author a new sanitized shared contribution in a Feedback Thread, optionally recording that private evidence informed it. The body must be separately authored; Arbor refuses an exact raw-body copy and never flips private visibility.",
18948
+ inputSchema: {
18949
+ threadId: exports_external.string().describe("shared Feedback Thread id or merged source redirect"),
18950
+ type: exports_external.enum(CONTRIBUTION_TYPES).describe("shared contribution type, including maintainer decision"),
18951
+ body: exports_external.string().min(1).describe("deliberately sanitized shared body"),
18952
+ summary: exports_external.string().max(500).optional().describe("optional shared gist"),
18953
+ confidence: exports_external.number().int().min(0).max(100).optional(),
18954
+ idempotencyKey: exports_external.string().min(1).max(200).optional(),
18955
+ feedbackId: exports_external.string().optional().describe("optional private evidence id; additionally requires review_private")
18956
+ },
18957
+ surfaces: ["mcp", "cli"],
18958
+ toolset: "admin",
18959
+ run: forward("feedback_admin.publish")
18960
+ },
18961
+ {
18962
+ name: "admin_feedback_edit",
18963
+ title: "Edit sanitized shared Feedback content",
18964
+ description: "Privileged shared-forum moderation: correct a shared Feedback contribution's body, summary, or type while preserving normal Arbor audit history. This cannot target private feedback records.",
18965
+ inputSchema: {
18966
+ contributionId: exports_external.string().describe("shared Feedback contribution id"),
18967
+ body: exports_external.string().min(1).optional().describe("replacement sanitized body"),
18968
+ summary: exports_external.string().max(500).nullable().optional().describe("replacement gist; null/empty clears"),
18969
+ type: exports_external.enum(CONTRIBUTION_TYPES).optional().describe("replacement shared contribution type")
18970
+ },
18971
+ surfaces: ["mcp", "cli"],
18972
+ toolset: "admin",
18973
+ run: forward("feedback_admin.edit")
18974
+ },
18975
+ {
18976
+ name: "admin_feedback_curate",
18977
+ title: "Curate shared Feedback content",
18978
+ description: "Privileged shared-forum moderation: soft-delete or restore a sanitized shared contribution. This capability remains independent of private-feedback review access.",
18979
+ inputSchema: {
18980
+ verb: exports_external.enum(["delete", "restore"]),
18981
+ contributionId: exports_external.string().describe("shared Feedback contribution id")
18982
+ },
18983
+ surfaces: ["mcp", "cli"],
18984
+ toolset: "admin",
18985
+ run: dispatch({ delete: "feedback_admin.remove", restore: "feedback_admin.restore" })
18986
+ },
18662
18987
  {
18663
18988
  name: "contribute",
18664
18989
  title: "Contribute to a thread",
@@ -20668,11 +20993,70 @@ var ACTIONS = ACTION_DEFINITIONS.map((action) => {
20668
20993
  });
20669
20994
 
20670
20995
  // src/config.ts
20671
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20996
+ import {
20997
+ chmodSync,
20998
+ existsSync,
20999
+ mkdirSync,
21000
+ readFileSync,
21001
+ rmSync,
21002
+ statSync,
21003
+ writeFileSync
21004
+ } from "node:fs";
20672
21005
  import { homedir } from "node:os";
20673
21006
  import { dirname, join } from "node:path";
20674
21007
  var CONFIG_PATH = process.env.ARBOR_CONFIG ?? join(homedir(), ".arbor", "config.json");
20675
21008
  var DEFAULT_API_URL = process.env.ARBOR_API_URL ?? "https://arborthreads.com";
21009
+ var DEFAULT_CONFIG_PATH = join(homedir(), ".arbor", "config.json");
21010
+ var DEFAULT_TOKEN_OVERWRITE_REFUSAL = "refusing to overwrite ~/.arbor/config.json — a token already exists. Set ARBOR_CONFIG to an isolated path (e.g. ~/.arbor/<agent>.config.json) and retry.";
21011
+ var LOCK_TIMEOUT_MS = 5000;
21012
+ var LOCK_STALE_MS = 30000;
21013
+ var LOCK_POLL_MS = 20;
21014
+ function sleepSync(ms) {
21015
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
21016
+ }
21017
+ function acquireConfigLock(configPath) {
21018
+ const lockPath = `${configPath}.lock`;
21019
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
21020
+ for (;; ) {
21021
+ try {
21022
+ mkdirSync(lockPath);
21023
+ return () => {
21024
+ try {
21025
+ rmSync(lockPath, { recursive: true, force: true });
21026
+ } catch {}
21027
+ };
21028
+ } catch (err) {
21029
+ if (err?.code !== "EEXIST")
21030
+ throw err;
21031
+ try {
21032
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
21033
+ rmSync(lockPath, { recursive: true, force: true });
21034
+ continue;
21035
+ }
21036
+ } catch {
21037
+ continue;
21038
+ }
21039
+ if (Date.now() >= deadline) {
21040
+ throw new Error(`timed out waiting for another arbor process to finish writing ${configPath}. If none is running, remove ${lockPath}.`);
21041
+ }
21042
+ sleepSync(LOCK_POLL_MS);
21043
+ }
21044
+ }
21045
+ }
21046
+ function defaultConfigOverwriteBlocked(opts) {
21047
+ const isolated = opts?.isolated ?? Boolean(process.env.ARBOR_CONFIG);
21048
+ const path = opts?.path ?? DEFAULT_CONFIG_PATH;
21049
+ if (isolated)
21050
+ return false;
21051
+ if (!existsSync(path))
21052
+ return false;
21053
+ try {
21054
+ const cfg = JSON.parse(readFileSync(path, "utf8"));
21055
+ return Boolean(cfg.token);
21056
+ } catch {
21057
+ return false;
21058
+ }
21059
+ }
20676
21060
  function loadConfig() {
20677
21061
  const envToken = process.env.ARBOR_TOKEN || undefined;
20678
21062
  const envUrl = process.env.ARBOR_API_URL || undefined;
@@ -20684,15 +21068,32 @@ function loadConfig() {
20684
21068
  }
20685
21069
  return { apiUrl: envUrl ?? DEFAULT_API_URL, token: envToken };
20686
21070
  }
20687
- function saveConfig(cfg) {
20688
- mkdirSync(dirname(CONFIG_PATH), { recursive: true });
20689
- writeFileSync(CONFIG_PATH, `${JSON.stringify(cfg, null, 2)}
20690
- `);
20691
- chmodSync(CONFIG_PATH, 384);
21071
+ function writeConfigGuarded(opts) {
21072
+ mkdirSync(dirname(opts.configPath), { recursive: true });
21073
+ const release = acquireConfigLock(opts.configPath);
21074
+ try {
21075
+ if (!opts.allowDefaultTokenOverwrite && defaultConfigOverwriteBlocked({ isolated: opts.isolated, path: opts.defaultPath })) {
21076
+ throw new Error(DEFAULT_TOKEN_OVERWRITE_REFUSAL);
21077
+ }
21078
+ writeFileSync(opts.configPath, `${JSON.stringify(opts.cfg, null, 2)}
21079
+ `, { mode: 384 });
21080
+ chmodSync(opts.configPath, 384);
21081
+ } finally {
21082
+ release();
21083
+ }
21084
+ }
21085
+ function saveConfig(cfg, opts) {
21086
+ writeConfigGuarded({
21087
+ cfg,
21088
+ configPath: CONFIG_PATH,
21089
+ defaultPath: DEFAULT_CONFIG_PATH,
21090
+ isolated: Boolean(process.env.ARBOR_CONFIG),
21091
+ allowDefaultTokenOverwrite: opts?.allowDefaultTokenOverwrite
21092
+ });
20692
21093
  }
20693
21094
  function clearToken() {
20694
21095
  const cfg = loadConfig();
20695
- saveConfig({ apiUrl: cfg.apiUrl });
21096
+ saveConfig({ apiUrl: cfg.apiUrl }, { allowDefaultTokenOverwrite: true });
20696
21097
  }
20697
21098
 
20698
21099
  // src/errors.ts
@@ -21170,13 +21571,27 @@ function buildInput(inputSchema, flags, command) {
21170
21571
  }
21171
21572
 
21172
21573
  // src/commands.ts
21574
+ var COMMAND_WORD_OVERRIDES = {
21575
+ contribute_arbor_feedback: "feedback",
21576
+ feedback_recall: "feedback find",
21577
+ feedback_thread_get: "feedback thread"
21578
+ };
21173
21579
  function commandWords(action) {
21174
- return action.name.replace(/_/g, " ");
21580
+ return COMMAND_WORD_OVERRIDES[action.name] ?? action.name.replace(/_/g, " ");
21175
21581
  }
21176
21582
  var CLI_ACTIONS = ACTIONS.filter((a) => a.surfaces.includes("cli") || a.surfaces.includes("computer-cli"));
21177
21583
  var BY_COMMAND = new Map(CLI_ACTIONS.map((a) => [commandWords(a), a]));
21178
21584
  var MAX_WORDS = Math.max(1, ...CLI_ACTIONS.map((a) => commandWords(a).split(" ").length));
21179
21585
  var POSITIONAL_FIELDS = {
21586
+ contribute_arbor_feedback: ["body"],
21587
+ feedback_recall: ["query"],
21588
+ feedback_thread_get: ["threadId"],
21589
+ admin_feedback_capability: ["verb"],
21590
+ admin_feedback_evidence: ["verb"],
21591
+ admin_feedback_topic: ["action"],
21592
+ admin_feedback_thread: ["action"],
21593
+ admin_feedback_edit: ["contributionId"],
21594
+ admin_feedback_curate: ["verb", "contributionId"],
21180
21595
  app_fetch: ["appId", "path"],
21181
21596
  app_request: ["appId", "path"],
21182
21597
  computer_run_start: ["computerSessionId", "mode"],
@@ -21344,8 +21759,26 @@ function applyPositionals(action, extras, flags) {
21344
21759
  flags[target.flag] = value;
21345
21760
  }
21346
21761
  }
21762
+ function resolveCommand(positionals, flags) {
21763
+ const normalized = positionals.map((word) => word.replace(/_/g, " "));
21764
+ const feedbackInputFlags = new Set([
21765
+ "type",
21766
+ "body",
21767
+ "body-file",
21768
+ "summary",
21769
+ "summary-file",
21770
+ "confidence",
21771
+ "idempotency-key",
21772
+ "thread-id",
21773
+ "topic-id",
21774
+ "computer-session-id"
21775
+ ]);
21776
+ const explicitFeedbackInput = Object.keys(flags).some((flag) => feedbackInputFlags.has(flag));
21777
+ const bareFeedbackTree = normalized.length === 1 && normalized[0] === "feedback" && !explicitFeedbackInput;
21778
+ return bareFeedbackTree ? { action: CLI_ACTIONS.find((action) => action.name === "feedback_tree"), words: 1 } : matchCommand(positionals);
21779
+ }
21347
21780
  async function runObjectVerb(positionals, flags, ctx) {
21348
- const match = matchCommand(positionals);
21781
+ const match = resolveCommand(positionals, flags);
21349
21782
  if (!match) {
21350
21783
  const typed = positionals.join(" ") || "(none)";
21351
21784
  throw new UsageError(`unknown command: ${typed} — ${suggestCommand(positionals)}`);
@@ -21390,20 +21823,38 @@ async function runObjectVerb(positionals, flags, ctx) {
21390
21823
  // src/connect.ts
21391
21824
  var DEFAULT_POLL_INTERVAL_MS = 2000;
21392
21825
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21826
+ function bareConnectUrl(apiUrl, returnedUrl) {
21827
+ const fallback = `${apiUrl.replace(/\/$/, "")}/connect`;
21828
+ if (!returnedUrl)
21829
+ return fallback;
21830
+ try {
21831
+ return `${new URL(returnedUrl).origin}/connect`;
21832
+ } catch {
21833
+ return fallback;
21834
+ }
21835
+ }
21393
21836
  async function connect(opts) {
21837
+ if (defaultConfigOverwriteBlocked()) {
21838
+ throw new Error(DEFAULT_TOKEN_OVERWRITE_REFUSAL);
21839
+ }
21394
21840
  const apiUrl = opts.url ?? loadConfig().apiUrl;
21395
21841
  const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
21396
- const body = JSON.stringify({ pairingKey: opts.pairingKey });
21397
21842
  const headers = { "content-type": "application/json" };
21398
- const initRes = await fetch(`${apiUrl}/api/agent/connect`, { method: "POST", headers, body });
21843
+ const body = JSON.stringify({ pairingKey: opts.pairingKey });
21844
+ const initRes = await fetch(`${apiUrl}/api/agent/connect`, {
21845
+ method: "POST",
21846
+ headers,
21847
+ body
21848
+ });
21399
21849
  const init = await initRes.json().catch(() => ({}));
21400
21850
  if (!initRes.ok || !init.url || !init.userCode) {
21401
21851
  throw new Error(`could not start pairing (${initRes.status}): ${init.error ?? "is the server reachable?"}`);
21402
21852
  }
21853
+ const approvalUrl = bareConnectUrl(apiUrl, init.url);
21403
21854
  process.stderr.write(`
21404
21855
  Pairing against ${apiUrl}
21405
21856
  ` + ` Ask the person who will manage this agent${init.agentName ? ` (“${init.agentName}”)` : ""} to approve it:
21406
- ` + ` 1. open ${init.url}
21857
+ ` + ` 1. open ${approvalUrl}
21407
21858
  ` + ` 2. enter the code: ${init.userCode}
21408
21859
 
21409
21860
  ` + ` Only approve an agent you intend to manage.
@@ -21478,7 +21929,7 @@ async function login(opts) {
21478
21929
  });
21479
21930
  const tok = await tokRes.json().catch(() => ({}));
21480
21931
  if (tok.access_token) {
21481
- saveConfig({ apiUrl, token: tok.access_token });
21932
+ saveConfig({ apiUrl, token: tok.access_token }, { allowDefaultTokenOverwrite: true });
21482
21933
  return;
21483
21934
  }
21484
21935
  if (tok.error === "authorization_pending")
@@ -21680,7 +22131,7 @@ async function main() {
21680
22131
  if (!token)
21681
22132
  throw new UsageError("usage: arbor auth <token> [--url <api-url>]");
21682
22133
  const url2 = stringFlag(flags.url, "url") ?? loadConfig().apiUrl;
21683
- saveConfig({ apiUrl: url2, token });
22134
+ saveConfig({ apiUrl: url2, token }, { allowDefaultTokenOverwrite: true });
21684
22135
  advise(` ✓ Token saved (${CONFIG_PATH}).
21685
22136
  `, ctx);
21686
22137
  await renderMe(ctx, "auth");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdacurry/arbor",
3
- "version": "0.20.31",
3
+ "version": "0.20.33",
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",