@remits/remits-cli 0.1.166 → 0.1.167

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/index.js CHANGED
@@ -15,7 +15,7 @@
15
15
  - L9873 Tokens, Tools, Verification, And Config
16
16
  - L11390 Service Dashboard And WebSocket Listener
17
17
  - L13900 Agent And Ticket Workflows
18
- - L18202 Help, Auto Update, And Command Dispatch
18
+ - L18308 Help, Auto Update, And Command Dispatch
19
19
  */
20
20
 
21
21
  /*
@@ -14613,8 +14613,50 @@ function agentSessionFor(flags, entry) {
14613
14613
  };
14614
14614
  }
14615
14615
 
14616
- /** Every account repo checked out on this machine, as registration claims. The platform verifies them. */
14617
- function agentAccountClaims(cwd) {
14616
+ /**
14617
+ * `--accounts 33,52` -> { roots: [33, 52], descendants: true }; `--no-descendants` narrows each root to
14618
+ * itself. Returns null when coverage was not declared, which keeps today's behaviour (every indexed repo).
14619
+ */
14620
+ function parseAgentCoverage(flags) {
14621
+ const raw = flags && (flags.accounts || flags['account-ids']);
14622
+ if (raw === undefined || raw === null || raw === true || raw === false) {
14623
+ return null;
14624
+ }
14625
+ const roots = String(raw).split(',')
14626
+ .map((entry) => Number(String(entry).trim()))
14627
+ .filter((id, index, all) => Number.isFinite(id) && id > 0 && all.indexOf(id) === index);
14628
+ if (!roots.length) {
14629
+ throw new Error('--accounts needs one or more numeric account ids, e.g. --accounts 33 or --accounts 33,52.');
14630
+ }
14631
+ const descendants = !(flagEnabled(flags['no-descendants']) || flags.descendants === false ||
14632
+ String(flags.descendants || '').toLowerCase() === 'false');
14633
+ return { roots, descendants };
14634
+ }
14635
+
14636
+ /**
14637
+ * Every account repo checked out on this machine, as registration claims. The platform verifies them.
14638
+ *
14639
+ * With declared coverage (`--accounts`), ONLY those roots are claimed — each with its descendants unless
14640
+ * `--no-descendants` — whether or not a checkout of it is indexed. Without it, every indexed repository is
14641
+ * claimed, which is the historical behaviour and is too wide for a fleet member meant to serve one tree.
14642
+ * The session account (`--account-id`) is context, never coverage.
14643
+ */
14644
+ function agentAccountClaims(cwd, coverage) {
14645
+ if (coverage) {
14646
+ const indexed = listAccountRepoEntries(readAccountRepoIndex());
14647
+ return coverage.roots.map((accountId) => {
14648
+ const entry = indexed.find((candidate) => Number(candidate.accountId) === accountId) || {};
14649
+ return {
14650
+ accountId,
14651
+ name: entry.name || null,
14652
+ type: entry.type || null,
14653
+ platformAccountId: entry.platformAccountId || null,
14654
+ directory: entry.directory || null,
14655
+ routingScope: coverage.descendants ? 'self_and_descendants' : 'self',
14656
+ source: 'coverage'
14657
+ };
14658
+ });
14659
+ }
14618
14660
  const claims = listAccountRepoEntries(readAccountRepoIndex()).map((entry) => ({
14619
14661
  accountId: Number(entry.accountId),
14620
14662
  name: entry.name || null,
@@ -14662,6 +14704,7 @@ async function agentRegisterCommand(flags, options = {}) {
14662
14704
  try { discoverAccountRepos(); } catch (_) { /* best-effort */ }
14663
14705
 
14664
14706
  const { session, baseUrl, dataMode } = agentSessionFor(flags, null);
14707
+ const coverage = parseAgentCoverage(Object.assign({}, options.serve && options.serve.coverageFlags, flags));
14665
14708
  const anchor = resolveAgentAnchorPid();
14666
14709
  if (!anchor.pid) {
14667
14710
  throw new Error(
@@ -14698,7 +14741,8 @@ async function agentRegisterCommand(flags, options = {}) {
14698
14741
  workspace: resolveWorkspace(cwd, flags) || undefined,
14699
14742
  dataMode,
14700
14743
  accountId: Number(session.accountId) || undefined,
14701
- accounts: agentAccountClaims(cwd),
14744
+ accounts: agentAccountClaims(cwd, coverage),
14745
+ coverageExplicit: coverage ? true : undefined,
14702
14746
  // Capacity is how many workers this session can actually RUN, which only this side knows. The
14703
14747
  // platform stops routing past it, so a supervisor at capacity is not handed work it cannot start.
14704
14748
  maxConcurrent: options.serve ? options.serve.maxConcurrent : (flags['max-concurrent'] ? parsePositiveInt(flags['max-concurrent'], 1) : undefined),
@@ -14733,6 +14777,7 @@ async function agentRegisterCommand(flags, options = {}) {
14733
14777
  // The accounts the platform ACCEPTED, so `agent list` can scope itself to the work this session
14734
14778
  // can actually be routed rather than to whichever session happened to be most recent.
14735
14779
  accountIds: response.accountIds || [],
14780
+ coverage: coverage || null,
14736
14781
  accountId: Number(session.accountId) || null,
14737
14782
  anchorPid: anchor.pid,
14738
14783
  anchorSource: anchor.source,
@@ -14774,6 +14819,13 @@ async function agentRegisterCommand(flags, options = {}) {
14774
14819
  console.log(' data lane ' + dataMode + (dataMode === 'prod' ? ' (live support tickets)' : ' (fixtures only)'));
14775
14820
  console.log(' accounts ' + accepted.length + ' account(s) this session can be routed work for');
14776
14821
  console.log(' ' + accepted.join(', '));
14822
+ // The scope the platform ACCEPTED, per claim — "33 + descendants" vs "every repo on this machine" is the
14823
+ // difference between a bounded fleet member and one that takes any account it can reach.
14824
+ const scopes = (response.accounts || []).map((entry) => entry.accountId +
14825
+ (entry.routingScope === 'self_and_descendants' ? ' + descendants' : ''));
14826
+ console.log(' coverage ' + (coverage
14827
+ ? 'declared (--accounts): ' + scopes.join(', ')
14828
+ : 'every indexed account repo (bound it with --accounts <id>[,<id>] [--no-descendants]): ' + scopes.join(', ')));
14777
14829
  console.log(' heartbeat every ' + Math.round(AGENT_HEARTBEAT_INTERVAL_MS / 1000) + 's, anchored to pid ' +
14778
14830
  anchor.pid + ' (' + anchor.source + '), ends when this session ends');
14779
14831
  if ((response.skippedAccounts || []).length) {
@@ -15010,6 +15062,14 @@ function startAgentHeartbeatDaemon(entry, serve = null) {
15010
15062
  if (entry.persona) {
15011
15063
  args.push('--persona', entry.persona);
15012
15064
  }
15065
+ // Declared coverage travels with the daemon, so a re-registration after a TTL lapse cannot silently
15066
+ // widen a fleet member back to every indexed repository.
15067
+ if (entry.coverage && Array.isArray(entry.coverage.roots) && entry.coverage.roots.length) {
15068
+ args.push('--accounts', entry.coverage.roots.join(','));
15069
+ if (entry.coverage.descendants === false) {
15070
+ args.push('--no-descendants', 'true');
15071
+ }
15072
+ }
15013
15073
  if (serve) {
15014
15074
  args.push(
15015
15075
  '--serve', 'true',
@@ -15185,6 +15245,9 @@ async function releaseTicketClaim(context, agentId, ticket, options = {}) {
15185
15245
  accountId: Number(ticket.accountId),
15186
15246
  outcome: options.outcome || null,
15187
15247
  notes: options.notes || null,
15248
+ // The run whose claim this is. The platform ignores the call when a NEWER run holds the claim, so
15249
+ // a late exit callback cannot drop a restarted supervisor's claim or erase a handoff it routed.
15250
+ runId: options.runId || null,
15188
15251
  release: options.release === true,
15189
15252
  unroute: options.unroute === true
15190
15253
  }).then((r) => r.data);
@@ -15455,6 +15518,7 @@ async function spawnTicketWorker(context, ctx, ticket) {
15455
15518
  // it back to the queue) once it has genuinely failed enough times and is still not terminal.
15456
15519
  const giveUp = !ok && attempt >= AGENT_WORKER_MAX_ATTEMPTS;
15457
15520
  const result = await releaseTicketClaim(context, agentId, { ticketId, accountId: ticket.accountId }, {
15521
+ runId: ctx.runId,
15458
15522
  outcome: ok ? 'completed' : (signal ? 'killed' : 'failed'),
15459
15523
  release: giveUp,
15460
15524
  // A clean exit that left the ticket short of terminal is a DELIVERED judgement, not a failure —
@@ -15551,7 +15615,7 @@ async function agentHeartbeatDaemonCommand(flags) {
15551
15615
  const context = agentSessionFor(flags, { cwd, baseUrl: flags['base-url'] });
15552
15616
  for (const [ticketId, entry] of inFlight.entries()) {
15553
15617
  await releaseTicketClaim(context, agentId,
15554
- { ticketId, accountId: entry.accountId }, { outcome: 'supervisor_stopped' });
15618
+ { ticketId, accountId: entry.accountId }, { outcome: 'supervisor_stopped', runId: ctx.runId });
15555
15619
  }
15556
15620
  await buildAxios(context.baseUrl, context.session.token, 5000)
15557
15621
  .post('/cli/agentDeregister', { agentId });
@@ -15691,6 +15755,8 @@ async function agentHeartbeatDaemonCommand(flags) {
15691
15755
  'max-concurrent': String(ctx.maxConcurrent),
15692
15756
  role: ctx.role || undefined,
15693
15757
  persona: ctx.persona || undefined,
15758
+ accounts: flags.accounts || undefined,
15759
+ 'no-descendants': flags['no-descendants'] || undefined,
15694
15760
  json: 'true'
15695
15761
  },
15696
15762
  // Re-register with THIS supervisor's declared bound and queues. The platform treats an absent
@@ -15990,6 +16056,26 @@ async function ticketCommand(flags, subcommand) {
15990
16056
  idempotencyKey: flags['idempotency-key'] || flags.idempotencyKey || undefined,
15991
16057
  providerMessageId: flags['provider-message-id'] || undefined,
15992
16058
  reason: flags.reason,
16059
+ // Explicit requests and decisions. A request id is short (R1, R2); `2`, `r2` and `#2` all work.
16060
+ request: flags.request || undefined,
16061
+ decision: flags.decision || flags._?.[3] || undefined,
16062
+ requestAction: flags.for || flags['request-action'] || undefined,
16063
+ approvers: flags.approvers ? String(flags.approvers).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
16064
+ reviewers: flags.reviewers ? String(flags.reviewers).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
16065
+ expiresAt: flags['expires-at'] || undefined,
16066
+ delivery: flags.delivery || undefined,
16067
+ // The revision a call acts on or a decision looked at: a patch digest, a staged snapshot, a version.
16068
+ revision: flags.revision || undefined,
16069
+ note: flags.note,
16070
+ // Handoff: who should take the next step, and whether this agent must be kept off it (independence).
16071
+ exclude: flagEnabled(flags.exclude) ? true : undefined,
16072
+ reviewAction: flags['review-action'] || flags.review || undefined,
16073
+ findings: flags.findings ? String(flags.findings).split('|').map((v) => v.trim()).filter(Boolean) : undefined,
16074
+ // `complete` after new input arrived during this run: say you read it.
16075
+ acknowledgeInput: flagEnabled(flags['acknowledge-input']) ? true : undefined,
16076
+ // `deliver`: bind the request to one conversation entry, and hold it for a person's approval.
16077
+ messageId: flags['message-id'] || undefined,
16078
+ requireApproval: flagEnabled(flags['require-approval']) ? true : undefined,
15993
16079
  // Lease operations act as the AGENT, not the user: the lease is held by a worker session, and a
15994
16080
  // human's email is not a thing that can hold one. Inside a worker these come from the environment
15995
16081
  // the supervisor set, so the commands need no flags.
@@ -16688,9 +16774,27 @@ function printTicketHelp() {
16688
16774
  console.log(' accept Claim it as yours before you start work.');
16689
16775
  console.log(' status --status in_progress|pending_review');
16690
16776
  console.log(' progress --summary "..." [--category investigation|fix|verification|...] [--next-step "..."]');
16691
- console.log(' complete --resolution "what you found and did"');
16692
- console.log(' ask --question "..." [--context "..."] [--to WHO]');
16693
- console.log(' Put a decision to a human and park. Use it when the CHOICE is not yours.');
16777
+ console.log(' complete --resolution "what you found and did" [--revision R] [--acknowledge-input]');
16778
+ console.log(' Refused when new input arrived after your run started and you have not read it;');
16779
+ console.log(' read the ticket, then pass --acknowledge-input if your resolution still stands.');
16780
+ console.log(' ask --question "..." [--context "..."] [--to WHO] [--visibility internal]');
16781
+ console.log(' Put a question to a human and park. Use it when the CHOICE is not yours. Recorded as');
16782
+ console.log(' request R<n>; answering it resumes the work even if no worker is online right now.');
16783
+ console.log(' request-approval --for ACTION [--revision R | --delivery ID] [--approvers a@x,b@y] [--summary "..."]');
16784
+ console.log(' Ask a PERSON to authorize one action on one revision (promote a patch, send a message).');
16785
+ console.log(' An answer is not an approval: only an authenticated person can decide it, never an');
16786
+ console.log(' agent run, and a changed revision needs a new approval.');
16787
+ console.log(' request-review --for ACTION --revision R [--reviewers ...]');
16788
+ console.log(' Ask another worker or a person to review exactly this revision. You cannot review your own.');
16789
+ console.log(' decide --request R2 --decision approve|reject|changes [--revision R] [--note "..."]');
16790
+ console.log(' Decide an approval or review. Name the revision you looked at; a stale one is refused.');
16791
+ console.log(' cancel-request --request R2 [--reason "..."]');
16792
+ console.log(' handoff --summary "what the next worker should do" [--workstream review] [--exclude]');
16793
+ console.log(' [--review-action promote --revision R] [--next-step "..."] [--findings "a|b"]');
16794
+ console.log(' Hand the next step to another session (queued if nobody is online). --exclude keeps');
16795
+ console.log(' YOU off it — use it for independent review. Your exit will not erase this handoff.');
16796
+ console.log(' continue [--reason requested] Queue the ticket for the automation to work again.');
16797
+ console.log(' reset-budget OPERATOR: restore a ticket\'s run budget after looking at it.');
16694
16798
  console.log(' answer --message "..." [--route false] (alias: reply)');
16695
16799
  console.log(' Answer the open question a previous run asked. By default this re-routes the ticket,');
16696
16800
  console.log(' so a fresh worker resumes with your answer already in its brief. This is NOT how you');
@@ -16699,11 +16803,13 @@ function printTicketHelp() {
16699
16803
  console.log(' message --message "..." [--to a@x,b@y] [--subject "..."] [--channel email]');
16700
16804
  console.log(' A PUBLIC conversation entry the requester reads. Writing it does not send it:');
16701
16805
  console.log(' the record is vendor-agnostic and the account\'s own rule chooses the transport.');
16702
- console.log(' deliver --channel email --to a@x [--template T] [--subject "..."] [--idempotency-key K]');
16806
+ console.log(' deliver --channel email --to a@x [--message-id M] [--require-approval] [--subject "..."] [--idempotency-key K]');
16703
16807
  console.log(' Ask for that entry to actually be SENT. It records an outbox request; an account-owned');
16704
16808
  console.log(' rule or scheduled Action delivers it and marks the result. Use it when a ticket is');
16705
16809
  console.log(' waiting on somebody who is not watching the queue.');
16706
16810
  console.log(' deliveries [--channel email] What is queued to go out, and what already went.');
16811
+ console.log(' Senders take a delivery exclusively (claimDelivery) before sending; a failure retries');
16812
+ console.log(' with backoff; an "uncertain" or "blocked" one waits for a person to reconcile it.');
16707
16813
  console.log(' delivered --delivery-id ID | delivery-failed --delivery-id ID --reason "..."');
16708
16814
  console.log(' release [--notes "..."] Hand it back to the queue.');
16709
16815
  console.log(' reclaim [--ticket ID | --account-id ID] [--stale-hours 24] [--apply]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remits/remits-cli",
3
- "version": "0.1.166",
3
+ "version": "0.1.167",
4
4
  "description": "Local CLI for auth, component sync, and live test execution against Remits",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -26,6 +26,7 @@ independent of everything else — no background service is required, and every
26
26
 
27
27
  ```bash
28
28
  remits-cli agent serve # routable AND autonomously working tickets — the usual choice
29
+ remits-cli agent serve --accounts 33 # ...bounded to account 33 and its descendants
29
30
  remits-cli agent workers # what this session is running right now
30
31
  remits-cli agent list # who else is available across the accounts you cover
31
32
  remits-cli agent release # stop serving and stop receiving tickets right now
@@ -12,6 +12,7 @@
12
12
  - [You are an agent, and you register yourself](#you-are-an-agent-and-you-register-yourself)
13
13
  - [Autonomous: one ticket, one process](#autonomous-one-ticket-one-process)
14
14
  - [Serving only some workstreams](#serving-only-some-workstreams)
15
+ - [Serving only some accounts](#serving-only-some-accounts)
15
16
  - [The manual loop](#the-manual-loop)
16
17
  - [If you are the worker](#if-you-are-the-worker)
17
18
  - [Before you edit anything: where you are, and whether you may](#before-you-edit-anything-where-you-are-and-whether-you-may)
@@ -24,6 +25,7 @@
24
25
  - [Agent components are workers too](#agent-components-are-workers-too)
25
26
  - [Moving a ticket through its lifecycle](#moving-a-ticket-through-its-lifecycle)
26
27
  - [When a worker needs a decision from a human](#when-a-worker-needs-a-decision-from-a-human)
28
+ - [Approvals, reviews and handing work on](#approvals-reviews-and-handing-work-on)
27
29
 
28
30
  ## Support Ticket Mental Model
29
31
 
@@ -155,6 +157,22 @@ than quietly overriding the declaration —
155
157
  **So after specialising a fleet, run `remits-cli agent map`** and confirm every workstream the
156
158
  account ingests is covered by something.
157
159
 
160
+ ### Serving only some accounts
161
+
162
+ Without a bound, a session claims **every account repository indexed on this machine**. `--accounts`
163
+ bounds it to named roots and their descendants:
164
+
165
+ ```bash
166
+ remits-cli agent serve --accounts 33 # account 33 and everything beneath it
167
+ remits-cli agent serve --accounts 33,52 --no-descendants
168
+ ```
169
+
170
+ `--account-id` only chooses which session context a command uses; it is not coverage. The registration
171
+ output prints the coverage the platform **accepted** — check it before leaving a production supervisor
172
+ running. The bound survives heartbeat re-registration. Only use it when the user asked for a bounded
173
+ fleet member, and never register a production worker merely to look at configuration: registering makes
174
+ the session routable.
175
+
158
176
  ### The manual loop
159
177
 
160
178
  Only when the session registered with `agent register` rather than `agent serve`.
@@ -542,6 +560,38 @@ Inside an autonomous worker `--ticket` defaults to the ticket that worker was la
542
560
  A refusal is an **answer**: "accept it first", "already assigned to someone else", "reopen it
543
561
  first". Act on the sentence — do not go looking for another tool that lets you skip the step.
544
562
 
563
+ An answer resumes the work **even if no worker is online when it arrives**: the ticket records queued work
564
+ (`pendingWork`) and the next eligible worker takes it, whatever its status and owner. Each question is a
565
+ request with a short id (`R1`); answer a specific one with `ticket answer --request R1 --message "..."`.
566
+ If new input arrives while you are working, `ticket complete` is refused and quotes it — read it, then
567
+ complete with `--acknowledge-input` if your resolution still stands.
568
+
569
+ ### Approvals, reviews and handing work on
570
+
571
+ An answer is not an authorization. When a person must **authorize** something — promoting a patch, a
572
+ production data change, sending a sensitive message — request an approval bound to the exact revision:
573
+
574
+ ```bash
575
+ remits-cli ticket request-approval --for promote --revision <patch-digest> --summary "what and why"
576
+ remits-cli ticket request-approval --for deliver --delivery <delivery-id> # approve exactly that message
577
+ ```
578
+
579
+ You cannot decide it — a person does, with `remits-cli ticket decide --request R2 --decision approve
580
+ --revision <rev>`. A changed patch needs a new approval. Record verification against the revision with
581
+ `ticket progress --category verification --revision <rev>`; an account rule may require exactly that.
582
+
583
+ When somebody else should do the next step — independent review, a different workstream:
584
+
585
+ ```bash
586
+ remits-cli ticket handoff --summary "fix staged in ticket-19463; needs independent review" \
587
+ --workstream review --exclude --review-action promote --revision <patch-digest> \
588
+ --next-step "review the patch against the reported case"
589
+ ```
590
+
591
+ The handoff is queued even if nobody is online, `--exclude` keeps you off it (independent review needs
592
+ that), and your own exit will not undo it. A reviewer decides with `ticket decide --request R3 --decision
593
+ approve|changes|reject --revision <rev>`; "changes" sends the work back with the reviewer's note.
594
+
545
595
  **`mcp_support_ticket` is an account convention, not a platform guarantee.** It belongs to one
546
596
  particular System Account: it may not exist on the platform you are on, it scopes to its own
547
597
  account, and it will refuse a ticket owned by a different one. Use it only when you are working in