@remits/remits-cli 0.1.165 → 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/README.md +2 -1
- package/index.js +240 -33
- package/package.json +1 -1
- package/skills/remits-cli/SKILL.md +7 -0
- package/skills/remits-cli/references/agent-sessions.md +1 -0
- package/skills/remits-cli/references/cli-state.md +3 -1
- package/skills/remits-cli/references/command-reference.md +1 -1
- package/skills/remits-cli/references/development-loop.md +8 -0
- package/skills/remits-cli/references/support-tickets.md +50 -0
- package/skills/remits-cli/references/tool-reference.md +25 -0
- package/skills/remits-cli/references/troubleshooting.md +1 -1
package/README.md
CHANGED
|
@@ -101,7 +101,8 @@ remits-cli install --skills --overwrite true
|
|
|
101
101
|
- 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.
|
|
102
102
|
- Active verification envelopes are scoped per `baseUrl + accountId + branch + workspace + dataMode`, so a localhost envelope does not silently capture evidence from a production command. Pass `--verify-envelope <id>` for an intentional cross-context attachment.
|
|
103
103
|
- `--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.
|
|
104
|
-
- `remits-cli start` scans
|
|
104
|
+
- `remits-cli start` scans for `account-info.json` files and updates `~/.remits-cli/account-repos.json` before bringing up the background service. While running, it rescans every five minutes using asynchronous directory reads, then reports the refreshed inventory to the platform. Repos cloned outside the CLI are discovered without a restart or a command run inside them. Both scans use `REMITS_CLI_SCAN_ROOTS` (default: the home directory), the same depth limit and directory exclusions. For immediate discovery, use `remits-cli agent repos --rescan` or **Rescan Account Repos** in the control center.
|
|
105
|
+
- Scans preserve manually selected repository directories and record competing checkouts as ambiguity. `remits-cli agent repos --account-id <id> --forget` suppresses rediscovery until an explicit registration (`--directory <path>`) or a CLI command run from that account repo restores it.
|
|
105
106
|
- Front-stage guides are **not** bundled with the CLI npm package. For account-work commands (`components`, `test`, `token`, `tools`, `tool`) and on `auth`, `remits-cli` downloads the latest guides from the authenticated `/cli/guides` endpoint and writes them into the current account repo: root `platform-overview.md` / `development-guide.md`, `guides/*.md`, `guides/features/*.md`, and the agent-guidance files `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` (from `docs/front-stage/remits-components-zip-readme.md`).
|
|
106
107
|
- Guide sync requires authentication. If no valid session exists and the terminal is interactive, the CLI auto-authenticates first; in non-interactive contexts (CI, agent sandboxes) it skips silently so guides are only ever delivered to users who can authenticate to the platform.
|
|
107
108
|
- On every command, `remits-cli` refreshes the installed remits-cli `SKILL.md` and reference files for `claude`, `codex`, `gemini`, and `antigravity` so local agents stay on the latest packaged instructions.
|
package/index.js
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
## Table of Contents
|
|
5
5
|
|
|
6
6
|
- L22 Runtime Bootstrap And Shared State
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
7
|
+
- L126 Sessions, Account Resolution, And Production Guards
|
|
8
|
+
- L632 Local State, Workspaces, And Verification Evidence
|
|
9
|
+
- L2670 Account Repos, Guide Sync, And Platform Repo
|
|
10
|
+
- L3569 Component Discovery And HTTP Logging
|
|
11
|
+
- L4339 Skill Delivery And TOC Resolution
|
|
12
|
+
- L4654 Auth And Component Staging
|
|
13
|
+
- L5286 Component Summaries, Status, And Sync Gates
|
|
14
|
+
- L7610 Branches, Promotion, Commit, And Test Runs
|
|
15
|
+
- L9873 Tokens, Tools, Verification, And Config
|
|
16
|
+
- L11390 Service Dashboard And WebSocket Listener
|
|
17
|
+
- L13900 Agent And Ticket Workflows
|
|
18
|
+
- L18308 Help, Auto Update, And Command Dispatch
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/*
|
|
@@ -75,6 +75,7 @@ const DASHBOARD_HOST = '127.0.0.1';
|
|
|
75
75
|
const DEFAULT_DASHBOARD_PORT = Number(process.env.REMITS_CLI_DASHBOARD_PORT || 8787);
|
|
76
76
|
const DEFAULT_SCAN_MAX_DEPTH = Number(process.env.REMITS_CLI_SCAN_MAX_DEPTH || 8);
|
|
77
77
|
const SERVICE_MAINTENANCE_INTERVAL_MS = Number(process.env.REMITS_CLI_SERVICE_MAINTENANCE_INTERVAL_MS || 15000);
|
|
78
|
+
const ACCOUNT_REPO_RESCAN_INTERVAL_MS = 5 * 60 * 1000;
|
|
78
79
|
const WEBSOCKET_RECONNECT_DELAY_MS = Number(process.env.REMITS_CLI_WEBSOCKET_RECONNECT_DELAY_MS || 5000);
|
|
79
80
|
const WEBSOCKET_HEARTBEAT_MS = Number(process.env.REMITS_CLI_WEBSOCKET_HEARTBEAT_MS || 10000);
|
|
80
81
|
const WEBSOCKET_CONNECTION_TIMEOUT_MS = Number(process.env.REMITS_CLI_WEBSOCKET_CONNECTION_TIMEOUT_MS || 15000);
|
|
@@ -3253,7 +3254,8 @@ function countAccountRepos(index) {
|
|
|
3253
3254
|
*/
|
|
3254
3255
|
function mergeAccountRepoEntries(entries, options = {}) {
|
|
3255
3256
|
const nextIndex = options.existingIndex ? { ...options.existingIndex } : {};
|
|
3256
|
-
const
|
|
3257
|
+
const sourceRank = (entry) => entry.source === 'manual' ? 2 : (entry.source === 'cwd' ? 1 : 0);
|
|
3258
|
+
const rank = (entry) => (isAccountCheckout(entry.directory) ? 3 : 0) + sourceRank(entry);
|
|
3257
3259
|
|
|
3258
3260
|
for (const entry of entries) {
|
|
3259
3261
|
if (!entry || !entry.accountId || !entry.directory) {
|
|
@@ -3261,8 +3263,15 @@ function mergeAccountRepoEntries(entries, options = {}) {
|
|
|
3261
3263
|
}
|
|
3262
3264
|
const key = String(entry.accountId);
|
|
3263
3265
|
const current = nextIndex[key];
|
|
3266
|
+
// Forget is an explicit opt-out, not a temporary absence until the next service scan.
|
|
3267
|
+
if (current && current.source === 'forgotten' && entry.source === 'scan') continue;
|
|
3264
3268
|
if (!current || !current.directory || current.directory === entry.directory) {
|
|
3265
|
-
nextIndex[key] = {
|
|
3269
|
+
nextIndex[key] = {
|
|
3270
|
+
...entry,
|
|
3271
|
+
source: current && current.directory === entry.directory && sourceRank(current) > sourceRank(entry)
|
|
3272
|
+
? current.source : entry.source,
|
|
3273
|
+
ambiguousWith: (current && current.ambiguousWith) || []
|
|
3274
|
+
};
|
|
3266
3275
|
continue;
|
|
3267
3276
|
}
|
|
3268
3277
|
|
|
@@ -3305,19 +3314,17 @@ function defaultAccountScanRoots() {
|
|
|
3305
3314
|
return [home].filter((dir) => fs.existsSync(dir));
|
|
3306
3315
|
}
|
|
3307
3316
|
|
|
3308
|
-
|
|
3317
|
+
// Share traversal rules between the synchronous command path and asynchronous service scans.
|
|
3318
|
+
// The driver supplies directory entries so the service never blocks its WebSocket on readdirSync.
|
|
3319
|
+
function* accountRepoScanSteps(dir, depth, matches, visited, platformMatches) {
|
|
3309
3320
|
const resolvedDir = path.resolve(dir);
|
|
3310
3321
|
if (visited.has(resolvedDir)) {
|
|
3311
3322
|
return;
|
|
3312
3323
|
}
|
|
3313
3324
|
visited.add(resolvedDir);
|
|
3314
3325
|
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
|
|
3318
|
-
} catch (_) {
|
|
3319
|
-
return;
|
|
3320
|
-
}
|
|
3326
|
+
const entries = yield resolvedDir;
|
|
3327
|
+
if (!entries) return;
|
|
3321
3328
|
|
|
3322
3329
|
const names = new Set(entries.map((entry) => entry.name));
|
|
3323
3330
|
for (const entry of entries) {
|
|
@@ -3347,7 +3354,17 @@ function walkForAccountInfo(dir, depth, matches, visited, platformMatches) {
|
|
|
3347
3354
|
if (entry.name.startsWith('.') && entry.name !== '.config') {
|
|
3348
3355
|
continue;
|
|
3349
3356
|
}
|
|
3350
|
-
|
|
3357
|
+
yield* accountRepoScanSteps(path.join(resolvedDir, entry.name), depth + 1, matches, visited, platformMatches);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
function walkForAccountInfo(dir, depth, matches, visited, platformMatches) {
|
|
3362
|
+
const steps = accountRepoScanSteps(dir, depth, matches, visited, platformMatches);
|
|
3363
|
+
let step = steps.next();
|
|
3364
|
+
while (!step.done) {
|
|
3365
|
+
let entries = null;
|
|
3366
|
+
try { entries = fs.readdirSync(step.value, { withFileTypes: true }); } catch (_) { /* unreadable */ }
|
|
3367
|
+
step = steps.next(entries);
|
|
3351
3368
|
}
|
|
3352
3369
|
}
|
|
3353
3370
|
|
|
@@ -3359,7 +3376,29 @@ function discoverAccountRepos() {
|
|
|
3359
3376
|
for (const root of roots) {
|
|
3360
3377
|
walkForAccountInfo(root, 0, matches, visited, platformMatches);
|
|
3361
3378
|
}
|
|
3379
|
+
return reconcileDiscoveredAccountRepos(roots, matches, platformMatches);
|
|
3380
|
+
}
|
|
3362
3381
|
|
|
3382
|
+
async function discoverAccountReposInBackground() {
|
|
3383
|
+
const matches = [];
|
|
3384
|
+
const platformMatches = [];
|
|
3385
|
+
const visited = new Set();
|
|
3386
|
+
const roots = defaultAccountScanRoots();
|
|
3387
|
+
for (const root of roots) {
|
|
3388
|
+
const steps = accountRepoScanSteps(root, 0, matches, visited, platformMatches);
|
|
3389
|
+
let step = steps.next();
|
|
3390
|
+
while (!step.done) {
|
|
3391
|
+
let entries = null;
|
|
3392
|
+
try { entries = await fs.promises.readdir(step.value, { withFileTypes: true }); } catch (_) { /* unreadable */ }
|
|
3393
|
+
step = steps.next(entries);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
// Read the latest index only at reconciliation: CLI commands may register a repo during the scan.
|
|
3397
|
+
// Background maintenance discovers existing files; it must never clone or pull repositories.
|
|
3398
|
+
return reconcileDiscoveredAccountRepos(roots, matches, platformMatches, { background: true });
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
function reconcileDiscoveredAccountRepos(roots, matches, platformMatches, options = {}) {
|
|
3363
3402
|
const entries = [];
|
|
3364
3403
|
for (const file of matches) {
|
|
3365
3404
|
try {
|
|
@@ -3380,7 +3419,7 @@ function discoverAccountRepos() {
|
|
|
3380
3419
|
const found = platformMatches.find(looksLikePlatformRepo);
|
|
3381
3420
|
if (found) {
|
|
3382
3421
|
setPlatformRepoEntry(found, 'scan');
|
|
3383
|
-
} else {
|
|
3422
|
+
} else if (!options.background) {
|
|
3384
3423
|
try { ensurePlatformRepoLocal({ allowClone: true }); } catch (_) { /* best-effort */ }
|
|
3385
3424
|
}
|
|
3386
3425
|
}
|
|
@@ -3395,6 +3434,27 @@ function discoverAccountRepos() {
|
|
|
3395
3434
|
};
|
|
3396
3435
|
}
|
|
3397
3436
|
|
|
3437
|
+
let accountRepoDiscoveryInFlight = null;
|
|
3438
|
+
let lastAccountRepoDiscoveryAttemptAt = 0;
|
|
3439
|
+
|
|
3440
|
+
function refreshAccountReposIfDue(now = Date.now()) {
|
|
3441
|
+
if (accountRepoDiscoveryInFlight) return accountRepoDiscoveryInFlight;
|
|
3442
|
+
const lastScanAt = Date.parse(runtimeState.discovery.scannedAt || '') || 0;
|
|
3443
|
+
if (now - Math.max(lastScanAt, lastAccountRepoDiscoveryAttemptAt) < ACCOUNT_REPO_RESCAN_INTERVAL_MS) return;
|
|
3444
|
+
lastAccountRepoDiscoveryAttemptAt = now;
|
|
3445
|
+
accountRepoDiscoveryInFlight = discoverAccountReposInBackground()
|
|
3446
|
+
.then((summary) => {
|
|
3447
|
+
recordDiscoverySummary(summary);
|
|
3448
|
+
// Publish the refreshed inventory on the next maintenance tick, even if presence isn't due yet.
|
|
3449
|
+
for (const managedClient of managedClients) managedClient.lastPresenceAt = 0;
|
|
3450
|
+
})
|
|
3451
|
+
.catch((err) => {
|
|
3452
|
+
appendGlobalActivityLog('listener', 'repos.discovery.failed', { error: describeError(err) }, 'error');
|
|
3453
|
+
})
|
|
3454
|
+
.finally(() => { accountRepoDiscoveryInFlight = null; });
|
|
3455
|
+
return accountRepoDiscoveryInFlight;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3398
3458
|
function resolvePreferredAccountId(cwd, flags, session) {
|
|
3399
3459
|
// Explicit --account-id wins over the repo's account-info.json so users can
|
|
3400
3460
|
// target a child account (e.g. a customer) from a parent-account repo.
|
|
@@ -14553,8 +14613,50 @@ function agentSessionFor(flags, entry) {
|
|
|
14553
14613
|
};
|
|
14554
14614
|
}
|
|
14555
14615
|
|
|
14556
|
-
/**
|
|
14557
|
-
|
|
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
|
+
}
|
|
14558
14660
|
const claims = listAccountRepoEntries(readAccountRepoIndex()).map((entry) => ({
|
|
14559
14661
|
accountId: Number(entry.accountId),
|
|
14560
14662
|
name: entry.name || null,
|
|
@@ -14602,6 +14704,7 @@ async function agentRegisterCommand(flags, options = {}) {
|
|
|
14602
14704
|
try { discoverAccountRepos(); } catch (_) { /* best-effort */ }
|
|
14603
14705
|
|
|
14604
14706
|
const { session, baseUrl, dataMode } = agentSessionFor(flags, null);
|
|
14707
|
+
const coverage = parseAgentCoverage(Object.assign({}, options.serve && options.serve.coverageFlags, flags));
|
|
14605
14708
|
const anchor = resolveAgentAnchorPid();
|
|
14606
14709
|
if (!anchor.pid) {
|
|
14607
14710
|
throw new Error(
|
|
@@ -14638,7 +14741,8 @@ async function agentRegisterCommand(flags, options = {}) {
|
|
|
14638
14741
|
workspace: resolveWorkspace(cwd, flags) || undefined,
|
|
14639
14742
|
dataMode,
|
|
14640
14743
|
accountId: Number(session.accountId) || undefined,
|
|
14641
|
-
accounts: agentAccountClaims(cwd),
|
|
14744
|
+
accounts: agentAccountClaims(cwd, coverage),
|
|
14745
|
+
coverageExplicit: coverage ? true : undefined,
|
|
14642
14746
|
// Capacity is how many workers this session can actually RUN, which only this side knows. The
|
|
14643
14747
|
// platform stops routing past it, so a supervisor at capacity is not handed work it cannot start.
|
|
14644
14748
|
maxConcurrent: options.serve ? options.serve.maxConcurrent : (flags['max-concurrent'] ? parsePositiveInt(flags['max-concurrent'], 1) : undefined),
|
|
@@ -14673,6 +14777,7 @@ async function agentRegisterCommand(flags, options = {}) {
|
|
|
14673
14777
|
// The accounts the platform ACCEPTED, so `agent list` can scope itself to the work this session
|
|
14674
14778
|
// can actually be routed rather than to whichever session happened to be most recent.
|
|
14675
14779
|
accountIds: response.accountIds || [],
|
|
14780
|
+
coverage: coverage || null,
|
|
14676
14781
|
accountId: Number(session.accountId) || null,
|
|
14677
14782
|
anchorPid: anchor.pid,
|
|
14678
14783
|
anchorSource: anchor.source,
|
|
@@ -14714,6 +14819,13 @@ async function agentRegisterCommand(flags, options = {}) {
|
|
|
14714
14819
|
console.log(' data lane ' + dataMode + (dataMode === 'prod' ? ' (live support tickets)' : ' (fixtures only)'));
|
|
14715
14820
|
console.log(' accounts ' + accepted.length + ' account(s) this session can be routed work for');
|
|
14716
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(', ')));
|
|
14717
14829
|
console.log(' heartbeat every ' + Math.round(AGENT_HEARTBEAT_INTERVAL_MS / 1000) + 's, anchored to pid ' +
|
|
14718
14830
|
anchor.pid + ' (' + anchor.source + '), ends when this session ends');
|
|
14719
14831
|
if ((response.skippedAccounts || []).length) {
|
|
@@ -14950,6 +15062,14 @@ function startAgentHeartbeatDaemon(entry, serve = null) {
|
|
|
14950
15062
|
if (entry.persona) {
|
|
14951
15063
|
args.push('--persona', entry.persona);
|
|
14952
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
|
+
}
|
|
14953
15073
|
if (serve) {
|
|
14954
15074
|
args.push(
|
|
14955
15075
|
'--serve', 'true',
|
|
@@ -15125,6 +15245,9 @@ async function releaseTicketClaim(context, agentId, ticket, options = {}) {
|
|
|
15125
15245
|
accountId: Number(ticket.accountId),
|
|
15126
15246
|
outcome: options.outcome || null,
|
|
15127
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,
|
|
15128
15251
|
release: options.release === true,
|
|
15129
15252
|
unroute: options.unroute === true
|
|
15130
15253
|
}).then((r) => r.data);
|
|
@@ -15395,6 +15518,7 @@ async function spawnTicketWorker(context, ctx, ticket) {
|
|
|
15395
15518
|
// it back to the queue) once it has genuinely failed enough times and is still not terminal.
|
|
15396
15519
|
const giveUp = !ok && attempt >= AGENT_WORKER_MAX_ATTEMPTS;
|
|
15397
15520
|
const result = await releaseTicketClaim(context, agentId, { ticketId, accountId: ticket.accountId }, {
|
|
15521
|
+
runId: ctx.runId,
|
|
15398
15522
|
outcome: ok ? 'completed' : (signal ? 'killed' : 'failed'),
|
|
15399
15523
|
release: giveUp,
|
|
15400
15524
|
// A clean exit that left the ticket short of terminal is a DELIVERED judgement, not a failure —
|
|
@@ -15491,7 +15615,7 @@ async function agentHeartbeatDaemonCommand(flags) {
|
|
|
15491
15615
|
const context = agentSessionFor(flags, { cwd, baseUrl: flags['base-url'] });
|
|
15492
15616
|
for (const [ticketId, entry] of inFlight.entries()) {
|
|
15493
15617
|
await releaseTicketClaim(context, agentId,
|
|
15494
|
-
{ ticketId, accountId: entry.accountId }, { outcome: 'supervisor_stopped' });
|
|
15618
|
+
{ ticketId, accountId: entry.accountId }, { outcome: 'supervisor_stopped', runId: ctx.runId });
|
|
15495
15619
|
}
|
|
15496
15620
|
await buildAxios(context.baseUrl, context.session.token, 5000)
|
|
15497
15621
|
.post('/cli/agentDeregister', { agentId });
|
|
@@ -15631,6 +15755,8 @@ async function agentHeartbeatDaemonCommand(flags) {
|
|
|
15631
15755
|
'max-concurrent': String(ctx.maxConcurrent),
|
|
15632
15756
|
role: ctx.role || undefined,
|
|
15633
15757
|
persona: ctx.persona || undefined,
|
|
15758
|
+
accounts: flags.accounts || undefined,
|
|
15759
|
+
'no-descendants': flags['no-descendants'] || undefined,
|
|
15634
15760
|
json: 'true'
|
|
15635
15761
|
},
|
|
15636
15762
|
// Re-register with THIS supervisor's declared bound and queues. The platform treats an absent
|
|
@@ -15911,6 +16037,7 @@ async function ticketCommand(flags, subcommand) {
|
|
|
15911
16037
|
requiresRepositoryWork: firstDefined(flags['repo-work'], flags['requires-repository-work']),
|
|
15912
16038
|
tags: flags.tags ? String(flags.tags).split(',').map((v) => v.trim()).filter(Boolean) : undefined,
|
|
15913
16039
|
label: flags.label,
|
|
16040
|
+
...(action === 'artifact' ? ticketArtifactFields(flags) : {}),
|
|
15914
16041
|
url: flags.url,
|
|
15915
16042
|
body: flags.body,
|
|
15916
16043
|
role: flags.role,
|
|
@@ -15929,6 +16056,26 @@ async function ticketCommand(flags, subcommand) {
|
|
|
15929
16056
|
idempotencyKey: flags['idempotency-key'] || flags.idempotencyKey || undefined,
|
|
15930
16057
|
providerMessageId: flags['provider-message-id'] || undefined,
|
|
15931
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,
|
|
15932
16079
|
// Lease operations act as the AGENT, not the user: the lease is held by a worker session, and a
|
|
15933
16080
|
// human's email is not a thing that can hold one. Inside a worker these come from the environment
|
|
15934
16081
|
// the supervisor set, so the commands need no flags.
|
|
@@ -16627,9 +16774,27 @@ function printTicketHelp() {
|
|
|
16627
16774
|
console.log(' accept Claim it as yours before you start work.');
|
|
16628
16775
|
console.log(' status --status in_progress|pending_review');
|
|
16629
16776
|
console.log(' progress --summary "..." [--category investigation|fix|verification|...] [--next-step "..."]');
|
|
16630
|
-
console.log(' complete --resolution "what you found and did"');
|
|
16631
|
-
console.log('
|
|
16632
|
-
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.');
|
|
16633
16798
|
console.log(' answer --message "..." [--route false] (alias: reply)');
|
|
16634
16799
|
console.log(' Answer the open question a previous run asked. By default this re-routes the ticket,');
|
|
16635
16800
|
console.log(' so a fresh worker resumes with your answer already in its brief. This is NOT how you');
|
|
@@ -16638,11 +16803,13 @@ function printTicketHelp() {
|
|
|
16638
16803
|
console.log(' message --message "..." [--to a@x,b@y] [--subject "..."] [--channel email]');
|
|
16639
16804
|
console.log(' A PUBLIC conversation entry the requester reads. Writing it does not send it:');
|
|
16640
16805
|
console.log(' the record is vendor-agnostic and the account\'s own rule chooses the transport.');
|
|
16641
|
-
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]');
|
|
16642
16807
|
console.log(' Ask for that entry to actually be SENT. It records an outbox request; an account-owned');
|
|
16643
16808
|
console.log(' rule or scheduled Action delivers it and marks the result. Use it when a ticket is');
|
|
16644
16809
|
console.log(' waiting on somebody who is not watching the queue.');
|
|
16645
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.');
|
|
16646
16813
|
console.log(' delivered --delivery-id ID | delivery-failed --delivery-id ID --reason "..."');
|
|
16647
16814
|
console.log(' release [--notes "..."] Hand it back to the queue.');
|
|
16648
16815
|
console.log(' reclaim [--ticket ID | --account-id ID] [--stale-hours 24] [--apply]');
|
|
@@ -16656,7 +16823,7 @@ function printTicketHelp() {
|
|
|
16656
16823
|
console.log(' tag --tags a,b Mark it, for a HUMAN reading the queue. When the');
|
|
16657
16824
|
console.log(' tickets genuinely share a cause, use `relate` instead or as well: the platform reads');
|
|
16658
16825
|
console.log(' that one, and a tag it cannot read will not stop a cluster being worked N times.');
|
|
16659
|
-
console.log(' artifact --type TYPE --label "..."
|
|
16826
|
+
console.log(' artifact --type TYPE --label "..." (--content "..." | --file PATH | --url U) [--filename F] [--content-type T]');
|
|
16660
16827
|
console.log(' Attach evidence — a log, a query result, a repro. Better than pasting it into a');
|
|
16661
16828
|
console.log(' worklog summary, where it is neither typed nor retrievable.');
|
|
16662
16829
|
console.log(' note --message "..." An INTERNAL note. The next worker and a reviewer');
|
|
@@ -16701,6 +16868,44 @@ function printTicketHelp() {
|
|
|
16701
16868
|
* One helper so the lease, the claim and the registration cannot describe this process three ways —
|
|
16702
16869
|
* the same reason the platform builds the tuple in exactly one class (AgentRunContext).
|
|
16703
16870
|
*/
|
|
16871
|
+
/**
|
|
16872
|
+
* The evidence half of `ticket artifact`. Bytes always travel as `contentBase64`: it is the one form every
|
|
16873
|
+
* deployed platform accepts (JSON cannot carry a byte[], and older platforms ignored a `content` string), so
|
|
16874
|
+
* `--content "text"` and `--file path` behave the same against every host. The help promised `--type` and
|
|
16875
|
+
* `--content` for a long time while neither was sent, and every artifact was refused as `type [null]`; the
|
|
16876
|
+
* refusals here name the missing flag before anything reaches the platform.
|
|
16877
|
+
*/
|
|
16878
|
+
function ticketArtifactFields(flags = {}, readFile = (p) => fs.readFileSync(p)) {
|
|
16879
|
+
const text = (v) => (typeof v === 'string' && v.trim() ? v : undefined);
|
|
16880
|
+
const type = text(flags.type);
|
|
16881
|
+
if (!type) {
|
|
16882
|
+
throw new Error('ticket artifact needs --type (screenshot, pdf, trace, video, log, test_result, link or other).');
|
|
16883
|
+
}
|
|
16884
|
+
if (!text(flags.label)) {
|
|
16885
|
+
throw new Error('ticket artifact needs --label "...".');
|
|
16886
|
+
}
|
|
16887
|
+
const fields = { type, filename: text(flags.filename), contentType: text(flags['content-type'] || flags.contentType) };
|
|
16888
|
+
const filePath = text(flags.file);
|
|
16889
|
+
const content = typeof flags.content === 'string' ? flags.content : undefined;
|
|
16890
|
+
if (filePath) {
|
|
16891
|
+
const bytes = readFile(filePath);
|
|
16892
|
+
fields.contentBase64 = Buffer.from(bytes).toString('base64');
|
|
16893
|
+
fields.filename = fields.filename || path.basename(filePath);
|
|
16894
|
+
if (!fields.contentType) {
|
|
16895
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
16896
|
+
fields.contentType = ({ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.pdf': 'application/pdf',
|
|
16897
|
+
'.json': 'application/json', '.zip': 'application/zip', '.webm': 'video/webm', '.html': 'text/html',
|
|
16898
|
+
'.md': 'text/markdown', '.txt': 'text/plain', '.log': 'text/plain' })[ext];
|
|
16899
|
+
}
|
|
16900
|
+
} else if (content !== undefined) {
|
|
16901
|
+
fields.contentBase64 = Buffer.from(content, 'utf8').toString('base64');
|
|
16902
|
+
fields.contentType = fields.contentType || 'text/plain; charset=utf-8';
|
|
16903
|
+
} else if (!text(flags.url)) {
|
|
16904
|
+
throw new Error('ticket artifact needs something to attach: --content "...", --file PATH or --url URL.');
|
|
16905
|
+
}
|
|
16906
|
+
return fields;
|
|
16907
|
+
}
|
|
16908
|
+
|
|
16704
16909
|
function runContextPayload(flags = {}) {
|
|
16705
16910
|
const cwd = process.cwd();
|
|
16706
16911
|
return {
|
|
@@ -17504,7 +17709,7 @@ async function agentReposCommand(flags) {
|
|
|
17504
17709
|
if (Number.isFinite(accountId) && accountId > 0 && flagEnabled(flags.forget)) {
|
|
17505
17710
|
const index = readAccountRepoIndex();
|
|
17506
17711
|
const previous = index[String(accountId)];
|
|
17507
|
-
|
|
17712
|
+
index[String(accountId)] = { accountId, source: 'forgotten', updatedAt: new Date().toISOString() };
|
|
17508
17713
|
writeAccountRepoIndex(index);
|
|
17509
17714
|
console.log(previous
|
|
17510
17715
|
? 'account ' + accountId + ' removed (was ' + previous.directory + '). Its tickets will now park ' +
|
|
@@ -18012,6 +18217,8 @@ async function listenCommand(flags) {
|
|
|
18012
18217
|
activeSessionSignature = currentSignature;
|
|
18013
18218
|
}
|
|
18014
18219
|
|
|
18220
|
+
// Do not await the filesystem scan: a slow/unavailable volume must not stall socket upkeep.
|
|
18221
|
+
refreshAccountReposIfDue();
|
|
18015
18222
|
await runManagedWebsocketWatchdog();
|
|
18016
18223
|
updateRuntimeServiceState();
|
|
18017
18224
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -96,6 +96,13 @@ reference named after it.
|
|
|
96
96
|
"branch" or "verified"; say what was actually proven: Test pass, browser journey, corpus measurement,
|
|
97
97
|
sync mutation, or ticket lifecycle state. (`component-resolution.md`, `branch-variants.md`,
|
|
98
98
|
`development-loop.md`, `support-tickets.md`)
|
|
99
|
+
- **There are no environments to deploy to, so translate SDLC words before acting.** "Let me try it" means a
|
|
100
|
+
token link to your staged lane. "Put it in UAT" means a sync to the pre-production component branch, or
|
|
101
|
+
a link if UAT is a link on this account. "Push to prod" means the promotion loop. "Roll it back" means
|
|
102
|
+
`git revert` plus a sync, and it does not undo emails or external writes. People who simply sign in never
|
|
103
|
+
see staged code. Syncing a branch others use, every promotion gate, and a trunk edit on a release-train
|
|
104
|
+
account each need the person's go for that step. (the account repo's core platform guide → *How A Change Reaches People*,
|
|
105
|
+
`branch-variants.md`)
|
|
99
106
|
- **Embeddable verification is browser verification.** For page, widget, form, upload, or other
|
|
100
107
|
browser-facing changes, mint a URL with `remits-cli token` and drive it with `playwright-cli`.
|
|
101
108
|
Prefer this over `curl`: `curl` can prove an HTTP response or inspect an action payload, but it does
|
|
@@ -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
|
|
@@ -114,7 +114,7 @@ Agents should use this mental model before guessing.
|
|
|
114
114
|
|
|
115
115
|
The account repository index file is located at: `{{ACCOUNT_REPO_INDEX_PATH}}`
|
|
116
116
|
|
|
117
|
-
This JSON file is automatically maintained by `remits-cli` and tracks
|
|
117
|
+
This JSON file is automatically maintained by `remits-cli` and tracks known Remits account repositories on this machine. It is updated when a CLI command runs from an account repo directory, at service startup, and by background discovery every five minutes while the service runs. Background scans use the same configured roots, depth limit and exclusions as startup; they report newly discovered repos to the platform without restarting. Use `remits-cli agent repos --rescan` for immediate discovery. The file maps account IDs to their metadata:
|
|
118
118
|
|
|
119
119
|
```json
|
|
120
120
|
{
|
|
@@ -133,6 +133,8 @@ This JSON file is automatically maintained by `remits-cli` and tracks all known
|
|
|
133
133
|
- Resolve account names and IDs without making remote API calls
|
|
134
134
|
- Support cross-account workflows where an agent in one repo needs context from another
|
|
135
135
|
|
|
136
|
+
Manual directory selections survive rescans; competing checkouts are recorded as ambiguity. `agent repos --account-id <id> --forget` stores an opt-out without a directory, so scans do not silently restore it. Explicitly register a directory or run a CLI command from that repo to restore registration.
|
|
137
|
+
|
|
136
138
|
If the index file doesn't exist yet, run any `remits-cli` command from an account repo to bootstrap it.
|
|
137
139
|
|
|
138
140
|
## Local State Files
|
|
@@ -234,7 +234,7 @@ remits-cli ticket deliver --ticket ID --channel email --to a@x [--template T] [-
|
|
|
234
234
|
remits-cli ticket deliveries --ticket ID [--channel email] # what is queued to go out, and what already went
|
|
235
235
|
remits-cli ticket delivered --ticket ID --delivery-id ID | ticket delivery-failed --ticket ID --delivery-id ID --reason "..."
|
|
236
236
|
remits-cli ticket tag --ticket ID --tags a,b # make a cluster of near-identical tickets visible as one
|
|
237
|
-
remits-cli ticket artifact --ticket ID --type TYPE --label "..."
|
|
237
|
+
remits-cli ticket artifact --ticket ID --type TYPE --label "..." (--content "..." | --file PATH | --url U)
|
|
238
238
|
remits-cli ticket participant --ticket ID --email E [--role watcher|requester|agent]
|
|
239
239
|
remits-cli ticket field --ticket ID --key K --value V # the ORGANIZATION's own field, outside the planning slots
|
|
240
240
|
remits-cli ticket reclaim [--ticket ID | --account-id ID] [--stale-hours 24] [--apply] # OPERATOR: take back an agent's abandoned ticket
|
|
@@ -808,3 +808,11 @@ Some users want to review every change before staging. Others want you to move f
|
|
|
808
808
|
- If they say "you don't need to ask me" or "stop asking" — remember this preference and work autonomously through the full loop.
|
|
809
809
|
|
|
810
810
|
The default should be: make the change, stage it, verify it, and present the results. Only block on the user when you're genuinely unsure about intent.
|
|
811
|
+
|
|
812
|
+
**Four steps always need the person's go for that step, whatever they said earlier:**
|
|
813
|
+
- syncing a component branch that a pre-production or client account uses;
|
|
814
|
+
- each promotion gate (`features/subscriber-branch-promotions.md` → *The three approval gates*);
|
|
815
|
+
- unsubscribing an account or retiring a branch;
|
|
816
|
+
- editing trunk on an account that runs a release-train branch.
|
|
817
|
+
|
|
818
|
+
A general "just do it" covers staging and verifying, not these. Each one changes what live accounts run.
|
|
@@ -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
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
- [Component branches](#component-branches)
|
|
33
33
|
- [`mcp_cache`](#mcp_cache)
|
|
34
34
|
- [`mcp_sql_query`](#mcp_sql_query)
|
|
35
|
+
- [`mcp_usage`](#mcp_usage)
|
|
35
36
|
- [`mcp_index_search`](#mcp_index_search)
|
|
36
37
|
- [`mcp_get_guide`](#mcp_get_guide)
|
|
37
38
|
- [`mcp_test_fixture`](#mcp_test_fixture)
|
|
@@ -931,6 +932,30 @@ SELECT u.id, u.username, u.enabled FROM user u
|
|
|
931
932
|
Prefer the purpose-built tools when one fits — they apply account scoping, data-mode segmentation, and
|
|
932
933
|
resolution awareness that raw SQL does not. Reach for SQL when nothing else models the question.
|
|
933
934
|
|
|
935
|
+
### `mcp_usage`
|
|
936
|
+
Administrator-only usage reports and configuration through the platform's `usageControl` API. The
|
|
937
|
+
System Account owns this global tool; deploy the platform API before staging or syncing its wrapper.
|
|
938
|
+
|
|
939
|
+
| Parameter | Description |
|
|
940
|
+
|---|---|
|
|
941
|
+
| `operation` | `status` (default), `report`, `gaps`, `savePrice`, `replacePrice`, `endPrice`, `saveBillingMap`, `deleteBillingMap`, `saveManualCharge`, `deleteManualCharge`, `configure`, `runSettle`, `runBilling`, `flushMeter` |
|
|
942
|
+
| `query` | `usage(...)` selectors for reports/gaps: dates, accountId, includeDescendants, lanes, includeSuites, grouping. Omit accountId for platform scope. |
|
|
943
|
+
| `values` | Rate, mapping, charge or settings fields; status lists supported units and allowed settings. `replacePrice` requires the old id and full successor rate. |
|
|
944
|
+
| `apply` | `false` by default: mutations return their normalized plan. `true` applies the authorized change. Configuration is platform-wide, independently of the test/prod data selector. |
|
|
945
|
+
|
|
946
|
+
Read `status.pricingNotes` first. Rates start at future UTC hour boundaries; overlapping or historical
|
|
947
|
+
changes are refused. Use `replacePrice` for an atomic rate change. Do not price a storage byte counter
|
|
948
|
+
as byte-months or wall time as billed CPU time. `gaps` returns account/provider/model grouping so exact
|
|
949
|
+
missing rate keys are visible. Shared platform overhead is excluded from account hierarchies.
|
|
950
|
+
Provider invoices and estimates remain separate when filters do not match invoice scope.
|
|
951
|
+
|
|
952
|
+
```bash
|
|
953
|
+
remits-cli tool --name mcp_usage --input '{"operation":"status"}'
|
|
954
|
+
remits-cli tool --name mcp_usage --input '{"operation":"report","query":{"accountId":41,"includeDescendants":true,"from":"2026-10-01"}}'
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
The full field contract and examples are in `features/usage-and-cost.md` → Configuring usage through MCP.
|
|
958
|
+
|
|
934
959
|
### `mcp_index_search`
|
|
935
960
|
Query and **diagnose** the Vertex AI Search index through the platform's Vertex DSL. Use it to check what a
|
|
936
961
|
RAG/search-backed component actually retrieves before blaming the component.
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
| Unsure whether a run used staged vs DB source | Query `mcp_system_logs` for `Using Cached BCD` — `Signature: cli:<hash>` = staged, `version:<N>:<sourceHash12>` = DB. CLI/MCP tool results also report `componentSource` and `componentSignature` when available. |
|
|
63
63
|
| Service already running | Run `remits-cli status` to get the dashboard URL, or `remits-cli stop` before restarting. |
|
|
64
64
|
| Control center URL unknown | Read `~/.remits-cli/service-state.json` or run `remits-cli status` |
|
|
65
|
-
| Dashboard missing repos |
|
|
65
|
+
| Dashboard or GitHub menu missing local repos | The running service rediscovers repos every five minutes. Use `remits-cli agent repos --rescan` or the control center rescan action for immediate discovery; allow the next presence update (normally within 30 seconds). Check scan roots if still absent. A forgotten account needs explicit registration with `agent repos --account-id <id> --directory <path>`. |
|
|
66
66
|
| Agent dispatch to wrong directory | Check `~/.remits-cli/account-repos.json` has the correct directory and that you resolved the account type correctly. A `CLIENT` ticket may still belong to a parent `PLATFORM` or `PRODUCT` repo for code changes. |
|
|
67
67
|
|
|
68
68
|
### When Something Doesn't Work as Expected
|