@remits/remits-cli 0.1.112 → 0.1.113
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/README.md +3 -1
- package/index.js +749 -18
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +186 -4
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ remits-cli stop
|
|
|
21
21
|
remits-cli install --skills
|
|
22
22
|
remits-cli tools
|
|
23
23
|
remits-cli tool --base-url http://localhost:8080 --name "My Tool" --input '{"foo":"bar"}'
|
|
24
|
+
remits-cli tool --name mcp_firestore_search --input '{"collection":"statements","documentId":"1234"}' --scope children
|
|
24
25
|
remits-cli components stage
|
|
25
26
|
remits-cli components status
|
|
26
27
|
remits-cli components clear
|
|
@@ -70,7 +71,8 @@ remits-cli install --skills --overwrite true
|
|
|
70
71
|
- `components commit` is a convenience wrapper that performs local git commit/push, then `components sync`, then local `git fetch`/`git pull --ff-only`.
|
|
71
72
|
- `components sync` returns the post-sync branch SHA produced by the platform. `components commit` verifies that `origin/<branch>` and local `HEAD` both match that exact SHA after the final pull.
|
|
72
73
|
- `components push` is deprecated and currently behaves the same as `components stage`.
|
|
73
|
-
- `accountId` resolution for CLI commands: explicit `--account-id` flag wins, then the current repo's `account-info.json` (`resolution.accountId`, then the short-lived `repoContext.accountInfoAccountId`, then the legacy top-level id — legacy files are rooted at the hierarchy ROOT, so their top-level `id` may be an ancestor), then the active session.
|
|
74
|
+
- `accountId` resolution for CLI commands: explicit `--account-id` flag wins, then the current repo's `account-info.json` (`resolution.accountId`, then the short-lived `repoContext.accountInfoAccountId`, then the legacy top-level id — legacy files are rooted at the hierarchy ROOT, so their top-level `id` may be an ancestor), then the active session.
|
|
75
|
+
- For `remits-cli tool`, read/discovery tools treat that resolved account as the **scope root**. You do not need the owning child account id for an exact document/record lookup: pass `--scope children`, or put `scope:"children"` in `--input`, and use the returned `resolvedTargetAccountId` for follow-up writes/runs. Use `--target-account-id` when you already know the exact owner, `--account-ids` for an explicit bounded owner list, and `--anchor-account-id` only to disambiguate multi-parent paths. Mutating tools still require an exact target.
|
|
74
76
|
- Auth sessions are stored per `accountId + dataMode + baseUrl`, so the same account can stay authenticated against both localhost and production without overwriting the other session.
|
|
75
77
|
- `--base-url` and `--data-mode` are independent. `--base-url` chooses the Remits host (`http://localhost:8080` vs deployed prod), while `--data-mode` chooses the data segment on that host (`test` vs `prod`). Do not assume `--data-mode prod` means the deployed prod host, or that `--data-mode test` means localhost.
|
|
76
78
|
- `remits-cli start` scans the machine for `account-info.json` files and rebuilds `~/.remits-cli/account-repos.json` before bringing up the background service.
|
package/index.js
CHANGED
|
@@ -1248,13 +1248,45 @@ function countAccountRepos(index) {
|
|
|
1248
1248
|
return listAccountRepoEntries(index).length;
|
|
1249
1249
|
}
|
|
1250
1250
|
|
|
1251
|
+
/**
|
|
1252
|
+
* Fold discovered checkouts into the index, one entry per account.
|
|
1253
|
+
*
|
|
1254
|
+
* <p><b>Two checkouts can claim one account, and the naive merge picked a silent winner.</b> The index is
|
|
1255
|
+
* keyed by accountId, so a plain last-write-wins loop resolved a fork, a worktree or a dated backup copy
|
|
1256
|
+
* by scan order — which is alphabetical, which is meaningless. Measured on this machine: account 33 had
|
|
1257
|
+
* both `remits-invoice-automation` and `remits-invoice-automation copy-EOD-09302025`, and the index held
|
|
1258
|
+
* the dated copy. Four open tickets would have been worked in a month-old snapshot.</p>
|
|
1259
|
+
*
|
|
1260
|
+
* <p>So: a real checkout always beats a non-checkout (an unpacked export cannot win merely by sorting
|
|
1261
|
+
* later); an explicit `cwd` entry — the operator standing in the directory — always beats a scan; and
|
|
1262
|
+
* when two genuine checkouts remain, the incumbent is KEPT and the loser is recorded in `ambiguousWith`
|
|
1263
|
+
* rather than discarded. Ambiguity a person can see is recoverable; a silent winner is not.</p>
|
|
1264
|
+
*/
|
|
1251
1265
|
function mergeAccountRepoEntries(entries, options = {}) {
|
|
1252
1266
|
const nextIndex = options.existingIndex ? { ...options.existingIndex } : {};
|
|
1267
|
+
const rank = (entry) => (isAccountCheckout(entry.directory) ? 2 : 0) + (entry.source === 'cwd' ? 1 : 0);
|
|
1268
|
+
|
|
1253
1269
|
for (const entry of entries) {
|
|
1254
1270
|
if (!entry || !entry.accountId || !entry.directory) {
|
|
1255
1271
|
continue;
|
|
1256
1272
|
}
|
|
1257
|
-
|
|
1273
|
+
const key = String(entry.accountId);
|
|
1274
|
+
const current = nextIndex[key];
|
|
1275
|
+
if (!current || !current.directory || current.directory === entry.directory) {
|
|
1276
|
+
nextIndex[key] = { ...entry, ambiguousWith: (current && current.ambiguousWith) || [] };
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const winner = rank(entry) > rank(current) ? entry : current;
|
|
1281
|
+
const loser = winner === entry ? current : entry;
|
|
1282
|
+
const seen = new Set([...((current.ambiguousWith) || []), ...((entry.ambiguousWith) || [])]);
|
|
1283
|
+
// Only a genuine rival is worth reporting. A non-checkout losing to a checkout is the system
|
|
1284
|
+
// working, not a decision the operator needs to review.
|
|
1285
|
+
if (isAccountCheckout(loser.directory) && isAccountCheckout(winner.directory)) {
|
|
1286
|
+
seen.add(loser.directory);
|
|
1287
|
+
}
|
|
1288
|
+
seen.delete(winner.directory);
|
|
1289
|
+
nextIndex[key] = { ...winner, ambiguousWith: [...seen] };
|
|
1258
1290
|
}
|
|
1259
1291
|
writeAccountRepoIndex(nextIndex);
|
|
1260
1292
|
return nextIndex;
|
|
@@ -3756,6 +3788,51 @@ function parseToolInput(flags) {
|
|
|
3756
3788
|
return {};
|
|
3757
3789
|
}
|
|
3758
3790
|
|
|
3791
|
+
const TOOL_SCOPES = ['self', 'children', 'hierarchy', 'parents'];
|
|
3792
|
+
|
|
3793
|
+
// Hierarchy-scoped discovery flags, folded into the tool's own input.
|
|
3794
|
+
//
|
|
3795
|
+
// They are CONVENIENCE only: every one of them can be written inside --input, and a value already there
|
|
3796
|
+
// wins, because the explicit JSON is what the caller typed for this specific tool. The flags exist so the
|
|
3797
|
+
// overwhelmingly common shape — "search my repo account's children for this id" — does not require
|
|
3798
|
+
// hand-writing JSON.
|
|
3799
|
+
function applyScopeFlags(input, flags) {
|
|
3800
|
+
const merged = input && typeof input === 'object' ? input : {};
|
|
3801
|
+
|
|
3802
|
+
const scope = flags && (flags.scope || flags['tool-scope']);
|
|
3803
|
+
if (scope && merged.scope === undefined) {
|
|
3804
|
+
const normalized = String(scope).trim().toLowerCase();
|
|
3805
|
+
if (!TOOL_SCOPES.includes(normalized)) {
|
|
3806
|
+
throw new Error('Invalid --scope "' + scope + '". Valid values: ' + TOOL_SCOPES.join(', '));
|
|
3807
|
+
}
|
|
3808
|
+
merged.scope = normalized;
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
const targetAccountId = flags && (flags['target-account-id'] || flags.targetAccountId);
|
|
3812
|
+
if (targetAccountId && merged.targetAccountId === undefined) {
|
|
3813
|
+
merged.targetAccountId = String(targetAccountId).trim();
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
const anchorAccountId = flags && (flags['anchor-account-id'] || flags.anchorAccountId);
|
|
3817
|
+
if (anchorAccountId && merged.anchorAccountId === undefined) {
|
|
3818
|
+
merged.anchorAccountId = String(anchorAccountId).trim();
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
const accountIds = flags && (flags['account-ids'] || flags.accountIds);
|
|
3822
|
+
if (accountIds && merged.accountIds === undefined) {
|
|
3823
|
+
const parsed = String(accountIds)
|
|
3824
|
+
.split(',')
|
|
3825
|
+
.map((part) => Number(String(part).trim()))
|
|
3826
|
+
.filter((value) => Number.isFinite(value) && value > 0);
|
|
3827
|
+
if (!parsed.length) {
|
|
3828
|
+
throw new Error('--account-ids expects a comma-separated list of numeric account ids');
|
|
3829
|
+
}
|
|
3830
|
+
merged.accountIds = parsed;
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
return merged;
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3759
3836
|
async function toolCommand(flags) {
|
|
3760
3837
|
const cwd = process.cwd();
|
|
3761
3838
|
ensureLocalState(cwd);
|
|
@@ -3774,7 +3851,7 @@ async function toolCommand(flags) {
|
|
|
3774
3851
|
const explicitFlagAccountId = Number(flags && flags['account-id']);
|
|
3775
3852
|
const accountIdExplicit = Number.isFinite(explicitFlagAccountId) && explicitFlagAccountId > 0;
|
|
3776
3853
|
|
|
3777
|
-
const input = parseToolInput(flags);
|
|
3854
|
+
const input = applyScopeFlags(parseToolInput(flags), flags);
|
|
3778
3855
|
const baseUrl = flags['base-url'] || session.baseUrl || DEFAULT_BASE_URL;
|
|
3779
3856
|
const branchName = flags.branch || currentBranch(cwd);
|
|
3780
3857
|
const dataMode = resolveDataMode(flags, session);
|
|
@@ -3840,6 +3917,10 @@ async function toolCommand(flags) {
|
|
|
3840
3917
|
token: session.token,
|
|
3841
3918
|
accountId,
|
|
3842
3919
|
accountIdExplicit,
|
|
3920
|
+
// The repo/session account, sent as a FIRST-CLASS value rather than left to be inferred from
|
|
3921
|
+
// `accountId`: a read tool may reinterpret its own `input.accountId`, and the scope root — what bounds
|
|
3922
|
+
// what the call may discover — has to survive that. The server validates it; it is never authorization.
|
|
3923
|
+
scopeRootAccountId: accountId,
|
|
3843
3924
|
branchName,
|
|
3844
3925
|
workspace: resolveWorkspace(cwd, flags),
|
|
3845
3926
|
dataMode,
|
|
@@ -6074,28 +6155,55 @@ const AGENT_WORKER_MAX_LOG_BYTES = 8 * 1024 * 1024;
|
|
|
6074
6155
|
const AGENT_LAUNCHERS = {
|
|
6075
6156
|
codex: {
|
|
6076
6157
|
binary: 'codex',
|
|
6158
|
+
// A ticket worker's whole job runs THROUGH the platform: `remits-cli ticket read/accept/progress/
|
|
6159
|
+
// complete`, and every investigation tool. So the sandbox has to let it reach the platform, and by
|
|
6160
|
+
// default it does not — `workspace-write` disables network, and a worker launched without this flag
|
|
6161
|
+
// fails its FIRST command with exit 6 / HTTP 000 while looking, from the outside, like an agent that
|
|
6162
|
+
// simply did not do much. Passed on the command line rather than documented as a config edit so the
|
|
6163
|
+
// supervisor works on a machine nobody has prepared.
|
|
6164
|
+
//
|
|
6165
|
+
// `read-only` is deliberately NOT used for investigate mode, even though it is the tighter sandbox.
|
|
6166
|
+
// It has no network AND no setting that restores it, so a read-only worker cannot read the ticket it
|
|
6167
|
+
// was given, cannot run a diagnostic, and cannot record a finding — the three things investigating
|
|
6168
|
+
// IS. It would then exit 0 having written nothing, which the supervisor reads as a delivered
|
|
6169
|
+
// judgement and un-routes, so the next sweep hands the same ticket to another worker that learns the
|
|
6170
|
+
// same nothing. A loop that reports success is worse than a refusal.
|
|
6171
|
+
//
|
|
6172
|
+
// What enforces the read-only phase instead: the brief's Boundaries section, which names the lease
|
|
6173
|
+
// holder, says which commands not to run, and explains that the shared CHECKOUT is the thing at
|
|
6174
|
+
// risk. Enforcement moved from the sandbox to the instructions deliberately — the sandbox could not
|
|
6175
|
+
// express "may talk to the platform, may not write this directory".
|
|
6077
6176
|
build: ({ cwd, mode, lastMessageFile }) => ({
|
|
6078
6177
|
args: [
|
|
6079
6178
|
'exec',
|
|
6080
6179
|
'--cd', cwd,
|
|
6081
6180
|
'--skip-git-repo-check',
|
|
6082
|
-
'--sandbox',
|
|
6181
|
+
'--sandbox', 'workspace-write',
|
|
6182
|
+
'-c', 'sandbox_workspace_write.network_access=true',
|
|
6083
6183
|
'--json',
|
|
6084
6184
|
...(lastMessageFile ? ['--output-last-message', lastMessageFile] : []),
|
|
6085
6185
|
'-'
|
|
6086
|
-
]
|
|
6186
|
+
],
|
|
6187
|
+
// Reported on the worker record so an operator can tell a phase the platform derived from a
|
|
6188
|
+
// sandbox the launcher applied — they are no longer the same thing for codex.
|
|
6189
|
+
sandbox: 'workspace-write' + (mode === 'investigate' ? ' (investigate: boundary is the brief)' : '')
|
|
6087
6190
|
})
|
|
6088
6191
|
},
|
|
6089
6192
|
claude: {
|
|
6090
6193
|
binary: 'claude',
|
|
6194
|
+
// Same requirement, different mechanism: in `-p` there is no one to answer a permission prompt, so
|
|
6195
|
+
// an un-allowlisted Bash call is DENIED rather than queued. Without this the worker cannot run
|
|
6196
|
+
// remits-cli at all. `plan` mode is avoided for the same reason `read-only` is avoided above.
|
|
6091
6197
|
build: ({ cwd, mode }) => ({
|
|
6092
6198
|
args: [
|
|
6093
6199
|
'-p',
|
|
6094
6200
|
'--add-dir', cwd,
|
|
6095
6201
|
'--output-format', 'stream-json',
|
|
6096
6202
|
'--verbose',
|
|
6097
|
-
'--permission-mode',
|
|
6098
|
-
|
|
6203
|
+
'--permission-mode', 'acceptEdits',
|
|
6204
|
+
'--allowedTools', 'Bash,Read,Grep,Glob,WebFetch' + (mode === 'investigate' ? '' : ',Edit,Write')
|
|
6205
|
+
],
|
|
6206
|
+
sandbox: 'acceptEdits' + (mode === 'investigate' ? ' (investigate: no Edit/Write tool)' : '')
|
|
6099
6207
|
})
|
|
6100
6208
|
},
|
|
6101
6209
|
gemini: {
|
|
@@ -6107,7 +6215,8 @@ const AGENT_LAUNCHERS = {
|
|
|
6107
6215
|
'-p', 'Work the support ticket described above, following its instructions exactly.',
|
|
6108
6216
|
'--approval-mode', mode === 'investigate' ? 'plan' : 'auto_edit',
|
|
6109
6217
|
'--output-format', 'stream-json'
|
|
6110
|
-
]
|
|
6218
|
+
],
|
|
6219
|
+
sandbox: mode === 'investigate' ? 'plan' : 'auto_edit'
|
|
6111
6220
|
})
|
|
6112
6221
|
}
|
|
6113
6222
|
};
|
|
@@ -6179,7 +6288,31 @@ function resolveWorkerAgent(value, anchor = null) {
|
|
|
6179
6288
|
};
|
|
6180
6289
|
}
|
|
6181
6290
|
|
|
6182
|
-
/**
|
|
6291
|
+
/**
|
|
6292
|
+
* Is this directory a real checkout, rather than something that merely contains an account-info.json?
|
|
6293
|
+
*
|
|
6294
|
+
* <p>The index is built by scanning for `account-info.json`, and that file is also in every zip export
|
|
6295
|
+
* of an account. An unpacked export in `~/Downloads` therefore looks exactly like a checkout, and it
|
|
6296
|
+
* exists — which was the whole of the previous validation.</p>
|
|
6297
|
+
*/
|
|
6298
|
+
function isAccountCheckout(directory) {
|
|
6299
|
+
try {
|
|
6300
|
+
return !!directory && fs.existsSync(path.join(directory, '.git'));
|
|
6301
|
+
} catch (_) {
|
|
6302
|
+
return false;
|
|
6303
|
+
}
|
|
6304
|
+
}
|
|
6305
|
+
|
|
6306
|
+
/**
|
|
6307
|
+
* Where the repository for an account is checked out on THIS machine, or null.
|
|
6308
|
+
*
|
|
6309
|
+
* <p>Null when the entry is not a git checkout, and that is the load-bearing part. The caller's next
|
|
6310
|
+
* step on null is the brief telling the worker "no local checkout was resolved — do not guess", which
|
|
6311
|
+
* is the correct outcome; the previous `fs.existsSync` test let an unpacked zip export in `~/Downloads`
|
|
6312
|
+
* satisfy it instead, so the guard never fired and a worker edited, staged and reported success against
|
|
6313
|
+
* a directory with no remote and no history. Nothing about that run looked wrong. Measured on this
|
|
6314
|
+
* machine: account 11 (11 open tickets) and account 2 both resolved that way.</p>
|
|
6315
|
+
*/
|
|
6183
6316
|
function repoDirectoryForAccount(accountId) {
|
|
6184
6317
|
const wanted = Number(accountId);
|
|
6185
6318
|
if (!Number.isFinite(wanted) || wanted <= 0) {
|
|
@@ -6187,12 +6320,48 @@ function repoDirectoryForAccount(accountId) {
|
|
|
6187
6320
|
}
|
|
6188
6321
|
const entry = listAccountRepoEntries(readAccountRepoIndex())
|
|
6189
6322
|
.find((candidate) => Number(candidate.accountId) === wanted);
|
|
6190
|
-
if (!entry || !entry.directory || !
|
|
6323
|
+
if (!entry || !entry.directory || !isAccountCheckout(entry.directory)) {
|
|
6191
6324
|
return null;
|
|
6192
6325
|
}
|
|
6193
6326
|
return entry.directory;
|
|
6194
6327
|
}
|
|
6195
6328
|
|
|
6329
|
+
/**
|
|
6330
|
+
* Everything wrong with the local account-repo index, as sentences an operator can act on.
|
|
6331
|
+
*
|
|
6332
|
+
* <p>Reported at `agent serve` rather than discovered one ticket at a time: a supervisor about to work a
|
|
6333
|
+
* production queue should be told up front which accounts it cannot work, because the alternative is
|
|
6334
|
+
* finding out per ticket, hours apart, from a brief that says only "no checkout resolved".</p>
|
|
6335
|
+
*/
|
|
6336
|
+
function accountRepoIndexProblems(ownCwd = null) {
|
|
6337
|
+
const problems = [];
|
|
6338
|
+
// The account this session is standing in resolves from its OWN directory, not from the shared index,
|
|
6339
|
+
// so an ambiguity there is not a problem for this supervisor — it is two worktrees working as intended.
|
|
6340
|
+
// Reporting it anyway trains an operator to ignore the warning, and the warning is load-bearing for
|
|
6341
|
+
// every OTHER account, where a silent wrong winner is unrecoverable.
|
|
6342
|
+
const own = ownCwd ? accountCheckoutIdentity(ownCwd) : null;
|
|
6343
|
+
for (const entry of listAccountRepoEntries(readAccountRepoIndex())) {
|
|
6344
|
+
const label = 'account ' + entry.accountId + ' (' + (entry.name || 'unnamed') + ')';
|
|
6345
|
+
if (!fs.existsSync(entry.directory)) {
|
|
6346
|
+
problems.push({ accountId: Number(entry.accountId), severity: 'unusable',
|
|
6347
|
+
message: label + ': the recorded directory no longer exists — ' + entry.directory });
|
|
6348
|
+
} else if (!isAccountCheckout(entry.directory)) {
|
|
6349
|
+
problems.push({ accountId: Number(entry.accountId), severity: 'unusable',
|
|
6350
|
+
message: label + ': ' + entry.directory + ' is not a git checkout (an unpacked export?). ' +
|
|
6351
|
+
'Tickets for this account will park instead of being worked.' });
|
|
6352
|
+
}
|
|
6353
|
+
if (Array.isArray(entry.ambiguousWith) && entry.ambiguousWith.length &&
|
|
6354
|
+
!(own && own.accountId === Number(entry.accountId))) {
|
|
6355
|
+
problems.push({ accountId: Number(entry.accountId), severity: 'ambiguous',
|
|
6356
|
+
message: label + ': more than one checkout claims this account. Using ' + entry.directory +
|
|
6357
|
+
'; also found ' + entry.ambiguousWith.join(', ') +
|
|
6358
|
+
'. Set the right one with `remits-cli agent repos --account-id ' + entry.accountId +
|
|
6359
|
+
' --directory <path>`.' });
|
|
6360
|
+
}
|
|
6361
|
+
}
|
|
6362
|
+
return problems;
|
|
6363
|
+
}
|
|
6364
|
+
|
|
6196
6365
|
/**
|
|
6197
6366
|
* The working directory a ticket should be worked in.
|
|
6198
6367
|
*
|
|
@@ -6201,13 +6370,27 @@ function repoDirectoryForAccount(accountId) {
|
|
|
6201
6370
|
* neither is checked out here the answer is null, and the brief says so explicitly instead of
|
|
6202
6371
|
* letting the worker start in whatever directory the supervisor happened to inherit.
|
|
6203
6372
|
*/
|
|
6204
|
-
function resolveTicketWorkingDirectory(ticket) {
|
|
6373
|
+
function resolveTicketWorkingDirectory(ticket, ownCwd = null) {
|
|
6205
6374
|
const facts = (ticket && ticket.briefFacts) || {};
|
|
6206
6375
|
const candidates = [
|
|
6207
6376
|
facts.repoAccountId,
|
|
6208
6377
|
ticket && ticket.implementationAccountId,
|
|
6209
6378
|
ticket && ticket.accountId
|
|
6210
|
-
];
|
|
6379
|
+
].map((value) => Number(value)).filter((value) => Number.isFinite(value) && value > 0);
|
|
6380
|
+
|
|
6381
|
+
// THIS supervisor's own checkout wins, when it is a checkout of one of the candidate accounts.
|
|
6382
|
+
//
|
|
6383
|
+
// The account-repo index is a single global map keyed by accountId, and `agent serve` writes its own
|
|
6384
|
+
// cwd into it — so two supervisors serving two git WORKTREES of one account fight over one entry, and
|
|
6385
|
+
// the second to start silently repoints the first's future workers into its own tree. That is the
|
|
6386
|
+
// shape worktrees exist to prevent: a supervisor must launch its workers where IT is, not where the
|
|
6387
|
+
// machine's last-registered session happened to be. The index stays the answer for every account this
|
|
6388
|
+
// session is not standing in, which is the normal case.
|
|
6389
|
+
const own = ownCwd ? accountCheckoutIdentity(ownCwd) : null;
|
|
6390
|
+
if (own && candidates.includes(own.accountId)) {
|
|
6391
|
+
return own.directory;
|
|
6392
|
+
}
|
|
6393
|
+
|
|
6211
6394
|
for (const candidate of candidates) {
|
|
6212
6395
|
const dir = repoDirectoryForAccount(candidate);
|
|
6213
6396
|
if (dir) {
|
|
@@ -6217,6 +6400,30 @@ function resolveTicketWorkingDirectory(ticket) {
|
|
|
6217
6400
|
return null;
|
|
6218
6401
|
}
|
|
6219
6402
|
|
|
6403
|
+
/**
|
|
6404
|
+
* Which account this directory is a checkout OF, read from the directory itself.
|
|
6405
|
+
*
|
|
6406
|
+
* Deliberately not a lookup in the shared index: the whole point is to answer for one process without
|
|
6407
|
+
* consulting a map another process can overwrite. Null when the directory is not a real checkout — an
|
|
6408
|
+
* unpacked zip export carries `account-info.json` too, which is exactly the trap `isAccountCheckout`
|
|
6409
|
+
* exists to close.
|
|
6410
|
+
*/
|
|
6411
|
+
function accountCheckoutIdentity(directory) {
|
|
6412
|
+
try {
|
|
6413
|
+
if (!directory || !isAccountCheckout(directory)) {
|
|
6414
|
+
return null;
|
|
6415
|
+
}
|
|
6416
|
+
const info = loadAccountInfo(directory);
|
|
6417
|
+
const accountId = Number(info && accountIdFromAccountInfo(info));
|
|
6418
|
+
if (!Number.isFinite(accountId) || accountId <= 0) {
|
|
6419
|
+
return null;
|
|
6420
|
+
}
|
|
6421
|
+
return { accountId, directory: path.resolve(directory) };
|
|
6422
|
+
} catch (_) {
|
|
6423
|
+
return null;
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
|
|
6220
6427
|
/**
|
|
6221
6428
|
* Turn a worker's streamed output into something an operator can read in one line.
|
|
6222
6429
|
*
|
|
@@ -6665,6 +6872,35 @@ async function agentServeCommand(flags) {
|
|
|
6665
6872
|
const mode = String(flags.mode || (flagEnabled(flags['investigate-only']) ? 'investigate' : 'edit'))
|
|
6666
6873
|
.toLowerCase() === 'investigate' ? 'investigate' : 'edit';
|
|
6667
6874
|
|
|
6875
|
+
// Say what this supervisor CANNOT work, before it starts rather than one ticket at a time. A worker
|
|
6876
|
+
// pointed at an account with no usable checkout parks correctly, but the operator only learns that
|
|
6877
|
+
// from a brief they may never read, hours later, on one ticket. `--strict-repos` turns it into a
|
|
6878
|
+
// refusal for anyone who would rather not start a production run with known holes.
|
|
6879
|
+
try {
|
|
6880
|
+
updateAccountRepoIndex(process.cwd());
|
|
6881
|
+
discoverAccountRepos();
|
|
6882
|
+
} catch (_) { /* best-effort; the checks below still report what the index does say */ }
|
|
6883
|
+
const problems = accountRepoIndexProblems(process.cwd());
|
|
6884
|
+
if (problems.length) {
|
|
6885
|
+
console.log('');
|
|
6886
|
+
console.log('Account repository index — ' + problems.length + ' issue(s) to know about before serving:');
|
|
6887
|
+
for (const problem of problems) {
|
|
6888
|
+
console.log(' [' + problem.severity + '] ' + problem.message);
|
|
6889
|
+
}
|
|
6890
|
+
console.log(' Tickets for an "unusable" account are parked with an explanation, never worked in the');
|
|
6891
|
+
console.log(' wrong directory. Fix with: remits-cli agent repos --account-id <id> --directory <path>');
|
|
6892
|
+
const ownHere = accountCheckoutIdentity(process.cwd());
|
|
6893
|
+
if (ownHere) {
|
|
6894
|
+
console.log(' Account ' + ownHere.accountId + ' is exempt: this session serves it from its own directory');
|
|
6895
|
+
console.log(' (' + ownHere.directory + '), so a second worktree elsewhere cannot redirect these workers.');
|
|
6896
|
+
}
|
|
6897
|
+
console.log('');
|
|
6898
|
+
if (flagEnabled(flags['strict-repos'])) {
|
|
6899
|
+
throw new Error('Refusing to serve with ' + problems.length +
|
|
6900
|
+
' account-repository issue(s). Resolve them, or drop --strict-repos to serve anyway.');
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
|
|
6668
6904
|
return agentRegisterCommand(flags, {
|
|
6669
6905
|
serve: {
|
|
6670
6906
|
workerAgent,
|
|
@@ -6876,6 +7112,54 @@ function clearWorkerAttempts(agentId, ticketId) {
|
|
|
6876
7112
|
}
|
|
6877
7113
|
}
|
|
6878
7114
|
|
|
7115
|
+
/**
|
|
7116
|
+
* The worker's closing summary, recorded on the ticket by the SUPERVISOR.
|
|
7117
|
+
*
|
|
7118
|
+
* <p>A worker is supposed to record what it established itself, with `remits-cli ticket progress`. Most
|
|
7119
|
+
* do. The ones that do not are exactly the runs worth hearing from: a worker that hit its context limit,
|
|
7120
|
+
* that crashed after investigating, or that could not reach the platform at all. Their conclusion was
|
|
7121
|
+
* captured to disk the whole time (`codex exec --output-last-message`) and thrown away, so a ticket came
|
|
7122
|
+
* back to the queue with nothing on it and the next worker rediscovered the same thing.</p>
|
|
7123
|
+
*
|
|
7124
|
+
* <p>Posted only when the run added no worklog entry of its own, so a worker that reported properly is
|
|
7125
|
+
* not duplicated. Marked `worker_summary` rather than `investigation` so a reader can tell a finding the
|
|
7126
|
+
* worker chose to record from a transcript tail the supervisor salvaged.</p>
|
|
7127
|
+
*/
|
|
7128
|
+
async function postWorkerSummary(context, agentId, ticket, summaryFile, options = {}) {
|
|
7129
|
+
let text = '';
|
|
7130
|
+
try {
|
|
7131
|
+
text = fs.readFileSync(summaryFile, 'utf8').trim();
|
|
7132
|
+
} catch (_) {
|
|
7133
|
+
return false; // codex only writes it on a clean finish, and the other launchers never do.
|
|
7134
|
+
}
|
|
7135
|
+
if (!text) {
|
|
7136
|
+
return false;
|
|
7137
|
+
}
|
|
7138
|
+
// The worklog is read by people and by the next worker's brief; a whole transcript would bury both.
|
|
7139
|
+
const MAX = 4000;
|
|
7140
|
+
const body = text.length > MAX ? (text.slice(0, MAX) + '\n\n…truncated; full run: ' + options.logFile) : text;
|
|
7141
|
+
try {
|
|
7142
|
+
await buildAxios(context.baseUrl, context.session.token, 20000)
|
|
7143
|
+
.post('/cli/ticket', {
|
|
7144
|
+
action: 'progress',
|
|
7145
|
+
ticketId: ticket.ticketId || ticket.id,
|
|
7146
|
+
accountId: Number(ticket.accountId) || undefined,
|
|
7147
|
+
dataMode: context.dataMode,
|
|
7148
|
+
agentId,
|
|
7149
|
+
category: 'worker_summary',
|
|
7150
|
+
summary: 'Autonomous worker exited without recording a finding; this is its closing summary.',
|
|
7151
|
+
details: body,
|
|
7152
|
+
nextStep: options.nextStep || null
|
|
7153
|
+
});
|
|
7154
|
+
return true;
|
|
7155
|
+
} catch (err) {
|
|
7156
|
+
appendGlobalActivityLog('agent', 'worker.summary_post_failed', {
|
|
7157
|
+
agentId, ticketId: ticket.ticketId || ticket.id, error: describeError(err)
|
|
7158
|
+
}, 'error');
|
|
7159
|
+
return false;
|
|
7160
|
+
}
|
|
7161
|
+
}
|
|
7162
|
+
|
|
6879
7163
|
/** Drop a claim on the platform. Best-effort: the claim expires on its own if this never lands. */
|
|
6880
7164
|
async function releaseTicketClaim(context, agentId, ticket, options = {}) {
|
|
6881
7165
|
try {
|
|
@@ -6928,7 +7212,7 @@ function spawnTicketWorker(context, ctx, ticket) {
|
|
|
6928
7212
|
return { pid: null, done: Promise.resolve({ ticketId, ok: false, reason: 'unsupported_agent' }) };
|
|
6929
7213
|
}
|
|
6930
7214
|
|
|
6931
|
-
const workingDirectory = resolveTicketWorkingDirectory(ticket);
|
|
7215
|
+
const workingDirectory = resolveTicketWorkingDirectory(ticket, ctx && ctx.cwd);
|
|
6932
7216
|
// The brief is rendered server-side without the local path (the platform cannot know it), so the
|
|
6933
7217
|
// resolved directory is appended here. When nothing resolved, the server-rendered brief already
|
|
6934
7218
|
// tells the worker to find it rather than guess — do not paper over that with a default.
|
|
@@ -6999,6 +7283,11 @@ function spawnTicketWorker(context, ctx, ticket) {
|
|
|
6999
7283
|
workspace: workerWorkspace,
|
|
7000
7284
|
attempt,
|
|
7001
7285
|
agentType,
|
|
7286
|
+
// The sandbox the launcher actually applied. Kept beside the phase because for codex they are no
|
|
7287
|
+
// longer the same fact: an investigating worker still runs workspace-write (it needs the network to
|
|
7288
|
+
// report at all), and its boundary is the brief. An operator reading "investigating" must be able to
|
|
7289
|
+
// see that.
|
|
7290
|
+
sandbox: built.sandbox || null,
|
|
7002
7291
|
startedAt: new Date().toISOString(),
|
|
7003
7292
|
workingDirectory: workingDirectory || null,
|
|
7004
7293
|
accountId: ticket.accountId || null,
|
|
@@ -7081,6 +7370,22 @@ function spawnTicketWorker(context, ctx, ticket) {
|
|
|
7081
7370
|
});
|
|
7082
7371
|
|
|
7083
7372
|
const row = (result && result.ticket) || {};
|
|
7373
|
+
|
|
7374
|
+
// Did this run leave anything on the record? If not, salvage its closing summary before the ticket
|
|
7375
|
+
// goes back to the queue — otherwise the next worker starts from exactly where this one did.
|
|
7376
|
+
// Compared against the count we were HANDED at spawn, so a worker that reported properly (or that
|
|
7377
|
+
// asked a question, or completed) is never duplicated.
|
|
7378
|
+
const worklogBefore = Number(ticket.worklogCount || 0);
|
|
7379
|
+
const worklogAfter = Number(row.worklogCount != null ? row.worklogCount : worklogBefore);
|
|
7380
|
+
if (worklogAfter <= worklogBefore && !['resolved', 'closed'].includes(String(row.status || ''))) {
|
|
7381
|
+
await postWorkerSummary(context, agentId, { ticketId, accountId: ticket.accountId }, lastMessageFile, {
|
|
7382
|
+
logFile,
|
|
7383
|
+
nextStep: giveUp
|
|
7384
|
+
? 'This worker failed ' + attempt + ' time(s) and the ticket was returned to the queue.'
|
|
7385
|
+
: null
|
|
7386
|
+
});
|
|
7387
|
+
}
|
|
7388
|
+
|
|
7084
7389
|
const terminal = ['resolved', 'closed'].includes(String(row.status || ''));
|
|
7085
7390
|
if (terminal || giveUp) {
|
|
7086
7391
|
clearWorkerAttempts(agentId, ticketId);
|
|
@@ -7397,6 +7702,21 @@ async function agentWorkCommand(flags) {
|
|
|
7397
7702
|
*/
|
|
7398
7703
|
async function ticketCommand(flags, subcommand) {
|
|
7399
7704
|
const action = String(subcommand || 'read').toLowerCase();
|
|
7705
|
+
|
|
7706
|
+
// `queue` and `create` are the two verbs with no ticket to act on, so they are dispatched before the
|
|
7707
|
+
// id requirement rather than being forced to invent one.
|
|
7708
|
+
if (action === 'queue' || action === 'list' || action === 'ls') {
|
|
7709
|
+
return ticketQueueCommand(flags);
|
|
7710
|
+
}
|
|
7711
|
+
if (action === 'create' || action === 'new' || action === 'raise') {
|
|
7712
|
+
return ticketCreateCommand(flags);
|
|
7713
|
+
}
|
|
7714
|
+
// Bulk reclaim scans a queue rather than acting on one id, so it is dispatched here too. With
|
|
7715
|
+
// --ticket it falls through to the single-ticket path below like any other verb.
|
|
7716
|
+
if (action === 'reclaim' && !flags.ticket && !flags['ticket-id']) {
|
|
7717
|
+
return ticketReclaimSweepCommand(flags);
|
|
7718
|
+
}
|
|
7719
|
+
|
|
7400
7720
|
const ticketId = String(flags.ticket || flags['ticket-id'] || flags._?.[2] || process.env.REMITS_SUPPORT_TICKET_ID || '').trim();
|
|
7401
7721
|
if (!ticketId) {
|
|
7402
7722
|
printTicketHelp();
|
|
@@ -7441,7 +7761,9 @@ async function ticketCommand(flags, subcommand) {
|
|
|
7441
7761
|
summary: flags.summary || flags.note,
|
|
7442
7762
|
question: flags.question,
|
|
7443
7763
|
context: flags.context,
|
|
7444
|
-
to
|
|
7764
|
+
// Recipients are a list on the platform side (message.to / deliver.to). A single value still works,
|
|
7765
|
+
// so `--to a@x.test` and `--to a@x.test,b@x.test` both do what they look like they do.
|
|
7766
|
+
to: flags.to ? String(flags.to).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
7445
7767
|
message: flags.message || flags.answer,
|
|
7446
7768
|
route: flags.route === 'false' ? false : undefined,
|
|
7447
7769
|
details: flags.details,
|
|
@@ -7457,6 +7779,26 @@ async function ticketCommand(flags, subcommand) {
|
|
|
7457
7779
|
size: planningFlagValue(flags.size),
|
|
7458
7780
|
blockedBy: planningFlagValue(firstDefined(flags['blocked-by'], flags.blockedBy)),
|
|
7459
7781
|
workstream: planningFlagValue(flags.workstream),
|
|
7782
|
+
tags: flags.tags ? String(flags.tags).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
7783
|
+
label: flags.label,
|
|
7784
|
+
url: flags.url,
|
|
7785
|
+
body: flags.body,
|
|
7786
|
+
role: flags.role,
|
|
7787
|
+
email: flags.email,
|
|
7788
|
+
key: flags.key,
|
|
7789
|
+
value: flags.value,
|
|
7790
|
+
// Conversation and outbox. `--message` is shared with `answer`/`note`, which is deliberate: it is
|
|
7791
|
+
// always "the text", and the ACTION decides who ends up reading it.
|
|
7792
|
+
channel: flags.channel,
|
|
7793
|
+
subject: flags.subject,
|
|
7794
|
+
visibility: flags.visibility,
|
|
7795
|
+
direction: flags.direction,
|
|
7796
|
+
template: flags.template,
|
|
7797
|
+
cc: flags.cc ? String(flags.cc).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
7798
|
+
deliveryId: flags['delivery-id'] || flags.deliveryId || undefined,
|
|
7799
|
+
idempotencyKey: flags['idempotency-key'] || flags.idempotencyKey || undefined,
|
|
7800
|
+
providerMessageId: flags['provider-message-id'] || undefined,
|
|
7801
|
+
reason: flags.reason,
|
|
7460
7802
|
// Lease operations act as the AGENT, not the user: the lease is held by a worker session, and a
|
|
7461
7803
|
// human's email is not a thing that can hold one. Inside a worker these come from the environment
|
|
7462
7804
|
// the supervisor set, so the commands need no flags.
|
|
@@ -7495,7 +7837,7 @@ async function ticketCommand(flags, subcommand) {
|
|
|
7495
7837
|
return;
|
|
7496
7838
|
}
|
|
7497
7839
|
|
|
7498
|
-
if (flagEnabled(flags.json) || action === 'read') {
|
|
7840
|
+
if (flagEnabled(flags.json) || action === 'read' || action === 'deliveries') {
|
|
7499
7841
|
console.log(JSON.stringify(response, null, 2));
|
|
7500
7842
|
return;
|
|
7501
7843
|
}
|
|
@@ -7537,6 +7879,19 @@ async function ticketCommand(flags, subcommand) {
|
|
|
7537
7879
|
const ticket = (response && response.ticket) || {};
|
|
7538
7880
|
console.log(action + ' ok — ticket ' + ticketId + ' is now [' + (ticket.status || '?') + ']' +
|
|
7539
7881
|
(ticket.assignedTo ? ', owned by ' + ticket.assignedTo : ', unassigned'));
|
|
7882
|
+
// Say what was actually written. Without this, `message`, `note`, `deliver` and `participant` all
|
|
7883
|
+
// printed the same status line, so the only feedback distinguishing "told the customer" from "left an
|
|
7884
|
+
// internal note" was the word the caller had just typed.
|
|
7885
|
+
if (action === 'message') {
|
|
7886
|
+
console.log(' public message added (' + (ticket.messageCount || '?') + ' on the ticket) — visible to the requester.');
|
|
7887
|
+
console.log(' This did NOT send anything. To have it delivered: remits-cli ticket deliver --channel email --to <who>');
|
|
7888
|
+
}
|
|
7889
|
+
if (action === 'note') console.log(' internal note added — the requester does not see it.');
|
|
7890
|
+
if (action === 'participant') console.log(' participants: ' + (ticket.participantCount || '?'));
|
|
7891
|
+
if (action === 'deliver') {
|
|
7892
|
+
console.log(' delivery queued (' + (ticket.deliveryCount || '?') + ' on the ticket). An account-owned rule or');
|
|
7893
|
+
console.log(' scheduled Action sends it. Check with: remits-cli ticket deliveries --ticket ' + ticketId);
|
|
7894
|
+
}
|
|
7540
7895
|
if (action === 'planning' || action === 'plan') {
|
|
7541
7896
|
const planning = [
|
|
7542
7897
|
ticket.workstream ? 'workstream=' + ticket.workstream : '',
|
|
@@ -7557,6 +7912,229 @@ async function ticketCommand(flags, subcommand) {
|
|
|
7557
7912
|
}
|
|
7558
7913
|
}
|
|
7559
7914
|
|
|
7915
|
+
/**
|
|
7916
|
+
* The ticket queue — what else is open, and what it looks like as a whole.
|
|
7917
|
+
*
|
|
7918
|
+
* <p><b>Why this had to exist.</b> The worker brief is emphatic that `remits-cli ticket` is the surface to
|
|
7919
|
+
* use, and that an account's own ticket tooling must not be reached for: it belongs to one account, may not
|
|
7920
|
+
* exist on this platform, and refuses tickets other accounts own. That advice was right and the surface was
|
|
7921
|
+
* incomplete — it could act on ONE ticket whose id it was handed, and could not see anything else. So a
|
|
7922
|
+
* worker could not tell that the ticket it was given is one of eleven instances of the same fix, could not
|
|
7923
|
+
* check whether the defect it just found is already filed, and could not triage at all. The server side
|
|
7924
|
+
* already existed in full (`/cli/tickets`, the same `queue(...)` the operator UI reads); its only consumer
|
|
7925
|
+
* was the local dashboard.</p>
|
|
7926
|
+
*/
|
|
7927
|
+
async function ticketQueueCommand(flags) {
|
|
7928
|
+
const local = listLocalAgents()[0] || null;
|
|
7929
|
+
const context = agentSessionFor(flags, local);
|
|
7930
|
+
const accountId = Number(flags['account-id'] || flags.accountId || context.accountId);
|
|
7931
|
+
if (!Number.isFinite(accountId) || accountId <= 0) {
|
|
7932
|
+
throw new Error('ticket queue needs --account-id (or run it from an account repo).');
|
|
7933
|
+
}
|
|
7934
|
+
|
|
7935
|
+
const csv = (value) => (value ? String(value).split(',').map((v) => v.trim()).filter(Boolean) : undefined);
|
|
7936
|
+
const response = await buildAxios(context.baseUrl, context.session.token, 30000)
|
|
7937
|
+
.post('/cli/tickets', {
|
|
7938
|
+
accountId,
|
|
7939
|
+
dataMode: context.dataMode,
|
|
7940
|
+
statuses: csv(flags.status || flags.statuses),
|
|
7941
|
+
priorities: csv(flags.priority || flags.priorities),
|
|
7942
|
+
types: csv(flags.type || flags.types),
|
|
7943
|
+
sources: csv(flags.source || flags.sources),
|
|
7944
|
+
workstream: flags.workstream,
|
|
7945
|
+
assignedTo: flags['assigned-to'] || flags.assignedTo,
|
|
7946
|
+
routedAgentId: flags['routed-to'] || flags.routedAgentId,
|
|
7947
|
+
unrouted: flagEnabled(flags.unrouted) || undefined,
|
|
7948
|
+
unassigned: flagEnabled(flags.unassigned) || undefined,
|
|
7949
|
+
// "Parked on a human" cannot be asked for with a status filter: `ask` parks a ticket in
|
|
7950
|
+
// pending_review, which also means "done, review me".
|
|
7951
|
+
awaitingResponse: flagEnabled(flags['awaiting-response'] || flags.awaitingResponse) || undefined,
|
|
7952
|
+
plannedIn: flags['planned-in'] || flags.plannedIn,
|
|
7953
|
+
boardStage: flags['board-stage'] || flags.boardStage,
|
|
7954
|
+
blockedBy: flags['blocked-by'] || flags.blockedBy,
|
|
7955
|
+
search: flags.search || flags.q,
|
|
7956
|
+
sortBy: flags['sort-by'] || flags.sortBy,
|
|
7957
|
+
sortDirection: flags['sort-direction'] || flags.sortDirection,
|
|
7958
|
+
includeChildren: flags['include-children'] === 'false' ? false : undefined,
|
|
7959
|
+
limit: parsePositiveInt(flags.limit, 25)
|
|
7960
|
+
}).then((r) => r.data);
|
|
7961
|
+
|
|
7962
|
+
if (flagEnabled(flags.json)) {
|
|
7963
|
+
console.log(JSON.stringify(response, null, 2));
|
|
7964
|
+
return;
|
|
7965
|
+
}
|
|
7966
|
+
|
|
7967
|
+
const tickets = (response && response.tickets) || [];
|
|
7968
|
+
const counts = (response && response.counts) || {};
|
|
7969
|
+
if (!tickets.length) {
|
|
7970
|
+
console.log('No tickets match. (' + (response && response.dataMode) + ' lane, account ' + accountId + ')');
|
|
7971
|
+
return;
|
|
7972
|
+
}
|
|
7973
|
+
console.log(String(response.totalCount != null ? response.totalCount : tickets.length) +
|
|
7974
|
+
' ticket(s) — ' + (counts.unrouted || 0) + ' unrouted, ' + (counts.unassigned || 0) + ' unassigned' +
|
|
7975
|
+
', ' + (counts.awaitingResponse || 0) + ' waiting on a human' +
|
|
7976
|
+
' [' + (response.dataMode || '?') + ' lane]');
|
|
7977
|
+
console.log('');
|
|
7978
|
+
// Board position is printed only when some ticket in this queue actually has one. An always-on column
|
|
7979
|
+
// would be blank for every account that does not run a board, and a blank column reads as missing data.
|
|
7980
|
+
const showsBoard = tickets.some((t) => t.boardStage || t.plannedIn || t.blockedBy);
|
|
7981
|
+
for (const t of tickets) {
|
|
7982
|
+
// The repo account, not just the owning account: it is what the edit lease excludes on, so it is
|
|
7983
|
+
// what tells a reader which of these can be worked at the same time.
|
|
7984
|
+
const repo = t.implementationAccountId || t.accountId;
|
|
7985
|
+
const who = t.routedAgentLabel || t.routedAgentId || t.assignedTo || '—';
|
|
7986
|
+
console.log(' ' + String(t.ticketId || t.id).padStart(6) + ' ' +
|
|
7987
|
+
String(t.status || '').padEnd(14) + String(t.priority || '').padEnd(9) +
|
|
7988
|
+
'repo ' + String(repo || '?').padEnd(6) +
|
|
7989
|
+
String(who).slice(0, 18).padEnd(20) + String(t.subject || '').slice(0, 58));
|
|
7990
|
+
// A second line rather than more columns: these are the account's own labels, of unbounded length,
|
|
7991
|
+
// and truncating `awaiting_customer_confirmation` to fit a column makes it unrecognisable.
|
|
7992
|
+
const marks = [
|
|
7993
|
+
t.awaitingResponseSince ? 'WAITING ON A HUMAN' : '',
|
|
7994
|
+
showsBoard && t.boardStage ? 'stage ' + t.boardStage : '',
|
|
7995
|
+
showsBoard && t.plannedIn ? 'in ' + t.plannedIn : '',
|
|
7996
|
+
showsBoard && t.blockedBy ? 'BLOCKED BY ' + t.blockedBy : ''
|
|
7997
|
+
].filter(Boolean);
|
|
7998
|
+
if (marks.length) console.log(' '.repeat(10) + marks.join(' '));
|
|
7999
|
+
}
|
|
8000
|
+
if (response.totalCount > tickets.length) {
|
|
8001
|
+
console.log('');
|
|
8002
|
+
console.log(' showing ' + tickets.length + ' of ' + response.totalCount + ' — raise --limit to see more');
|
|
8003
|
+
}
|
|
8004
|
+
}
|
|
8005
|
+
|
|
8006
|
+
/**
|
|
8007
|
+
* Find tickets an agent took and abandoned, and hand them back to the queue.
|
|
8008
|
+
*
|
|
8009
|
+
* <p><b>Dry by default.</b> This takes work away from a holder on a judgement about staleness, across a
|
|
8010
|
+
* whole account tree, in one command — so the default is to show what it WOULD do. `--apply` is the
|
|
8011
|
+
* operator saying yes. The platform re-checks every row against the document before acting, so a stale
|
|
8012
|
+
* listing cannot cause a wrong reclaim even if this list is minutes old.</p>
|
|
8013
|
+
*/
|
|
8014
|
+
async function ticketReclaimSweepCommand(flags) {
|
|
8015
|
+
const local = listLocalAgents()[0] || null;
|
|
8016
|
+
const context = agentSessionFor(flags, local);
|
|
8017
|
+
const accountId = Number(flags['account-id'] || flags.accountId || context.accountId);
|
|
8018
|
+
if (!Number.isFinite(accountId) || accountId <= 0) {
|
|
8019
|
+
throw new Error('ticket reclaim needs --account-id (or --ticket ID for a single ticket).');
|
|
8020
|
+
}
|
|
8021
|
+
const staleHours = parsePositiveInt(flags['stale-hours'] || flags.staleHours, 24);
|
|
8022
|
+
const apply = flagEnabled(flags.apply);
|
|
8023
|
+
|
|
8024
|
+
const queue = await buildAxios(context.baseUrl, context.session.token, 30000)
|
|
8025
|
+
.post('/cli/tickets', {
|
|
8026
|
+
accountId, dataMode: context.dataMode,
|
|
8027
|
+
statuses: ['accepted', 'in_progress'], limit: parsePositiveInt(flags.limit, 200)
|
|
8028
|
+
}).then((r) => r.data);
|
|
8029
|
+
|
|
8030
|
+
const cutoff = Date.now() - staleHours * 3600000;
|
|
8031
|
+
// A local pre-filter only, to keep the output short and avoid asking about obviously-live rows. The
|
|
8032
|
+
// platform owns the actual decision and will refuse anything that does not qualify.
|
|
8033
|
+
const candidates = ((queue && queue.tickets) || []).filter((t) => {
|
|
8034
|
+
const holder = t.assignedTo || t.routedAgentId;
|
|
8035
|
+
if (!holder || String(holder).includes('@')) return false;
|
|
8036
|
+
if (t.awaitingResponseSince) return false;
|
|
8037
|
+
const touched = Date.parse(t.updatedAt || t.claimedAt || t.assignedAt || '');
|
|
8038
|
+
return Number.isFinite(touched) && touched < cutoff;
|
|
8039
|
+
});
|
|
8040
|
+
|
|
8041
|
+
if (!candidates.length) {
|
|
8042
|
+
console.log('No abandoned tickets under account ' + accountId + ' (agent-held, untouched for ' +
|
|
8043
|
+
staleHours + 'h). ' + (((queue && queue.tickets) || []).length) + ' accepted/in_progress ticket(s) checked.');
|
|
8044
|
+
return;
|
|
8045
|
+
}
|
|
8046
|
+
|
|
8047
|
+
console.log((apply ? 'Reclaiming ' : 'Would reclaim ') + candidates.length + ' abandoned ticket(s) — ' +
|
|
8048
|
+
'agent-held, no live session, untouched for ' + staleHours + 'h:');
|
|
8049
|
+
console.log('');
|
|
8050
|
+
let done = 0;
|
|
8051
|
+
for (const t of candidates) {
|
|
8052
|
+
const id = String(t.ticketId || t.id);
|
|
8053
|
+
const line = ' ' + id.padStart(6) + ' ' + String(t.status || '').padEnd(13) +
|
|
8054
|
+
String(t.assignedTo || t.routedAgentId || '').slice(0, 16).padEnd(18) +
|
|
8055
|
+
String(t.subject || '').slice(0, 52);
|
|
8056
|
+
if (!apply) {
|
|
8057
|
+
console.log(line);
|
|
8058
|
+
continue;
|
|
8059
|
+
}
|
|
8060
|
+
try {
|
|
8061
|
+
await buildAxios(context.baseUrl, context.session.token, 30000)
|
|
8062
|
+
.post('/cli/ticket', {
|
|
8063
|
+
action: 'reclaim', ticketId: id, accountId: Number(t.accountId) || undefined,
|
|
8064
|
+
dataMode: context.dataMode, staleHours, reason: flags.reason || undefined
|
|
8065
|
+
});
|
|
8066
|
+
done += 1;
|
|
8067
|
+
console.log(line + ' -> open');
|
|
8068
|
+
} catch (err) {
|
|
8069
|
+
const body = err && err.response && err.response.data;
|
|
8070
|
+
// A refusal is the platform re-checking against the document and disagreeing with this listing.
|
|
8071
|
+
// That is the guard working, so it is reported per ticket rather than failing the run.
|
|
8072
|
+
console.log(line + ' -> refused: ' + ((body && body.message) || describeError(err)).slice(0, 90));
|
|
8073
|
+
}
|
|
8074
|
+
}
|
|
8075
|
+
console.log('');
|
|
8076
|
+
console.log(apply
|
|
8077
|
+
? (done + ' reclaimed and back in the queue; an idle supervisor will sweep them.')
|
|
8078
|
+
: 'Dry run. Re-run with --apply to reclaim these.');
|
|
8079
|
+
}
|
|
8080
|
+
|
|
8081
|
+
/**
|
|
8082
|
+
* Raise a ticket.
|
|
8083
|
+
*
|
|
8084
|
+
* <p>The verb a worker needs when it finds a SECOND problem. Without it the only options were to widen the
|
|
8085
|
+
* ticket it was given until the record no longer described one piece of work, or to drop the finding.</p>
|
|
8086
|
+
*/
|
|
8087
|
+
async function ticketCreateCommand(flags) {
|
|
8088
|
+
const local = listLocalAgents()[0] || null;
|
|
8089
|
+
const context = agentSessionFor(flags, local);
|
|
8090
|
+
const accountId = Number(flags['account-id'] || flags.accountId || context.accountId);
|
|
8091
|
+
if (!Number.isFinite(accountId) || accountId <= 0) {
|
|
8092
|
+
throw new Error('ticket create needs --account-id (or run it from an account repo).');
|
|
8093
|
+
}
|
|
8094
|
+
if (!flags.subject) {
|
|
8095
|
+
throw new Error('ticket create needs --subject "..." and --type defect|question|task|incident|enhancement.');
|
|
8096
|
+
}
|
|
8097
|
+
|
|
8098
|
+
let response;
|
|
8099
|
+
try {
|
|
8100
|
+
response = await buildAxios(context.baseUrl, context.session.token, 30000)
|
|
8101
|
+
.post('/cli/ticketCreate', {
|
|
8102
|
+
accountId,
|
|
8103
|
+
dataMode: context.dataMode,
|
|
8104
|
+
subject: flags.subject,
|
|
8105
|
+
description: flags.description || flags.body,
|
|
8106
|
+
type: flags.type || 'defect',
|
|
8107
|
+
priority: flags.priority,
|
|
8108
|
+
source: flags.source,
|
|
8109
|
+
workstream: flags.workstream,
|
|
8110
|
+
affectedComponent: flags['affected-component'] || flags.affectedComponent,
|
|
8111
|
+
implementationAccountId: flags['implementation-account-id'] || flags.implementationAccountId,
|
|
8112
|
+
// A deterministic key makes this get-or-create, which is what stops a swept fleet raising the
|
|
8113
|
+
// same follow-up once per worker.
|
|
8114
|
+
referenceId: flags['reference-id'] || flags.referenceId,
|
|
8115
|
+
tags: flags.tags ? String(flags.tags).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
8116
|
+
agentId: flags['agent-id'] || process.env.REMITS_AGENT_ID || undefined
|
|
8117
|
+
}).then((r) => r.data);
|
|
8118
|
+
} catch (err) {
|
|
8119
|
+
const body = err && err.response && err.response.data;
|
|
8120
|
+
if (body && body.message) {
|
|
8121
|
+
console.error('Refused: ' + body.message);
|
|
8122
|
+
process.exitCode = 1;
|
|
8123
|
+
return;
|
|
8124
|
+
}
|
|
8125
|
+
throw err;
|
|
8126
|
+
}
|
|
8127
|
+
|
|
8128
|
+
if (flagEnabled(flags.json)) {
|
|
8129
|
+
console.log(JSON.stringify(response, null, 2));
|
|
8130
|
+
return;
|
|
8131
|
+
}
|
|
8132
|
+
const t = (response && response.ticket) || {};
|
|
8133
|
+
console.log('created ticket ' + response.ticketId + ' on account ' + response.accountId +
|
|
8134
|
+
' [' + (t.status || 'open') + ']');
|
|
8135
|
+
console.log(' ' + (t.subject || flags.subject));
|
|
8136
|
+
}
|
|
8137
|
+
|
|
7560
8138
|
/** The account a ticket belongs to, so routing does not make the caller look it up by hand. */
|
|
7561
8139
|
async function resolveTicketAccountId(context, ticketId) {
|
|
7562
8140
|
const read = await buildAxios(context.baseUrl, context.session.token, 20000)
|
|
@@ -7573,6 +8151,20 @@ async function resolveTicketAccountId(context, ticketId) {
|
|
|
7573
8151
|
function printTicketHelp() {
|
|
7574
8152
|
console.log('Usage: remits-cli ticket <action> --ticket ID');
|
|
7575
8153
|
console.log('');
|
|
8154
|
+
console.log(' queue [--account-id ID] [--status open,accepted] [--unrouted] [--search "..."]');
|
|
8155
|
+
console.log(' What else is open. Read it BEFORE deciding your ticket is unique — the same fix often');
|
|
8156
|
+
console.log(' appears a dozen times, and the "repo" column is the account the edit lease excludes');
|
|
8157
|
+
console.log(' on, so it also tells you which of these can be worked at the same time.');
|
|
8158
|
+
console.log(' Also: --priority --type --workstream --assigned-to --unassigned --board-stage');
|
|
8159
|
+
console.log(' --planned-in --blocked-by --sort-by --sort-direction --limit --json');
|
|
8160
|
+
console.log(' --awaiting-response lists only tickets parked on a human. A status filter cannot');
|
|
8161
|
+
console.log(' ask that: `ask` parks a ticket in pending_review, which also means "done, review me".');
|
|
8162
|
+
console.log(' create --subject "..." --type defect|question|task|incident|enhancement');
|
|
8163
|
+
console.log(' Raise a ticket. Use it when you find a SECOND problem while working one — file it');
|
|
8164
|
+
console.log(' rather than widening the ticket you were given. [--priority --description --tags');
|
|
8165
|
+
console.log(' --workstream --affected-component --implementation-account-id --reference-id]');
|
|
8166
|
+
console.log(' --reference-id makes it get-or-create, so a re-run does not duplicate it.');
|
|
8167
|
+
console.log('');
|
|
7576
8168
|
console.log(' read The full ticket record, as JSON.');
|
|
7577
8169
|
console.log(' where WHERE this work is happening: repository account,');
|
|
7578
8170
|
console.log(' checkout, branch, staging workspace lane, the live edit lease and who holds it, the');
|
|
@@ -7584,13 +8176,39 @@ function printTicketHelp() {
|
|
|
7584
8176
|
console.log(' complete --resolution "what you found and did"');
|
|
7585
8177
|
console.log(' ask --question "..." [--context "..."] [--to WHO]');
|
|
7586
8178
|
console.log(' Put a decision to a human and park. Use it when the CHOICE is not yours.');
|
|
7587
|
-
console.log('
|
|
7588
|
-
console.log(' Answer the open question. By default this re-routes the ticket,
|
|
7589
|
-
console.log(' resumes with your answer already in its brief.');
|
|
8179
|
+
console.log(' answer --message "..." [--route false] (alias: reply)');
|
|
8180
|
+
console.log(' Answer the open question a previous run asked. By default this re-routes the ticket,');
|
|
8181
|
+
console.log(' so a fresh worker resumes with your answer already in its brief. This is NOT how you');
|
|
8182
|
+
console.log(' talk to the requester — that is `message`. (`reply` means the conversational one in');
|
|
8183
|
+
console.log(' some account tools, so this surface teaches `answer` and keeps `reply` as an alias.)');
|
|
8184
|
+
console.log(' message --message "..." [--to a@x,b@y] [--subject "..."] [--channel email]');
|
|
8185
|
+
console.log(' A PUBLIC conversation entry the requester reads. Writing it does not send it:');
|
|
8186
|
+
console.log(' the record is vendor-agnostic and the account\'s own rule chooses the transport.');
|
|
8187
|
+
console.log(' deliver --channel email --to a@x [--template T] [--subject "..."] [--idempotency-key K]');
|
|
8188
|
+
console.log(' Ask for that entry to actually be SENT. It records an outbox request; an account-owned');
|
|
8189
|
+
console.log(' rule or scheduled Action delivers it and marks the result. Use it when a ticket is');
|
|
8190
|
+
console.log(' waiting on somebody who is not watching the queue.');
|
|
8191
|
+
console.log(' deliveries [--channel email] What is queued to go out, and what already went.');
|
|
8192
|
+
console.log(' delivered --delivery-id ID | delivery-failed --delivery-id ID --reason "..."');
|
|
7590
8193
|
console.log(' release [--notes "..."] Hand it back to the queue.');
|
|
8194
|
+
console.log(' reclaim [--ticket ID | --account-id ID] [--stale-hours 24] [--apply]');
|
|
8195
|
+
console.log(' OPERATOR: take a ticket back from an agent that accepted it and vanished. Such a');
|
|
8196
|
+
console.log(' ticket is invisible to the sweep (it is assigned, so nothing offers it to anyone).');
|
|
8197
|
+
console.log(' Never touches a ticket owned by a person, holding a live claim, or awaiting a reply.');
|
|
8198
|
+
console.log(' Without --ticket it scans the account tree and is a DRY RUN until --apply.');
|
|
7591
8199
|
console.log(' reopen | assign --assignee EMAIL');
|
|
7592
8200
|
console.log(' route --agent AGENT_ID Hand it to a registered agent session.');
|
|
7593
8201
|
console.log(' unroute Clear its routing, e.g. when that session is gone.');
|
|
8202
|
+
console.log(' tag --tags a,b Mark it. The cheapest way to make a cluster of');
|
|
8203
|
+
console.log(' near-identical tickets visible as one cluster to whoever looks next.');
|
|
8204
|
+
console.log(' artifact --type TYPE --label "..." [--url U | --content "..."]');
|
|
8205
|
+
console.log(' Attach evidence — a log, a query result, a repro. Better than pasting it into a');
|
|
8206
|
+
console.log(' worklog summary, where it is neither typed nor retrievable.');
|
|
8207
|
+
console.log(' note --message "..." An INTERNAL note. The next worker and a reviewer');
|
|
8208
|
+
console.log(' see it; the requester does not. Use `message` for anything they should read.');
|
|
8209
|
+
console.log(' participant --email E [--role watcher|requester|agent]');
|
|
8210
|
+
console.log(' field --key K --value V Write the ORGANIZATION\'s own field on the ticket,');
|
|
8211
|
+
console.log(' outside the platform\'s bounded planning slots. Whatever your account models.');
|
|
7594
8212
|
console.log(' planning [--workstream V] [--planned-in V] [--board-stage V] [--rank N] [--size V] [--blocked-by V]');
|
|
7595
8213
|
console.log(' Update organization-owned planning fields without changing platform lifecycle status.');
|
|
7596
8214
|
console.log(' Pass a flag with no value to CLEAR that slot, e.g. --blocked-by');
|
|
@@ -7773,6 +8391,104 @@ async function agentListCommand(flags) {
|
|
|
7773
8391
|
}
|
|
7774
8392
|
}
|
|
7775
8393
|
|
|
8394
|
+
/**
|
|
8395
|
+
* Inspect and correct the local account-repository index — which checkout a ticket for an account is
|
|
8396
|
+
* worked in.
|
|
8397
|
+
*
|
|
8398
|
+
* <p>This index is the one fact the platform cannot supply: it knows the ticket and the account, and
|
|
8399
|
+
* only this machine knows where that account is checked out. It is built by scanning for
|
|
8400
|
+
* `account-info.json`, which is also present in every unpacked zip export, so it can and does acquire
|
|
8401
|
+
* entries that are not checkouts at all. There was no way to see it, and no way to correct it.</p>
|
|
8402
|
+
*/
|
|
8403
|
+
async function agentReposCommand(flags) {
|
|
8404
|
+
const accountId = Number(flags['account-id'] || flags.accountId);
|
|
8405
|
+
const directory = flags.directory || flags.dir;
|
|
8406
|
+
|
|
8407
|
+
if (Number.isFinite(accountId) && accountId > 0 && directory) {
|
|
8408
|
+
const resolved = path.resolve(String(directory));
|
|
8409
|
+
if (!fs.existsSync(resolved)) {
|
|
8410
|
+
throw new Error('No such directory: ' + resolved);
|
|
8411
|
+
}
|
|
8412
|
+
if (!isAccountCheckout(resolved)) {
|
|
8413
|
+
throw new Error(resolved + ' is not a git checkout. A ticket worker edits, stages and reports ' +
|
|
8414
|
+
'against this directory, so an unpacked export would silently lose the work.');
|
|
8415
|
+
}
|
|
8416
|
+
const info = loadAccountInfo(resolved);
|
|
8417
|
+
const declared = info ? Number(accountIdFromAccountInfo(info)) : null;
|
|
8418
|
+
if (declared && declared !== accountId) {
|
|
8419
|
+
throw new Error(resolved + ' declares account ' + declared + ', not ' + accountId +
|
|
8420
|
+
'. Point account ' + declared + ' at it instead, or pass the directory that belongs to ' + accountId + '.');
|
|
8421
|
+
}
|
|
8422
|
+
const index = readAccountRepoIndex();
|
|
8423
|
+
const previous = index[String(accountId)];
|
|
8424
|
+
index[String(accountId)] = {
|
|
8425
|
+
...(previous || {}),
|
|
8426
|
+
...(info ? buildAccountRepoEntryFromInfo(info, resolved, 'manual') : {}),
|
|
8427
|
+
accountId,
|
|
8428
|
+
directory: resolved,
|
|
8429
|
+
source: 'manual',
|
|
8430
|
+
// The operator has just answered the question, so the rival is no longer ambiguity.
|
|
8431
|
+
ambiguousWith: [],
|
|
8432
|
+
updatedAt: new Date().toISOString()
|
|
8433
|
+
};
|
|
8434
|
+
writeAccountRepoIndex(index);
|
|
8435
|
+
console.log('account ' + accountId + ' -> ' + resolved);
|
|
8436
|
+
if (previous && previous.directory && previous.directory !== resolved) {
|
|
8437
|
+
console.log(' (was ' + previous.directory + ')');
|
|
8438
|
+
}
|
|
8439
|
+
return;
|
|
8440
|
+
}
|
|
8441
|
+
|
|
8442
|
+
if (Number.isFinite(accountId) && accountId > 0 && flagEnabled(flags.forget)) {
|
|
8443
|
+
const index = readAccountRepoIndex();
|
|
8444
|
+
const previous = index[String(accountId)];
|
|
8445
|
+
delete index[String(accountId)];
|
|
8446
|
+
writeAccountRepoIndex(index);
|
|
8447
|
+
console.log(previous
|
|
8448
|
+
? 'account ' + accountId + ' removed (was ' + previous.directory + '). Its tickets will now park ' +
|
|
8449
|
+
'with an explanation rather than being worked in that directory.'
|
|
8450
|
+
: 'account ' + accountId + ' was not in the index.');
|
|
8451
|
+
return;
|
|
8452
|
+
}
|
|
8453
|
+
|
|
8454
|
+
if (flagEnabled(flags.rescan)) {
|
|
8455
|
+
try { discoverAccountRepos(); } catch (err) { console.error('scan failed: ' + describeError(err)); }
|
|
8456
|
+
}
|
|
8457
|
+
|
|
8458
|
+
const entries = listAccountRepoEntries(readAccountRepoIndex())
|
|
8459
|
+
.sort((a, b) => Number(a.accountId) - Number(b.accountId));
|
|
8460
|
+
if (flagEnabled(flags.json)) {
|
|
8461
|
+
console.log(JSON.stringify({ repos: entries, problems: accountRepoIndexProblems() }, null, 2));
|
|
8462
|
+
return;
|
|
8463
|
+
}
|
|
8464
|
+
|
|
8465
|
+
console.log('Account repositories on this machine (' + entries.length + '):');
|
|
8466
|
+
console.log('');
|
|
8467
|
+
for (const entry of entries) {
|
|
8468
|
+
const ok = isAccountCheckout(entry.directory);
|
|
8469
|
+
const missing = !fs.existsSync(entry.directory);
|
|
8470
|
+
const state = missing ? 'MISSING ' : (ok ? 'ok ' : 'NOT-GIT ');
|
|
8471
|
+
console.log(' ' + state + String(entry.accountId).padStart(5) + ' ' +
|
|
8472
|
+
String(entry.name || '').slice(0, 26).padEnd(26) + ' ' + entry.directory);
|
|
8473
|
+
for (const rival of entry.ambiguousWith || []) {
|
|
8474
|
+
console.log(' also claims this account: ' + rival);
|
|
8475
|
+
}
|
|
8476
|
+
}
|
|
8477
|
+
|
|
8478
|
+
const problems = accountRepoIndexProblems();
|
|
8479
|
+
if (problems.length) {
|
|
8480
|
+
console.log('');
|
|
8481
|
+
console.log(problems.length + ' issue(s):');
|
|
8482
|
+
for (const problem of problems) {
|
|
8483
|
+
console.log(' [' + problem.severity + '] ' + problem.message);
|
|
8484
|
+
}
|
|
8485
|
+
}
|
|
8486
|
+
console.log('');
|
|
8487
|
+
console.log(' Set one: remits-cli agent repos --account-id <id> --directory <path>');
|
|
8488
|
+
console.log(' Remove one: remits-cli agent repos --account-id <id> --forget');
|
|
8489
|
+
console.log(' Rescan: remits-cli agent repos --rescan');
|
|
8490
|
+
}
|
|
8491
|
+
|
|
7776
8492
|
async function agentReleaseCommand(flags) {
|
|
7777
8493
|
const agentId = resolveAgentId(flags, { optional: true });
|
|
7778
8494
|
if (!agentId) {
|
|
@@ -7813,6 +8529,7 @@ async function agentCommand(flags, subcommand) {
|
|
|
7813
8529
|
case 'work': return agentWorkCommand(flags);
|
|
7814
8530
|
case 'list': case 'ls': return agentListCommand(flags);
|
|
7815
8531
|
case 'map': case 'work-map': case 'workmap': return agentWorkMapCommand(flags);
|
|
8532
|
+
case 'repos': case 'repo': return agentReposCommand(flags);
|
|
7816
8533
|
case 'release': case 'deregister': case 'stop': return agentReleaseCommand(flags);
|
|
7817
8534
|
case 'heartbeat-daemon': return agentHeartbeatDaemonCommand(flags);
|
|
7818
8535
|
default:
|
|
@@ -7858,6 +8575,12 @@ function printAgentHelp() {
|
|
|
7858
8575
|
console.log(' map [--account-ids 1,4] [--json] Who is EDITING which repository right now.');
|
|
7859
8576
|
console.log(' One agent at a time may edit an account\'s repo (the edit lease); everyone else');
|
|
7860
8577
|
console.log(' investigates read-only. Check this before starting work that changes files.');
|
|
8578
|
+
console.log(' repos [--rescan] [--json] WHERE each account is checked out on this machine —');
|
|
8579
|
+
console.log(' the one fact the platform cannot supply, and what decides the directory a ticket');
|
|
8580
|
+
console.log(' worker is launched into. Flags anything that is not a git checkout (an unpacked');
|
|
8581
|
+
console.log(' export looks identical to the scanner) and any account claimed by two checkouts.');
|
|
8582
|
+
console.log(' Correct it with --account-id <id> --directory <path>, or --account-id <id> --forget');
|
|
8583
|
+
console.log(' to make that account\'s tickets park instead of being worked somewhere wrong.');
|
|
7861
8584
|
console.log(' release Deregister this session immediately.');
|
|
7862
8585
|
console.log('');
|
|
7863
8586
|
console.log(' After `register`, the other subcommands need no flags: they reuse the platform, lane');
|
|
@@ -8366,8 +9089,16 @@ function printToolHelp() {
|
|
|
8366
9089
|
console.log('Usage: remits-cli tool --name <toolName> [--base-url URL] [--account-id ID] [--branch BRANCH] [--input "{...}"|--input-file file.json] [--data-mode test|prod] [--variant-branch NAME|none] [--timeout-ms 60000] [--async true --wait true]');
|
|
8367
9090
|
console.log(' remits-cli tool status --call-id <callId> [--base-url URL] [--account-id ID] [--data-mode test|prod]');
|
|
8368
9091
|
console.log('');
|
|
9092
|
+
console.log('Hierarchy scope (read/discovery tools) — you do NOT need the owning child account id:');
|
|
9093
|
+
console.log(' --scope self|children|hierarchy expand from the repo account when searching');
|
|
9094
|
+
console.log(' --target-account-id ID the exact account to act on (required by writes)');
|
|
9095
|
+
console.log(' --account-ids 1,2,3 an explicit bounded list of owner accounts');
|
|
9096
|
+
console.log(' --anchor-account-id ID disambiguate the path for a multi-parent account');
|
|
9097
|
+
console.log(' These are merged into --input; a value already in --input wins.');
|
|
9098
|
+
console.log('');
|
|
8369
9099
|
console.log('Examples:');
|
|
8370
9100
|
console.log(' remits-cli tool --name mcp_firestore_search --input-file query.json --data-mode prod');
|
|
9101
|
+
console.log(' remits-cli tool --name mcp_firestore_search --input \'{"collection":"statements","documentId":"1234"}\' --scope children');
|
|
8371
9102
|
console.log(' remits-cli tool --name mcp_run_action --input \'{"accountId":49,"actionId":200,"executionMode":"async"}\' --data-mode prod');
|
|
8372
9103
|
console.log('');
|
|
8373
9104
|
console.log('For mcp_run_action and mcp_run_agent, prefer the tool\'s own executionMode:"async" and poll with that tool\'s controlAction:"status".');
|
|
@@ -8454,7 +9185,7 @@ async function main() {
|
|
|
8454
9185
|
console.log(' remits-cli data-mode [set test|prod]');
|
|
8455
9186
|
console.log(' remits-cli install --skills [--target codex|claude|gemini|all] [--overwrite true]');
|
|
8456
9187
|
console.log(' remits-cli tools [--base-url URL] [--account-id ID] [--branch BRANCH] [--data-mode test|prod] [--variant-branch NAME|none]');
|
|
8457
|
-
console.log(' remits-cli tool --name <toolName> [--base-url URL] [--branch BRANCH] [--input \"{...}\"|--input-file file.json] [--data-mode test|prod] [--variant-branch NAME|none] [--timeout-ms 60000] [--async true --wait true]');
|
|
9188
|
+
console.log(' remits-cli tool --name <toolName> [--base-url URL] [--branch BRANCH] [--input \"{...}\"|--input-file file.json] [--data-mode test|prod] [--scope self|children|hierarchy] [--target-account-id ID] [--account-ids 1,2,3] [--variant-branch NAME|none] [--timeout-ms 60000] [--async true --wait true]');
|
|
8458
9189
|
console.log(' remits-cli tool status --call-id <callId> [--base-url URL] [--account-id ID] [--data-mode test|prod]');
|
|
8459
9190
|
console.log(' remits-cli workspace [show|use <name>|use --auto|clear] # isolate staging when several agents share a repo');
|
|
8460
9191
|
console.log(' remits-cli components stage [--base-url URL] [--account-id ID] [--branch BRANCH] [--workspace NAME] [--changed-only] [--data-mode test|prod] [--json|--verbose]');
|
package/package.json
CHANGED
|
@@ -23,6 +23,10 @@ description: Use remits-cli for fast branch-scoped component staging, test execu
|
|
|
23
23
|
- [You are an agent, and you register yourself](#you-are-an-agent-and-you-register-yourself)
|
|
24
24
|
- [Autonomous: one ticket, one process](#autonomous-one-ticket-one-process)
|
|
25
25
|
- [If you are the worker](#if-you-are-the-worker)
|
|
26
|
+
- [Before you edit anything: where you are, and whether you may](#before-you-edit-anything-where-you-are-and-whether-you-may)
|
|
27
|
+
- [Your account's process is binding, and it is already in your brief](#your-accounts-process-is-binding-and-it-is-already-in-your-brief)
|
|
28
|
+
- [Seeing the queue as a human does](#seeing-the-queue-as-a-human-does)
|
|
29
|
+
- [Agent components are workers too](#agent-components-are-workers-too)
|
|
26
30
|
- [Moving a ticket through its lifecycle](#moving-a-ticket-through-its-lifecycle)
|
|
27
31
|
- [When a worker needs a decision from a human](#when-a-worker-needs-a-decision-from-a-human)
|
|
28
32
|
- [The manual loop](#the-manual-loop)
|
|
@@ -587,6 +591,64 @@ headless run is invisible otherwise), and **end in a terminal state** — `compl
|
|
|
587
591
|
resolution, or `update_status` with what you established and what the next agent should try. Exiting
|
|
588
592
|
quietly leaves a ticket that looks in-flight forever.
|
|
589
593
|
|
|
594
|
+
### Before you edit anything: where you are, and whether you may
|
|
595
|
+
|
|
596
|
+
Presence answers *who*. Two more facts answer *whether you may edit*, and with git worktrees and
|
|
597
|
+
workspace lanes an account id no longer identifies a working tree — so an agent that does not ask these
|
|
598
|
+
is assuming, and the assumption it makes when it guesses wrong is "this is my repository to edit".
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
remits-cli ticket where --ticket 22454 # repo account, checkout, branch, staging lane, lease, claim, YOUR phase
|
|
602
|
+
remits-cli agent map # every repository, who holds each lease, and where each agent is working
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
**Reading, reproducing and investigating are parallel-safe and unrestricted. Editing one account's
|
|
606
|
+
repository is exclusive**, enforced by a lease held per repository account per data lane:
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
remits-cli ticket lease --ticket 22454 # take it when you started read-only and reached an actual edit
|
|
610
|
+
remits-cli ticket unlease --ticket 22454 # complete / ask / release already do this for you
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Four things are worth knowing and are not obvious:
|
|
614
|
+
|
|
615
|
+
- **A refusal is not an error.** The run continues read-only, and the message names who holds the lease
|
|
616
|
+
**and where they are working**, so you can tell a real conflict from a holder in a different worktree.
|
|
617
|
+
**Do not wait for a lease and do not poll for one** — investigation is most of the work on most
|
|
618
|
+
tickets, and a ticket that turns out to need an edit hands off with `ticket progress --next-step`
|
|
619
|
+
saying exactly what change is needed. A lock people queue on turns one stuck worker into a stalled
|
|
620
|
+
fleet.
|
|
621
|
+
- **A staging workspace does not replace the lease.** Separate lanes stop two runs *resolving* each
|
|
622
|
+
other's staged code; they do nothing about two processes writing the same files or pushing the same
|
|
623
|
+
branch, which is what actually destroys work.
|
|
624
|
+
- **One editing worker per repository account per lane — worktrees do not change this.** Two worktrees
|
|
625
|
+
of one repo push to the same branch on the same remote, so per-directory leases would trade file
|
|
626
|
+
conflicts for non-fast-forward push conflicts, which surface later and are worse.
|
|
627
|
+
- **A spawned ticket worker already has its own staging lane** (`REMITS_WORKSPACE=ticket-<id>`). You do
|
|
628
|
+
not set it, and your brief states it. Say which lane you staged into when you report what you verified
|
|
629
|
+
— somebody looking at the shared lane will not see your changes.
|
|
630
|
+
|
|
631
|
+
`unlease` returns **your own** lease and deliberately cannot touch anybody else's. Breaking a stale one
|
|
632
|
+
is a separate, human verb: `remits-cli ticket force-unlease --ticket ID --reason "..."`.
|
|
633
|
+
|
|
634
|
+
### Your account's process is binding, and it is already in your brief
|
|
635
|
+
|
|
636
|
+
An account declares how its work is done as Prompts with purpose `OPERATIONS`, selected per the ticket's
|
|
637
|
+
`workstream` (`support`, `sdlc`, `incident`, `release`, or whatever that organization calls its
|
|
638
|
+
processes) via `Prompt.category`, with an uncategorised one as the catch-all. The matching text is
|
|
639
|
+
**inlined verbatim into the brief** of every agent that works one of that account's tickets and is
|
|
640
|
+
binding on it — follow it even where it differs from how you would normally proceed, and if it conflicts
|
|
641
|
+
with the brief, follow the process and say so on the ticket.
|
|
642
|
+
|
|
643
|
+
You do not fetch it: if the brief has a "The process that governs this ticket" section, that is it. If
|
|
644
|
+
it says the account documents no process, the likely cause is that the prompt is **staged but not
|
|
645
|
+
committed** — an autonomous worker resolves the committed one, so it is never bound by a procedure
|
|
646
|
+
nobody has reviewed. The repo root also carries a generated `OPERATIONS.md`; that file is generated
|
|
647
|
+
from the prompt, so **edit the prompt, not the file**.
|
|
648
|
+
|
|
649
|
+
`workstream` is not `type`: a type classifies the request, a workstream names the procedure, and they
|
|
650
|
+
cross — a `defect` handled by incident response out of hours goes through the SDLC in the morning.
|
|
651
|
+
|
|
590
652
|
### Seeing the queue as a human does
|
|
591
653
|
|
|
592
654
|
`remits-cli start` opens a browser control center showing the same facts you are acting on: which
|
|
@@ -632,10 +694,76 @@ remits-cli ticket progress --ticket 22454 --summary "Traced it to the posting Ac
|
|
|
632
694
|
remits-cli ticket status --ticket 22454 --status in_progress
|
|
633
695
|
remits-cli ticket complete --ticket 22454 --resolution "What you found and did"
|
|
634
696
|
remits-cli ticket ask --ticket 22454 --question "Re-issue or skip?" --context "412 affected"
|
|
635
|
-
remits-cli ticket
|
|
697
|
+
remits-cli ticket answer --ticket 22454 --message "Skip them." # alias: reply
|
|
698
|
+
remits-cli ticket message --ticket 22454 --message "We reproduced it; a fix is staged."
|
|
699
|
+
remits-cli ticket planning --ticket 22454 --board-stage ready_for_qa --blocked-by CAB-112
|
|
636
700
|
remits-cli ticket release --ticket 22454 # hand it back to the queue
|
|
637
701
|
```
|
|
638
702
|
|
|
703
|
+
**See the whole queue before deciding your ticket is unique.** Alert-raised tickets arrive in
|
|
704
|
+
clusters, and the same root cause routinely appears under several different account names — so the
|
|
705
|
+
first useful question is usually "how many of these are one fix?", and you cannot ask it from a single
|
|
706
|
+
ticket.
|
|
707
|
+
|
|
708
|
+
```bash
|
|
709
|
+
remits-cli ticket queue --account-id 49 # triage order, most urgent first
|
|
710
|
+
remits-cli ticket queue --account-id 49 --unrouted --status open
|
|
711
|
+
remits-cli ticket queue --account-id 49 --search "firestore index"
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
The `repo` column is the account the **edit lease** excludes on, so it also tells you which of these
|
|
715
|
+
could be worked at the same time and which will serialize behind one another.
|
|
716
|
+
|
|
717
|
+
Found a second, unrelated problem while working yours? **File it rather than widening the ticket you
|
|
718
|
+
were given** — a ticket that describes two things cannot be closed by either fix.
|
|
719
|
+
|
|
720
|
+
```bash
|
|
721
|
+
remits-cli ticket create --account-id 49 --subject "Vendor name lost on re-normalization" \
|
|
722
|
+
--type defect --priority medium --reference-id vendor-name-lost
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
`--reference-id` makes it get-or-create, so a re-run — or another worker reaching the same conclusion
|
|
726
|
+
— reconciles onto the same ticket instead of filing a duplicate.
|
|
727
|
+
|
|
728
|
+
Marking and evidence, so the next reader does not repeat your work:
|
|
729
|
+
|
|
730
|
+
```bash
|
|
731
|
+
remits-cli ticket tag --ticket 22454 --tags firestore-index,cluster-aug
|
|
732
|
+
remits-cli ticket artifact --ticket 22454 --type log --label "Failing query" --content "..."
|
|
733
|
+
remits-cli ticket note --ticket 22454 --message "Internal: same cause as 23465"
|
|
734
|
+
remits-cli ticket field --ticket 22454 --key customerReference --value CR-9182
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
`note` is internal — the next worker and a reviewer see it, the requester does not. Use `message` for
|
|
738
|
+
anything the requester should read. `field` writes your **organization's own** field, outside the
|
|
739
|
+
platform's bounded planning slots.
|
|
740
|
+
|
|
741
|
+
**`message`, `answer` and `note` are separated by who reads the result, not by tone.** They are easy to
|
|
742
|
+
confuse and they do different things to the ticket:
|
|
743
|
+
|
|
744
|
+
| Command | Who reads it | What it does to the ticket |
|
|
745
|
+
|---|---|---|
|
|
746
|
+
| `ticket message --message "..."` | the requester | appends a public entry. **Does not send anything** |
|
|
747
|
+
| `ticket note --message "..."` | the next worker, a reviewer | appends an internal entry |
|
|
748
|
+
| `ticket answer --message "..."` | whoever asked | answers the open question and **re-routes**, so a fresh worker resumes |
|
|
749
|
+
|
|
750
|
+
`ticket reply` is an **alias of `answer`** — kept because every existing brief says it. Do not read it as
|
|
751
|
+
"reply to the customer"; that is `message`. (Some account tools spell the conversational verb `reply`,
|
|
752
|
+
which is exactly why this surface teaches `answer`.)
|
|
753
|
+
|
|
754
|
+
**Appending a message is not emailing anyone.** The record is deliberately vendor-agnostic; the account
|
|
755
|
+
owns the transport. To ask for something to actually go out:
|
|
756
|
+
|
|
757
|
+
```bash
|
|
758
|
+
remits-cli ticket deliver --ticket 22454 --channel email --to ops@acme.test \
|
|
759
|
+
--subject "Waiting on you" --idempotency-key waiting-22454
|
|
760
|
+
remits-cli ticket deliveries --ticket 22454 # what is queued, and what already went
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
That records an outbox request. An account-owned Rule or scheduled Action sends it and marks the result
|
|
764
|
+
(`ticket delivered --delivery-id ...` / `ticket delivery-failed --delivery-id ... --reason "..."`). Use
|
|
765
|
+
this when a ticket is waiting on somebody who is not watching the queue.
|
|
766
|
+
|
|
639
767
|
### When a worker needs a decision from a human
|
|
640
768
|
|
|
641
769
|
There are **three** ways a run can end, not two, and the third is what stops an agent guessing:
|
|
@@ -644,12 +772,14 @@ There are **three** ways a run can end, not two, and the third is what stops an
|
|
|
644
772
|
|---|---|---|
|
|
645
773
|
| Done | `ticket complete --resolution` | finished, here is what I did |
|
|
646
774
|
| **Blocked on a choice** | `ticket ask --question` | I understand the work; the DECISION is not mine |
|
|
775
|
+
|
|
647
776
|
| Could not finish | `ticket status --status pending_review` + `ticket progress` | stuck, here is the hand-off |
|
|
648
777
|
|
|
649
778
|
`ask` puts the question on the ticket as an outbound message and parks it. The queue then shows it as
|
|
650
779
|
**awaiting a response** — distinct from "done, review me", which `pending_review` alone cannot express.
|
|
651
780
|
|
|
652
|
-
Someone answers with `remits-cli ticket
|
|
781
|
+
Someone answers with `remits-cli ticket answer --message "..."` (`reply` is an alias), or through an
|
|
782
|
+
operator Embeddable.
|
|
653
783
|
That **re-routes the ticket by default**, so the supervisor's next poll starts a **fresh worker whose
|
|
654
784
|
brief already contains the exchange**. Resumption costs nothing because a worker is one process per
|
|
655
785
|
ticket — there is no session to restore.
|
|
@@ -684,8 +814,9 @@ Ticket-routing context:
|
|
|
684
814
|
- For defect investigations, start from the owning ticket account, then move to the platform/product context if the root cause is in shared components.
|
|
685
815
|
|
|
686
816
|
Sandbox note:
|
|
687
|
-
- `remits-cli`
|
|
688
|
-
-
|
|
817
|
+
- Every `remits-cli` command that reaches the Remits service (`auth`, `tools`, `tool`, `components`, `test`, `token`, and all of `ticket` / `agent`) needs outbound network access.
|
|
818
|
+
- **Inside an autonomous ticket worker this is already arranged** — the supervisor launches the worker with network enabled, in both the editing and the investigating phase. An investigating worker is restricted from *writing the repository*, never from *talking to the platform*: reading its ticket, running a diagnostic and recording a finding are the whole of what investigating means.
|
|
819
|
+
- Elsewhere, `ENOTFOUND`, `EAI_AGAIN`, `ECONNREFUSED`, `EPERM` or a bare exit 6 / `HTTP:000` from a sandboxed session means the command needs escalated permissions or must run outside the sandbox. Do not read it as "the platform is down" — check a second command before concluding anything about the service.
|
|
689
820
|
|
|
690
821
|
Use the same repo/context rules as other tools:
|
|
691
822
|
- If the ticket targets a `CLIENT` account, do not assume that client's repo is the implementation repo. Confirm the parent `PLATFORM` / `PRODUCT` relationship first.
|
|
@@ -975,6 +1106,43 @@ remits-cli tool --name "mcp_run_action" --input '{"accountId":49,"actionId":200,
|
|
|
975
1106
|
|
|
976
1107
|
Every tool response is saved to `./.remits-cli/tool-responses/<callId>.json`.
|
|
977
1108
|
|
|
1109
|
+
### Hierarchy-scoped tool reads
|
|
1110
|
+
|
|
1111
|
+
For read/discovery tools, the account resolved from the checkout/session is the **scope root**, not proof
|
|
1112
|
+
that the business record is owned by that account. This closes the common support loop where you know a
|
|
1113
|
+
precise document, object, event, or indexed source id but do not yet know which child account owns it.
|
|
1114
|
+
|
|
1115
|
+
Use the tool flags rather than editing JSON by hand:
|
|
1116
|
+
|
|
1117
|
+
```bash
|
|
1118
|
+
remits-cli tool --name mcp_firestore_search \
|
|
1119
|
+
--input '{"collection":"statements","documentId":"1234"}' \
|
|
1120
|
+
--scope children --data-mode prod
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
Available flags:
|
|
1124
|
+
|
|
1125
|
+
| Flag | Meaning |
|
|
1126
|
+
|---|---|
|
|
1127
|
+
| `--scope self|children|hierarchy` | Expand from the repo/session account for read/discovery. Exact-id lookups usually default to `children`; broad searches default to `self` unless widened. |
|
|
1128
|
+
| `--target-account-id ID` | Exact owner/execution account when already known. Required by mutating tools. |
|
|
1129
|
+
| `--account-ids 1,2,3` | Explicit bounded owner list. The platform verifies every id against the scope root. |
|
|
1130
|
+
| `--anchor-account-id ID` | Path-disambiguation anchor for multi-parent account relationships. |
|
|
1131
|
+
|
|
1132
|
+
The CLI merges these into `--input`; a value already present in `--input` wins. Tool responses echo
|
|
1133
|
+
`scopeRootAccountId`, `scope`, `accountIds`, and, when an exact owner is discovered,
|
|
1134
|
+
`resolvedTargetAccountId`. Feed that returned owner to `mcp_firestore_patch`, action/test runs, and browser
|
|
1135
|
+
tokens. Mutating tools do not infer or fan out writes.
|
|
1136
|
+
|
|
1137
|
+
The implicit account ceiling is intentionally different by shape: broad searches stay capped at 100 accounts
|
|
1138
|
+
unless the tool says otherwise, while exact-id discovery may span up to 1000 accounts by default. If a broad
|
|
1139
|
+
tool returns `scopeTooBroad`, narrow with `--target-account-id`, `--account-ids`, or a smaller `--scope`.
|
|
1140
|
+
|
|
1141
|
+
`mcp_firestore_search` handles exact Firestore document ids. `mcp_index_search` handles fuzzy/semantic
|
|
1142
|
+
lookup through Vertex. `mcp_bigquery_query` handles warehouse lookup/query with server-resolved
|
|
1143
|
+
`{table_current}`/`{table}` placeholders and the scoped `{account_filter}` predicate. BigQuery is a prod
|
|
1144
|
+
analytics surface, so use `--data-mode prod` when you intend to query it.
|
|
1145
|
+
|
|
978
1146
|
Poll a CLI-transport async call (mechanism 2) by call id:
|
|
979
1147
|
|
|
980
1148
|
```bash
|
|
@@ -3181,6 +3349,20 @@ remits-cli agent release
|
|
|
3181
3349
|
remits-cli ticket read|accept|status|progress|complete|release|reopen|assign|planning --ticket ID [--status S] [--resolution "..."] [--summary "..."] [--category C] [--assignee EMAIL] [--workstream VALUE] [--planned-in VALUE] [--board-stage VALUE] [--rank N] [--size VALUE] [--blocked-by VALUE] [--notes "..."]
|
|
3182
3350
|
remits-cli ticket lease|unlease|force-unlease --ticket ID [--reason "..."] # force-unlease breaks SOMEBODY ELSE'S lease; operator only
|
|
3183
3351
|
remits-cli ticket where --ticket ID # WHERE this work is: repo account, checkout, branch, staging lane, live lease + holder's location, claim, your phase
|
|
3352
|
+
remits-cli ticket ask --ticket ID --question "..." [--context "..."] [--to WHO] # park on a human decision
|
|
3353
|
+
remits-cli ticket answer --ticket ID --message "..." [--route false] # answer it (alias: reply); re-routes by default
|
|
3354
|
+
remits-cli ticket message --ticket ID --message "..." [--to a@x,b@y] [--subject "..."] [--channel email] # PUBLIC — the requester reads it
|
|
3355
|
+
remits-cli ticket note --ticket ID --message "..." # INTERNAL — the next worker and a reviewer read it
|
|
3356
|
+
remits-cli ticket deliver --ticket ID --channel email --to a@x [--template T] [--subject "..."] [--idempotency-key K] # ask for it to be SENT
|
|
3357
|
+
remits-cli ticket deliveries --ticket ID [--channel email] # what is queued to go out, and what already went
|
|
3358
|
+
remits-cli ticket delivered --ticket ID --delivery-id ID | ticket delivery-failed --ticket ID --delivery-id ID --reason "..."
|
|
3359
|
+
remits-cli ticket tag --ticket ID --tags a,b # make a cluster of near-identical tickets visible as one
|
|
3360
|
+
remits-cli ticket artifact --ticket ID --type TYPE --label "..." [--url U | --content "..."]
|
|
3361
|
+
remits-cli ticket participant --ticket ID --email E [--role watcher|requester|agent]
|
|
3362
|
+
remits-cli ticket field --ticket ID --key K --value V # the ORGANIZATION's own field, outside the planning slots
|
|
3363
|
+
remits-cli ticket reclaim [--ticket ID | --account-id ID] [--stale-hours 24] [--apply] # OPERATOR: take back an agent's abandoned ticket
|
|
3364
|
+
remits-cli ticket queue --account-id ID [--status ...] [--unrouted] [--unassigned] [--awaiting-response] [--workstream W] [--board-stage S] [--planned-in P] [--search "..."] [--sort-by ...] [--json]
|
|
3365
|
+
remits-cli ticket create --account-id ID --subject "..." --type defect|question|task|incident|enhancement [--priority P] [--description "..."] [--tags a,b] [--workstream W] [--affected-component C] [--implementation-account-id ID] [--reference-id KEY]
|
|
3184
3366
|
remits-cli start [--foreground true] [--port 8787]
|
|
3185
3367
|
remits-cli stop
|
|
3186
3368
|
remits-cli status [--base-url URL] [--account-id ID] [--data-mode test|prod] [--json]
|