@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
|
-
-
|
|
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
|
-
/**
|
|
14617
|
-
|
|
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('
|
|
16693
|
-
console.log('
|
|
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 [--
|
|
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
|
@@ -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
|