crewbus 10.0.2 → 10.0.3
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/AGENTS.template.md +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +9 -2
- package/bin/crewbus.js +145 -55
- package/bin/lib/groups.js +4 -1
- package/bin/lib/identity.js +22 -26
- package/bin/lib/mail.js +5 -24
- package/bin/lib/spawn.js +95 -29
- package/bin/lib/store.js +3 -1
- package/bin/lib/web.js +78 -13
- package/docs/GROUPS.md +102 -77
- package/docs/TROUBLESHOOTING.md +26 -25
- package/docs/VERSION_SKEW.md +1 -1
- package/documentation.html +3 -3
- package/index.html +5 -5
- package/opencode/plugins/dm-watch.js +38 -18
- package/opencode/tools/dm-send.js +14 -6
- package/package.json +1 -1
package/AGENTS.template.md
CHANGED
|
@@ -13,7 +13,7 @@ appends the right subsection automatically.
|
|
|
13
13
|
You coordinate with other AI agents by messaging them directly — like Slack,
|
|
14
14
|
minimal structure, figure it out yourselves.
|
|
15
15
|
|
|
16
|
-
The CLI is run as (after `npm i -g
|
|
16
|
+
The CLI is run as (after `npm i -g crewbus`, just `crewbus`; otherwise the full
|
|
17
17
|
path `node <this-checkout>/bin/crewbus.js` — `init` writes the real path
|
|
18
18
|
into the project's `AGENTS.md` automatically, so prefer that copy):
|
|
19
19
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Package is now published as unscoped `crewbus` (v10.0.2): `npm i -g crewbus`.
|
|
6
|
+
|
|
7
|
+
## 10.0.3 (2026-10-10)
|
|
8
|
+
|
|
9
|
+
PATCH RELEASE: Worker-record integrity, truthful presence, and multi-group collaboration in spawn briefs. No board-schema change (`board.json` stays v2); all prompt additions are backward-compatible (absent unless the spawn carries group/compute context).
|
|
10
|
+
|
|
11
|
+
- **Spawn briefs carry a Crew: roster block**:
|
|
12
|
+
- Every `spawn` brief now names the worker's group(s) + members + lane, the other groups + members, and their group channels (`grp-<group>`).
|
|
13
|
+
- Ships the peer-DM norm with an explicit trigger (ask the group that owns a thing before asking the lead; lead stays for approvals/scope only), the read-channels/write-DMs distinction (channel-post is lead|admin-only), and a `crewbus agents` + `crewbus group list` boot habit.
|
|
14
|
+
- Documented as cross-group collaboration norms in `docs/GROUPS.md`.
|
|
15
|
+
- **Agent-doc merge canonicalization**:
|
|
16
|
+
- New `mergeAgentDoc(prev, patch)` in `bin/lib/identity.js`; `touchAgent`, CLI `heartbeat`, and `register`'s mint path route through it.
|
|
17
|
+
- The `dm-send` tool and `dm-watch` plugin upserts merge over prev instead of enumerating — fixes spawned workers losing `spawnedPid`/`spawnedBy`/`briefId`/`role` (and with it: vanishing from `spawn-status`, unkillable workers, and silent role promotion) the moment they report.
|
|
18
|
+
- `syncWorkerSession` preserves binding fields (`promptPath`, model, cwd, cmd, respawn counts), which `respawn` needs.
|
|
19
|
+
- **Truthful presence + session hygiene**:
|
|
20
|
+
- `boardSnapshot` gates pid-bearing agents on verified process liveness; pid-less agents keep lastSeen freshness.
|
|
21
|
+
- The watcher TTL-expires session mappings (30m without a confirmed delivery or re-registration) instead of heartbeating dead sessions forever.
|
|
22
|
+
- **Observability**:
|
|
23
|
+
- Boot-metadata first line in every worker log; `workerStatus` surfaces `logSize`/`logMtimeMs`/`logIdleMs`.
|
|
24
|
+
- `doctor` gains a read-only dead-man scan (exited without reply, past deadline, alive-but-quiet) — informational, never FAIL.
|
|
25
|
+
- Dashboard shows data age + STALE banner, relative timestamps, alive-gated kill buttons; `spawn-status --all` lists pid-wiped workers as "no pid recorded" instead of dropping them.
|
|
26
|
+
- **Windows ergonomics**: group lists split on whitespace too (unquoted `--add a,b,c` arriving space-rejoined now parses as several members); PowerShell quoting note in `docs/TROUBLESHOOTING.md`.
|
|
27
|
+
|
|
3
28
|
## 10.0.2 (2026-10-09)
|
|
4
29
|
|
|
5
30
|
PATCH RELEASE: Target workspace CWD selector & persistence in Launch Studio, popover clarity, eliminated upload prompt, and Desktop v0.1.16 companion release.
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# crewbus v10.0.
|
|
1
|
+
# crewbus v10.0.3 — DM bus
|
|
2
2
|
|
|
3
3
|
[](https://github.com/eamonpluto/crewbus/actions/workflows/ci.yml)
|
|
4
4
|
|
|
@@ -23,6 +23,13 @@ node $REPO/bin/crewbus.js <command>
|
|
|
23
23
|
|
|
24
24
|
Or install globally:
|
|
25
25
|
|
|
26
|
+
```sh
|
|
27
|
+
npm i -g crewbus
|
|
28
|
+
crewbus --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or from a local checkout (dev alternative):
|
|
32
|
+
|
|
26
33
|
```sh
|
|
27
34
|
cd $REPO
|
|
28
35
|
npm i -g .
|
|
@@ -48,7 +55,7 @@ env overrides below.)
|
|
|
48
55
|
Layout: `board.json`, `agents/<name>.json`, `dm/<recipient>/<id>.json`,
|
|
49
56
|
`delivered/<recipient>/<id>.json` (push markers, written by the plugin).
|
|
50
57
|
|
|
51
|
-
Versions: `package.json` (currently 10.0.
|
|
58
|
+
Versions: `package.json` (currently 10.0.3) is the source of truth for the
|
|
52
59
|
release version. `board.json`'s `version: 2` is the **board schema version**
|
|
53
60
|
(a different number on purpose — do not "align" them). New to the project?
|
|
54
61
|
Start with `docs/QUICKSTART.md` (5 minutes).
|
package/bin/crewbus.js
CHANGED
|
@@ -42,11 +42,11 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
42
42
|
// at top level so the import graph (store <- everyone; identity <- sync/spawn/
|
|
43
43
|
// mail/groups/channels/web/relay/export) has no evaluation-order hazards.
|
|
44
44
|
import { BOARD_VERSION, BROADCAST_AFTER, MAX_BODY_CHARS, MAX_RECIPIENTS, MAX_SPAWN, VALUE_FLAGS, boardDir, chmodAgentFile, cleanArtifact, cleanBranchPrefix, cleanChannelName, cleanGroupName, cleanPriority, cleanReply, cleanSenderType, cleanSubject, cleanWebName, dirs, ensureBoard, fail, findBoardUpward, getFlag, gitRevForBoard, listJson, newId, optionalAgent, parseDuration, readBoardMeta, readJson, refuseDriveRootBoard, requireBoard, resolveAgent, restArgs, sanitizeName, writeBoardMeta, writeExclusiveJson, writeJson, nextHlc, stampSyncDoc, hlcCompare, SEND_RATE_CAP, SEND_RATE_WINDOW_MS, MAX_FWD_DEPTH, DEDUPE_WINDOW_MS, webErr } from "./lib/store.js";
|
|
45
|
-
import { VALID_ROLES, agentTokenMatches, assertMintWon, authorize, authorizeCheck, checkToken, cleanRole, countAgentRecords, defaultRoleForNew, ensureSender, hashToken, isHashRevoked, mergeSyncedAgent, mintToken, newSalt, readAgent, readBoardAcl, resolveToken, revokedPathForHash, roleOfRecord, sanitizeAgentForSync, stripAgentSecrets, timingSafeEqualStr, touchAgent, writeAgentFile, writeBoardAcl, isBoardFrozen } from "./lib/identity.js";
|
|
45
|
+
import { VALID_ROLES, agentTokenMatches, assertMintWon, authorize, authorizeCheck, checkToken, cleanRole, countAgentRecords, defaultRoleForNew, ensureSender, hashToken, isHashRevoked, mergeAgentDoc, mergeSyncedAgent, mintToken, newSalt, readAgent, readBoardAcl, resolveToken, revokedPathForHash, roleOfRecord, sanitizeAgentForSync, stripAgentSecrets, timingSafeEqualStr, touchAgent, writeAgentFile, writeBoardAcl, isBoardFrozen } from "./lib/identity.js";
|
|
46
46
|
import { saveTokenFile } from "./lib/tokenfile.js";
|
|
47
47
|
import { CLIENT_TLS, SYNC_LWW, cleanSyncRel, clientInsecureFromArgs, crewSurvey, httpJson, readPemFlag, readSyncState, readTombstones, relayAuthEntries, relayCredFor, relayCredHeaders, setupClientTls, splitByWeight, syncRound, syncWalk, warnInsecureOnce, writeSyncState, writeTombstone, SYNC_SUBS, SYNC_UNION, RELAY_CAPS, SUB_CAP, SUB_CAP_NOTE, tombstoneIdForRel, readSyncDoc } from "./lib/sync.js";
|
|
48
48
|
import { devicePath, isLoopbackHost, newDeviceCred, newPairToken, pairingPath, parseDeviceCred, readDevice, readRelayState, relaySecretFromArgs, remoteSpawn, requireRelayClientCert, requireRelaySecret, requireScope, tryAcquireFence, verifyOidcJwt, writeRelayState, relayStatePath, bearerFromReq, getOidcConfig, getOidcJwks, b64urlDecode, b64urlJson, jwsRawToDer, oidcGetJson, OIDC_SKEW_SEC, OIDC_JWKS_TTL_MS, OIDC_ALG_HASH, OIDC_EC_SIZE, deviceFromReq } from "./lib/relay.js";
|
|
49
|
-
import { assertGitCheckout, bootRespawnedWorker, bootWorker, buildRespawnBrief, buildRespawnTarget, buildSpawnPrompt, buildSpawnTarget, formatSpawnCmd, isPidStale, killWorkers, maybeIsolate, parseAllowEnv, pidAlive, provisionBranch, provisionWorktree, readWorkerSession, requireAutoConfirm, sandboxPresent, scrubChildEnv, syncWorkerSession, workerStatus, worktreeStamp, defaultMaxTurnsFor } from "./lib/spawn.js";
|
|
49
|
+
import { assertGitCheckout, bootRespawnedWorker, bootWorker, buildCrewContext, buildRespawnBrief, buildRespawnTarget, buildSpawnPrompt, buildSpawnTarget, formatSpawnCmd, isPidStale, killWorkers, maybeIsolate, parseAllowEnv, pidAlive, provisionBranch, provisionWorktree, readWorkerSession, requireAutoConfirm, sandboxPresent, scrubChildEnv, syncWorkerSession, workerStatus, worktreeStamp, defaultMaxTurnsFor, ensureBoardCliShim, installCliToUserPath } from "./lib/spawn.js";
|
|
50
50
|
import { appendChainRecord, auditHmacKey, boardTotalBytes, chainFilePath, countChannels, dirSize, doExportToFile, enforceAgentQuota, enforceBytesQuota, enforceChannelQuota, holdActive, holdDocPath, holdRefusal, parseQuotaBytes, parseQuotaCount, readBackupInner, readBoardQuotas, readChainRecords, readHold, readSnapshotSchedule, resolveBackupKeyMaterial, setAuditForward, snapshotStamp, startAuditForwarder, verifyChainRecords, collectBoardFiles, encryptBackupPayload, decryptBackupPayload, rawKeyFromMaterial, deriveBackupKey, toAuditExport, spoolAuditEvent, postAuditEvent, auditSpoolDir, drainAuditSpool, enqueueAuditForward, signAuditRecord, chainRecordHash, AUDIT_FORWARD_URL, AUDIT_FORWARD_KEY } from "./lib/export.js";
|
|
51
51
|
import { ackedIds, checkSendRateLimit, deliverDMs, enforceMaxChars, filterDigest, findDuplicateSend, findMessageById, heartbeat, isVerified, loadManifest, manifestPath, msgTimeMs, parseRecipients, printDigest, printMsg, readDMs, readVisible, recordBroadcastManifest, requireFanoutConfirm, resolveFwdDepth, runVerifier, verifyMessageSig, isHigh, signMessage, untrustedEnvelope, relTime, rateFilePath, broadcastTargets, readBroadcastsFor, formatTo, msgHeader, readAckMarker, splitCommand, readRecipientsFile } from "./lib/mail.js";
|
|
52
52
|
import { batchReplyIds, collectBatch, contributingGroups, expandGroups, expandGroupsOrFail, gatherTelemetry, groupTelemetryData, heuristicSenderType, readGroup, ensureGroupCreatedAt, readResultRecord, writeResultRecord, findFirstVerifiedReply } from "./lib/groups.js";
|
|
@@ -591,22 +591,30 @@ export default tool({
|
|
|
591
591
|
const tokenHint = minted.created ? \` identity '\${from}' claimed, token \${minted.token} (set CREWBUS_TOKEN=\${minted.token})\` : "";
|
|
592
592
|
const rev = gitRev(root);
|
|
593
593
|
const at = new Date().toISOString();
|
|
594
|
-
// upsert sender with live session routing for the watcher plugin
|
|
594
|
+
// upsert sender with live session routing for the watcher plugin.
|
|
595
|
+
// Merge over prev, never enumerate: a hand-built record here once
|
|
596
|
+
// wiped spawn bookkeeping (spawnedPid/By, briefId) and roles off
|
|
597
|
+
// every worker that reported through this tool.
|
|
595
598
|
const ap = path.join(root, "agents", from + ".json");
|
|
596
599
|
let prev = null;
|
|
597
600
|
try {
|
|
598
601
|
prev = JSON.parse(fs.readFileSync(ap, "utf8"));
|
|
599
602
|
} catch {}
|
|
600
|
-
|
|
603
|
+
const merged = { ...(prev && typeof prev === "object" ? prev : {}) };
|
|
604
|
+
for (const [k, v] of Object.entries({
|
|
601
605
|
name: from,
|
|
602
606
|
firstSeen: (prev && prev.firstSeen) || new Date().toISOString(),
|
|
603
607
|
lastSeen: new Date().toISOString(),
|
|
604
608
|
sessionId: (context && context.sessionID) || (prev && prev.sessionId) || undefined,
|
|
605
609
|
lastDir: context.worktree || context.directory || undefined,
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
+
})) {
|
|
611
|
+
if (v === undefined) delete merged[k];
|
|
612
|
+
else merged[k] = v;
|
|
613
|
+
}
|
|
614
|
+
for (const k of Object.keys(merged)) {
|
|
615
|
+
if (merged[k] === undefined) delete merged[k];
|
|
616
|
+
}
|
|
617
|
+
writeJsonAtomic(ap, merged);
|
|
610
618
|
const isAll = recipients.length === 1 && recipients[0] === "@all";
|
|
611
619
|
const mirrorChannels = (batch) => {
|
|
612
620
|
if (args.also_channel !== true) return "";
|
|
@@ -731,6 +739,13 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
731
739
|
const deliveredDir = path.join(root, "delivered");
|
|
732
740
|
|
|
733
741
|
const agentToSession = new Map(); // agent -> sessionID
|
|
742
|
+
const sessionMeta = new Map(); // agent -> { sessionID, confirmedAt }
|
|
743
|
+
// A mapped session is proof of life only while fresh: entries age out
|
|
744
|
+
// after TTL without a confirmed delivery or re-registration, so dead
|
|
745
|
+
// workers stop being heartbeated (and stop reading "active") instead
|
|
746
|
+
// of lingering forever. Their mail then waits for pull until the live
|
|
747
|
+
// session re-registers — the same fallback as a refused push.
|
|
748
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
734
749
|
const processed = new Set(); // "<agent>/<msgId>"
|
|
735
750
|
const lastBeat = new Map(); // agent -> epoch ms of last presence write
|
|
736
751
|
|
|
@@ -763,7 +778,14 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
763
778
|
for (const f of files) {
|
|
764
779
|
if (!f.endsWith(".json")) continue;
|
|
765
780
|
const doc = readJsonSafe(path.join(agentsDir, f));
|
|
766
|
-
if (doc && doc.name && doc.sessionId)
|
|
781
|
+
if (doc && doc.name && doc.sessionId) {
|
|
782
|
+
agentToSession.set(doc.name, doc.sessionId);
|
|
783
|
+
const meta = sessionMeta.get(doc.name);
|
|
784
|
+
if (!meta || meta.sessionID !== doc.sessionId) {
|
|
785
|
+
// (Re)registration: a new session id is fresh proof of life.
|
|
786
|
+
sessionMeta.set(doc.name, { sessionID: doc.sessionId, confirmedAt: Date.now() });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
767
789
|
}
|
|
768
790
|
}
|
|
769
791
|
|
|
@@ -783,23 +805,15 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
783
805
|
if (prev && prev.lastSeen && now - new Date(prev.lastSeen).getTime() < 60000) return;
|
|
784
806
|
const at = new Date().toISOString();
|
|
785
807
|
try {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
briefId: (prev && prev.briefId) || undefined,
|
|
796
|
-
spawnedWorktree: (prev && prev.spawnedWorktree) || undefined,
|
|
797
|
-
spawnedBranch: (prev && prev.spawnedBranch) || undefined,
|
|
798
|
-
spawnedLifetime: (prev && prev.spawnedLifetime) || undefined,
|
|
799
|
-
token: (prev && prev.token) || undefined,
|
|
800
|
-
tokenHash: (prev && prev.tokenHash) || undefined,
|
|
801
|
-
salt: (prev && prev.salt) || undefined,
|
|
802
|
-
}, null, 2) + "\\n");
|
|
808
|
+
// Merge over prev, never enumerate (see mergeAgentDoc in
|
|
809
|
+
// bin/lib/identity.js — this file is standalone so the merge is
|
|
810
|
+
// inlined): a heartbeat must not drop fields it doesn't own.
|
|
811
|
+
const merged = { ...(prev && typeof prev === "object" ? prev : {}) };
|
|
812
|
+
for (const [k, v] of Object.entries({ name: agent, firstSeen: (prev && prev.firstSeen) || at, lastSeen: at })) {
|
|
813
|
+
if (v === undefined) delete merged[k];
|
|
814
|
+
else merged[k] = v;
|
|
815
|
+
}
|
|
816
|
+
fs.writeFileSync(p, JSON.stringify(merged, null, 2) + "\\n");
|
|
803
817
|
} catch {}
|
|
804
818
|
}
|
|
805
819
|
|
|
@@ -994,6 +1008,10 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
994
1008
|
await client.session.prompt({ path: { id: sessionID }, body: { parts: [{ type: "text", text }] } });
|
|
995
1009
|
}
|
|
996
1010
|
advanceCursor(agent, msg.id);
|
|
1011
|
+
// A completed push is the only proof of life this watcher can
|
|
1012
|
+
// observe directly — refresh the mapping TTL so live sessions
|
|
1013
|
+
// stay pushable across idle stretches.
|
|
1014
|
+
sessionMeta.set(agent, { sessionID, confirmedAt: Date.now() });
|
|
997
1015
|
} catch (e) {
|
|
998
1016
|
const detail = e && e.message ? e.message : String(e);
|
|
999
1017
|
releaseClaim(agent, msg.id);
|
|
@@ -1026,6 +1044,16 @@ export const DmWatchPlugin = async ({ client, directory }) => {
|
|
|
1026
1044
|
for (const agent of agents) {
|
|
1027
1045
|
const sessionID = agentToSession.get(agent);
|
|
1028
1046
|
if (!sessionID) continue; // nobody live for this name — mail waits for pull
|
|
1047
|
+
const meta = sessionMeta.get(agent);
|
|
1048
|
+
if (!meta || meta.sessionID !== sessionID || Date.now() - meta.confirmedAt > SESSION_TTL_MS) {
|
|
1049
|
+
// Stale mapping: no confirmed delivery and no re-registration
|
|
1050
|
+
// within TTL. Drop it (clears the doc's sessionId too) so the
|
|
1051
|
+
// agent stops reading "active" and its mail waits for pull until
|
|
1052
|
+
// the live session re-registers.
|
|
1053
|
+
invalidateSession(agent, sessionID);
|
|
1054
|
+
sessionMeta.delete(agent);
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1029
1057
|
heartbeat(agent);
|
|
1030
1058
|
let files = [];
|
|
1031
1059
|
try {
|
|
@@ -2023,24 +2051,20 @@ function cmdRegister(args) {
|
|
|
2023
2051
|
// here (else anyone could self-mint admin). Warn, keep the default.
|
|
2024
2052
|
process.stderr.write(`crewbus: warning: --role ignored (only an admin can grant roles; ask an admin to run register --from <admin> --for ${agent} --role ${String(roleWant).trim().toLowerCase()})\n`);
|
|
2025
2053
|
}
|
|
2026
|
-
const record = {
|
|
2054
|
+
const record = mergeAgentDoc(prev, {
|
|
2027
2055
|
name: agent,
|
|
2028
2056
|
firstSeen: (prev && prev.firstSeen) || now,
|
|
2029
2057
|
lastSeen: now,
|
|
2030
2058
|
sessionId: session || (prev && prev.sessionId) || undefined,
|
|
2031
2059
|
lastDir: process.cwd(),
|
|
2032
|
-
spawnedPid: (prev && prev.spawnedPid) || undefined,
|
|
2033
|
-
spawnedAt: (prev && prev.spawnedAt) || undefined,
|
|
2034
|
-
spawnedBy: (prev && prev.spawnedBy) || undefined,
|
|
2035
|
-
briefId: (prev && prev.briefId) || undefined,
|
|
2036
2060
|
tokenHash: hashToken(fresh, salt),
|
|
2037
2061
|
salt,
|
|
2038
|
-
expiresAt:
|
|
2062
|
+
expiresAt: expiresRaw !== undefined ? expiresAt : (serviceFlag ? null : ((prev && prev.expiresAt !== undefined) ? prev.expiresAt : undefined)),
|
|
2039
2063
|
service: serviceFlag ? true : ((prev && prev.service !== undefined) ? prev.service : undefined),
|
|
2040
2064
|
revokedAt: undefined,
|
|
2041
2065
|
role: newRole,
|
|
2042
2066
|
v, hlc,
|
|
2043
|
-
};
|
|
2067
|
+
});
|
|
2044
2068
|
if (!prev) {
|
|
2045
2069
|
// Fresh first-claim races (parallel registers): exactly one may win,
|
|
2046
2070
|
// via atomic exclusive create. Losers fail loudly like a claimed name.
|
|
@@ -2472,7 +2496,11 @@ function cmdGroup(args) {
|
|
|
2472
2496
|
const adds = [];
|
|
2473
2497
|
const addRaw = getFlag(rest, "--add");
|
|
2474
2498
|
if (addRaw) {
|
|
2475
|
-
|
|
2499
|
+
// Split on commas AND whitespace: PowerShell array-splats an unquoted
|
|
2500
|
+
// --add a,b,c into separate argv items that can rejoin with spaces
|
|
2501
|
+
// (npm shims), so "cam-1 cam-2 cam-3" must parse as three names, not
|
|
2502
|
+
// one joined member.
|
|
2503
|
+
for (const part of String(addRaw).split(/[,\s]+/)) {
|
|
2476
2504
|
if (part.trim() === "") continue;
|
|
2477
2505
|
const clean = sanitizeName(part, "recipient");
|
|
2478
2506
|
if (!adds.includes(clean)) adds.push(clean);
|
|
@@ -2840,6 +2868,17 @@ function cmdSpawn(args) {
|
|
|
2840
2868
|
const toGroupRaw = getFlag(args, "--to-group");
|
|
2841
2869
|
if (alsoChannel && (!toGroupRaw || !String(toGroupRaw).trim())) fail("--also-channel needs --to-group <g,...> (it mirrors the brief into each group's channel)");
|
|
2842
2870
|
const groupNames = String(toGroupRaw || "").split(",").map((s) => String(s).trim().toLowerCase().replace(/[^a-z0-9_.-]/g, "-").slice(0, 40)).filter(Boolean);
|
|
2871
|
+
// Crew roster for the prompt block (see buildCrewContext in lib/spawn.js):
|
|
2872
|
+
// every group on the board with its members, so each worker learns the
|
|
2873
|
+
// map — its own group, the other groups, and the peer-DM norms — at
|
|
2874
|
+
// boot instead of discovering (or never discovering) the board alone.
|
|
2875
|
+
const rosterGroups = [];
|
|
2876
|
+
for (const e of listJson(d.groups)) {
|
|
2877
|
+
const g = e && e.data;
|
|
2878
|
+
if (g && g.name && Array.isArray(g.members)) rosterGroups.push({ name: g.name, members: g.members.slice() });
|
|
2879
|
+
}
|
|
2880
|
+
rosterGroups.sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
|
2881
|
+
const crewFor = (to) => buildCrewContext({ name: to, from, subject, groups: rosterGroups, myGroups: rosterGroups.filter((g) => g.members.includes(to)).map((g) => g.name) });
|
|
2843
2882
|
const model = getFlag(args, "--model");
|
|
2844
2883
|
const auto = args.includes("--auto");
|
|
2845
2884
|
requireAutoConfirm(args);
|
|
@@ -2895,12 +2934,12 @@ function cmdSpawn(args) {
|
|
|
2895
2934
|
const driver = harnesses[i % harnesses.length];
|
|
2896
2935
|
const itemMaxTurns = maxTurnsNum !== undefined ? maxTurnsNum : ((driver === "claude" || driver === "grok") ? 50 : undefined);
|
|
2897
2936
|
const itemSpawnOpts = { ...spawnOpts, harness: driver, maxTurns: itemMaxTurns, allowTools: driver === "claude" ? allowTools : undefined };
|
|
2898
|
-
const previewPrompt = buildSpawnPrompt({ name: to, from, subject, body: body.trim(), replyId: previewBatch || "msg-<id>", rev, cwd, root });
|
|
2937
|
+
const previewPrompt = buildSpawnPrompt({ name: to, from, subject, body: body.trim(), replyId: previewBatch || "msg-<id>", rev, cwd, root, crew: crewFor(to) });
|
|
2899
2938
|
const t = buildSpawnTarget({ ...itemSpawnOpts, name: to, promptPath: `<logs>/${to}-<stamp>.prompt.md`, prompt: previewPrompt });
|
|
2900
2939
|
const wtNote = worktreePrefix !== undefined ? ` worktree ../${to}-<stamp> (branch ${worktreePrefix}/${to}-<stamp>)` : branchPrefix !== undefined ? ` branch ${branchPrefix}/${to}-<stamp>` : "";
|
|
2901
2940
|
console.log(`would spawn ${to} [${driver}] (${lifetime}) cwd ${cwd}${wtNote} cmd: ${formatSpawnCmd(t)} [board ${d.root}]`);
|
|
2902
2941
|
});
|
|
2903
|
-
console.log(`--- prompt (first worker) ---\n${buildSpawnPrompt({ name: recipients[0], from, subject, body: body.trim(), replyId: previewBatch || "msg-<id>", rev, cwd, root })}`);
|
|
2942
|
+
console.log(`--- prompt (first worker) ---\n${buildSpawnPrompt({ name: recipients[0], from, subject, body: body.trim(), replyId: previewBatch || "msg-<id>", rev, cwd, root, crew: crewFor(recipients[0]) })}`);
|
|
2904
2943
|
return;
|
|
2905
2944
|
}
|
|
2906
2945
|
// Direct (N-copy) path is forced here (every worker needs its own message
|
|
@@ -2934,7 +2973,7 @@ function cmdSpawn(args) {
|
|
|
2934
2973
|
} else if (branchPrefix !== undefined) {
|
|
2935
2974
|
workerBranch = provisionBranch(cwd, branchPrefix, to).branch;
|
|
2936
2975
|
}
|
|
2937
|
-
const r = bootWorker(d, { ...itemSpawnOpts, cwd: workerCwd }, { to, id, from, subject, body: body.trim(), rev, logDir, budgetTokens, budgetMinutes, deadlineAt, spawnedWorktree: workerWorktree, spawnedBranch: workerBranch, spawnedLifetime: lifetime });
|
|
2976
|
+
const r = bootWorker(d, { ...itemSpawnOpts, cwd: workerCwd }, { to, id, from, subject, body: body.trim(), rev, logDir, budgetTokens, budgetMinutes, deadlineAt, spawnedWorktree: workerWorktree, spawnedBranch: workerBranch, spawnedLifetime: lifetime, crew: crewFor(to) });
|
|
2938
2977
|
const where = workerWorktree ? ` worktree ${workerWorktree}` : workerBranch ? ` branch ${workerBranch}` : "";
|
|
2939
2978
|
console.log(`spawned ${to} pid ${r.pid} [${driver}] (${lifetime}) log ${r.logPath} reply ${id}${where} [board ${d.root}]`);
|
|
2940
2979
|
} catch (e) {
|
|
@@ -3366,9 +3405,12 @@ function cmdSpawnStatus(args) {
|
|
|
3366
3405
|
const json = args.includes("--json");
|
|
3367
3406
|
let names;
|
|
3368
3407
|
if (args.includes("--all")) {
|
|
3408
|
+
// Pid-bearing workers plus bookkept ones whose pid record was lost
|
|
3409
|
+
// (e.g. an older dm-send wiped it): the latter still report via
|
|
3410
|
+
// workerStatus as "no pid recorded" instead of vanishing silently.
|
|
3369
3411
|
names = listJson(d.agents)
|
|
3370
3412
|
.map((e) => e.data)
|
|
3371
|
-
.filter((x) => x && x.name && typeof x.spawnedPid === "number")
|
|
3413
|
+
.filter((x) => x && x.name && (typeof x.spawnedPid === "number" || x.spawnedBy || x.briefId))
|
|
3372
3414
|
.map((x) => x.name)
|
|
3373
3415
|
.sort();
|
|
3374
3416
|
} else {
|
|
@@ -4303,14 +4345,43 @@ function cmdDoctor(args) {
|
|
|
4303
4345
|
else info(`not a git checkout (sends omit rev)`);
|
|
4304
4346
|
|
|
4305
4347
|
let ids = parseHarnessFlag(args);
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4348
|
+
if (ids.length === 0) {
|
|
4349
|
+
if (meta && Array.isArray(meta.harnesses) && meta.harnesses.length > 0) ids = meta.harnesses.filter((h) => HARNESSES.includes(h));
|
|
4350
|
+
else {
|
|
4351
|
+
ids = detectHarnesses(cwd);
|
|
4352
|
+
if (ids.length === 0) ids = ["generic"];
|
|
4353
|
+
}
|
|
4311
4354
|
}
|
|
4312
|
-
|
|
4313
|
-
|
|
4355
|
+
// Dead-man scan (read-only, board-local only — no cwd needed): the
|
|
4356
|
+
// events a lead cannot see without polling — workers that exited
|
|
4357
|
+
// without replying, passed deadlines, or went quiet while their pid
|
|
4358
|
+
// is still alive. Informational only — never FAIL. Placed before the
|
|
4359
|
+
// non-local early return so it runs for --board boards too.
|
|
4360
|
+
try {
|
|
4361
|
+
const dd = ensureBoard(root);
|
|
4362
|
+
const found = [];
|
|
4363
|
+
for (const e of listJson(path.join(root, "agents"))) {
|
|
4364
|
+
const a = e && e.data;
|
|
4365
|
+
if (!a || !a.name || typeof a.spawnedPid !== "number") continue;
|
|
4366
|
+
let st = null;
|
|
4367
|
+
try {
|
|
4368
|
+
st = workerStatus(dd, a.name, 1);
|
|
4369
|
+
} catch {
|
|
4370
|
+
continue;
|
|
4371
|
+
}
|
|
4372
|
+
if (!st || !st.known) continue;
|
|
4373
|
+
if (st.alive === false && a.briefId && !st.reply) {
|
|
4374
|
+
found.push(`${a.name} exited with no reply to its brief — check its log (${st.logPath || "no log yet"})`);
|
|
4375
|
+
} else if (st.budget && st.budget.exceeded) {
|
|
4376
|
+
found.push(`${a.name} is past its deadline/budget — see spawn-status --to ${a.name}`);
|
|
4377
|
+
} else if (st.alive === true && st.logIdleMs !== null && st.logIdleMs > 15 * 60 * 1000) {
|
|
4378
|
+
found.push(`${a.name} alive but no log output for ${Math.round(st.logIdleMs / 60000)}m — possibly spinning; see spawn-status --to ${a.name}`);
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
for (const f of found.slice(0, 5)) info(`dead-man: ${f}`);
|
|
4382
|
+
if (found.length > 5) info(`dead-man: …and ${found.length - 5} more — see spawn-status --all`);
|
|
4383
|
+
} catch {}
|
|
4384
|
+
if (!local) {
|
|
4314
4385
|
info(`non-local board: skipping project-file checks for ${root}`);
|
|
4315
4386
|
console.log(bad === 0 ? "doctor: healthy" : `doctor: ${bad} problem(s)`);
|
|
4316
4387
|
if (bad > 0) process.exitCode = 1;
|
|
@@ -4426,16 +4497,16 @@ function cmdDoctor(args) {
|
|
|
4426
4497
|
break;
|
|
4427
4498
|
}
|
|
4428
4499
|
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4500
|
+
// Reconcile hint (read-only): workers whose pids look recycled since a
|
|
4501
|
+
// reboot read dead instead of running. Informational only — never FAIL.
|
|
4502
|
+
try {
|
|
4503
|
+
const stale = listJson(path.join(root, "agents"))
|
|
4504
|
+
.map((e) => e.data)
|
|
4505
|
+
.filter((x) => x && x.name && isPidStale(x.spawnedPid, x.spawnedAt))
|
|
4506
|
+
.map((x) => x.name);
|
|
4507
|
+
if (stale.length > 0) info(`${stale.length} worker(s) with stale pids (presumed dead after reboot): ${stale.slice(0, 5).join(",")}${stale.length > 5 ? "…" : ""} — see spawn-status --all; respawn to reboot`);
|
|
4508
|
+
} catch {}
|
|
4509
|
+
if (!process.env.CREWBUS_AGENT) info("CREWBUS_AGENT is unset — hooks need it to know who you are");
|
|
4439
4510
|
if (!process.env.CREWBUS_TOKEN) info("CREWBUS_TOKEN is unset — sends/reads as a claimed name need it");
|
|
4440
4511
|
try {
|
|
4441
4512
|
const legacy = fs.readdirSync(path.join(root, "agents")).filter((f) => f.endsWith(".json")).map((f) => {
|
|
@@ -4801,6 +4872,7 @@ async function cmdCrew(args) {
|
|
|
4801
4872
|
async function cmdServe(args) {
|
|
4802
4873
|
const root = boardDir(args);
|
|
4803
4874
|
const d = requireBoard(root);
|
|
4875
|
+
ensureBoardCliShim(d);
|
|
4804
4876
|
const host = getFlag(args, "--host") || "127.0.0.1";
|
|
4805
4877
|
const port = Number(getFlag(args, "--port") || 0);
|
|
4806
4878
|
if (!(port >= 0 && port < 65536)) fail("--port must be 0-65535 (0 = random)");
|
|
@@ -6240,6 +6312,8 @@ Setup:
|
|
|
6240
6312
|
crewbus init [--global] [--board <path>] [--force] [--no-opencode] [--portable]
|
|
6241
6313
|
[--harness opencode,claude,codex,antigravity,grok,cursor,generic]
|
|
6242
6314
|
create board in ./.crewbus, write AGENTS.md block, install harness
|
|
6315
|
+
crewbus install-cli
|
|
6316
|
+
install standalone CLI wrapper to user PATH (~/.crewbus/bin)
|
|
6243
6317
|
wiring (hooks + MCP config + notes). Without --harness, init applies the
|
|
6244
6318
|
union of detected markers (.opencode/.claude/.codex/.agents/.grok/.cursor).
|
|
6245
6319
|
init also pre-approves board-only bus I/O (crewbus MCP server +
|
|
@@ -7243,6 +7317,21 @@ async function cmdPoolResume(args) {
|
|
|
7243
7317
|
}
|
|
7244
7318
|
}
|
|
7245
7319
|
|
|
7320
|
+
function cmdInstallCli() {
|
|
7321
|
+
const res = installCliToUserPath();
|
|
7322
|
+
if (res.ok) {
|
|
7323
|
+
console.log(`crewbus: standalone CLI wrappers written to ${res.binDir}`);
|
|
7324
|
+
if (res.addedToPath) {
|
|
7325
|
+
console.log(`crewbus: ${res.detail}`);
|
|
7326
|
+
console.log(`crewbus: restart your terminal window, then type "crewbus" to verify`);
|
|
7327
|
+
} else {
|
|
7328
|
+
console.log(`crewbus: ${res.detail}`);
|
|
7329
|
+
}
|
|
7330
|
+
} else {
|
|
7331
|
+
fail(`install-cli failed: ${res.detail || "unknown error"}`);
|
|
7332
|
+
}
|
|
7333
|
+
}
|
|
7334
|
+
|
|
7246
7335
|
async function main() {
|
|
7247
7336
|
const [, , cmd, ...rest] = process.argv;
|
|
7248
7337
|
if (REMOVED.has(cmd)) {
|
|
@@ -7250,6 +7339,7 @@ async function main() {
|
|
|
7250
7339
|
}
|
|
7251
7340
|
switch (cmd) {
|
|
7252
7341
|
case "init": return cmdInit(rest);
|
|
7342
|
+
case "install-cli": return cmdInstallCli();
|
|
7253
7343
|
case "register": return cmdRegister(rest);
|
|
7254
7344
|
case "acl": return cmdAcl(rest);
|
|
7255
7345
|
case "agents": return cmdAgents(rest);
|
package/bin/lib/groups.js
CHANGED
|
@@ -104,7 +104,10 @@ export function readGroup(d, name) {
|
|
|
104
104
|
export function expandGroups(d, raw) {
|
|
105
105
|
const out = [];
|
|
106
106
|
if (raw === undefined || raw === null || String(raw).trim() === "") return out;
|
|
107
|
-
for (const part of String(raw).split(
|
|
107
|
+
for (const part of String(raw).split(/[,\s]+/)) {
|
|
108
|
+
// Group names carry no spaces (see cleanGroupName), so splitting on
|
|
109
|
+
// whitespace too is safe — and it saves PowerShell users whose
|
|
110
|
+
// unquoted --to-group a,b arrives space-rejoined (see cmdGroup --add).
|
|
108
111
|
if (part.trim() === "") continue;
|
|
109
112
|
const g = String(part).trim().toLowerCase().replace(/[^a-z0-9_.-]/g, "-").slice(0, 40);
|
|
110
113
|
if (!g || g === "@all") throw new Error(`invalid group name "${part}"`);
|
package/bin/lib/identity.js
CHANGED
|
@@ -385,6 +385,25 @@ export function authorizeThrow(d, agent, action, scope) { // line 591
|
|
|
385
385
|
return r;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
// Canonical agent-doc merge: every writer starts from prev and applies
|
|
389
|
+
// only its patch, so parallel writers (register, heartbeat, the dm-send
|
|
390
|
+
// tool, the dm-watch plugin) can never drop fields they don't own (spawn
|
|
391
|
+
// bookkeeping, tokens, roles). A patch key wins on any defined value —
|
|
392
|
+
// including false/0/"" — so callers MUST pass undefined for no-change.
|
|
393
|
+
// New agent-doc writers must use this or spread prev; never enumerate.
|
|
394
|
+
export function mergeAgentDoc(prev, patch) {
|
|
395
|
+
const base = (prev && typeof prev === "object") ? { ...prev } : {};
|
|
396
|
+
const p = (patch && typeof patch === "object") ? patch : {};
|
|
397
|
+
for (const k of Object.keys(p)) {
|
|
398
|
+
if (p[k] === undefined) delete base[k];
|
|
399
|
+
else base[k] = p[k];
|
|
400
|
+
}
|
|
401
|
+
for (const k of Object.keys(base)) {
|
|
402
|
+
if (base[k] === undefined) delete base[k];
|
|
403
|
+
}
|
|
404
|
+
return base;
|
|
405
|
+
}
|
|
406
|
+
|
|
388
407
|
export function touchAgent(d, name, extra) { // line 2647
|
|
389
408
|
const p = path.join(d.agents, `${name}.json`);
|
|
390
409
|
const now = new Date().toISOString();
|
|
@@ -393,36 +412,13 @@ export function touchAgent(d, name, extra) { // line 2647
|
|
|
393
412
|
prev = readJson(p);
|
|
394
413
|
} catch {}
|
|
395
414
|
const { v, hlc } = stampSyncDoc(prev);
|
|
396
|
-
const doc = {
|
|
415
|
+
const doc = mergeAgentDoc(prev, {
|
|
397
416
|
name,
|
|
398
417
|
firstSeen: (prev && prev.firstSeen) || now,
|
|
399
418
|
lastSeen: now,
|
|
400
|
-
|
|
401
|
-
lastDir: (extra && extra.lastDir) || (prev && prev.lastDir) || undefined,
|
|
402
|
-
spawnedPid: (extra && extra.spawnedPid) || (prev && prev.spawnedPid) || undefined,
|
|
403
|
-
spawnedAt: (extra && extra.spawnedAt) || (prev && prev.spawnedAt) || undefined,
|
|
404
|
-
spawnedBy: (extra && extra.spawnedBy) || (prev && prev.spawnedBy) || undefined,
|
|
405
|
-
briefId: (extra && extra.briefId) || (prev && prev.briefId) || undefined,
|
|
406
|
-
budgetTokens: (extra && extra.budgetTokens) || (prev && prev.budgetTokens) || undefined,
|
|
407
|
-
budgetMinutes: (extra && extra.budgetMinutes) || (prev && prev.budgetMinutes) || undefined,
|
|
408
|
-
budgetSince: (extra && extra.budgetSince) || (prev && prev.budgetSince) || undefined,
|
|
409
|
-
deadlineAt: (extra && extra.deadlineAt) || (prev && prev.deadlineAt) || undefined,
|
|
410
|
-
spawnedWorktree: (extra && extra.spawnedWorktree) || (prev && prev.spawnedWorktree) || undefined,
|
|
411
|
-
spawnedBranch: (extra && extra.spawnedBranch) || (prev && prev.spawnedBranch) || undefined,
|
|
412
|
-
spawnedLifetime: (extra && extra.spawnedLifetime) || (prev && prev.spawnedLifetime) || undefined,
|
|
413
|
-
spawnedHarness: (extra && extra.spawnedHarness) || (prev && prev.spawnedHarness) || undefined,
|
|
414
|
-
spawnedEnvScrubbed: (extra && extra.spawnedEnvScrubbed) || (prev && prev.spawnedEnvScrubbed) || undefined,
|
|
415
|
-
token: (prev && prev.token) || undefined,
|
|
416
|
-
tokenHash: (prev && prev.tokenHash) || undefined,
|
|
417
|
-
salt: (prev && prev.salt) || undefined,
|
|
418
|
-
expiresAt: (extra && extra.expiresAt !== undefined ? extra.expiresAt : undefined) ?? (prev && prev.expiresAt !== undefined ? prev.expiresAt : undefined),
|
|
419
|
-
rotatedAt: (extra && extra.rotatedAt !== undefined ? extra.rotatedAt : undefined) ?? (prev && prev.rotatedAt !== undefined ? prev.rotatedAt : undefined),
|
|
420
|
-
service: (extra && extra.service !== undefined ? extra.service : undefined) ?? (prev && prev.service !== undefined ? prev.service : undefined),
|
|
421
|
-
offboarded: (extra && extra.offboarded !== undefined ? extra.offboarded : undefined) ?? (prev && prev.offboarded !== undefined ? prev.offboarded : undefined),
|
|
422
|
-
revokedAt: (extra && extra.revokedAt !== undefined ? extra.revokedAt : undefined) ?? (prev && prev.revokedAt !== undefined ? prev.revokedAt : undefined),
|
|
423
|
-
role: (extra && extra.role !== undefined ? extra.role : undefined) ?? (prev && prev.role !== undefined ? prev.role : undefined),
|
|
419
|
+
...(extra || {}),
|
|
424
420
|
v, hlc,
|
|
425
|
-
};
|
|
421
|
+
});
|
|
426
422
|
writeAgentFile(d, name, doc);
|
|
427
423
|
return doc;
|
|
428
424
|
}
|
package/bin/lib/mail.js
CHANGED
|
@@ -9,7 +9,7 @@ import path from "node:path";
|
|
|
9
9
|
import crypto from "node:crypto";
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { readJson, writeJson, listJson, newId, sanitizeName, fail, MAX_RECIPIENTS, BROADCAST_AFTER, MAX_FWD_DEPTH, SEND_RATE_CAP, SEND_RATE_WINDOW_MS, DEDUPE_WINDOW_MS, cleanPriority, cleanSubject, cleanReply, cleanArtifact, stampSyncDoc } from "./store.js";
|
|
12
|
-
import { writeAgentFile, timingSafeEqualStr } from "./identity.js";
|
|
12
|
+
import { writeAgentFile, timingSafeEqualStr, mergeAgentDoc } from "./identity.js";
|
|
13
13
|
import { writeTombstone } from "./sync.js";
|
|
14
14
|
import { boardHmacKey } from "./export.js";
|
|
15
15
|
|
|
@@ -733,34 +733,15 @@ export function heartbeat(d, name, minAgeMs) { // line 2689
|
|
|
733
733
|
if (!(age >= minAgeMs)) return prev;
|
|
734
734
|
}
|
|
735
735
|
const { v, hlc } = stampSyncDoc(prev);
|
|
736
|
-
|
|
736
|
+
// Merge, never enumerate: heartbeat must not drop fields it doesn't own
|
|
737
|
+
// (spawn bookkeeping, tokens, roles — see mergeAgentDoc in identity.js).
|
|
738
|
+
const doc = mergeAgentDoc(prev, {
|
|
737
739
|
name,
|
|
738
740
|
firstSeen: (prev && prev.firstSeen) || now,
|
|
739
741
|
lastSeen: now,
|
|
740
|
-
sessionId: (prev && prev.sessionId) || undefined,
|
|
741
742
|
lastDir: process.cwd(),
|
|
742
|
-
spawnedPid: (prev && prev.spawnedPid) || undefined,
|
|
743
|
-
spawnedAt: (prev && prev.spawnedAt) || undefined,
|
|
744
|
-
spawnedBy: (prev && prev.spawnedBy) || undefined,
|
|
745
|
-
briefId: (prev && prev.briefId) || undefined,
|
|
746
|
-
budgetTokens: (prev && prev.budgetTokens) || undefined,
|
|
747
|
-
budgetMinutes: (prev && prev.budgetMinutes) || undefined,
|
|
748
|
-
budgetSince: (prev && prev.budgetSince) || undefined,
|
|
749
|
-
deadlineAt: (prev && prev.deadlineAt) || undefined,
|
|
750
|
-
spawnedWorktree: (prev && prev.spawnedWorktree) || undefined,
|
|
751
|
-
spawnedBranch: (prev && prev.spawnedBranch) || undefined,
|
|
752
|
-
spawnedLifetime: (prev && prev.spawnedLifetime) || undefined,
|
|
753
|
-
token: (prev && prev.token) || undefined,
|
|
754
|
-
tokenHash: (prev && prev.tokenHash) || undefined,
|
|
755
|
-
salt: (prev && prev.salt) || undefined,
|
|
756
|
-
expiresAt: (prev && prev.expiresAt !== undefined ? prev.expiresAt : undefined),
|
|
757
|
-
rotatedAt: (prev && prev.rotatedAt !== undefined ? prev.rotatedAt : undefined),
|
|
758
|
-
service: (prev && prev.service !== undefined ? prev.service : undefined),
|
|
759
|
-
offboarded: (prev && prev.offboarded !== undefined ? prev.offboarded : undefined),
|
|
760
|
-
revokedAt: (prev && prev.revokedAt !== undefined ? prev.revokedAt : undefined),
|
|
761
|
-
role: (prev && prev.role !== undefined ? prev.role : undefined),
|
|
762
743
|
v, hlc,
|
|
763
|
-
};
|
|
744
|
+
});
|
|
764
745
|
try {
|
|
765
746
|
fs.mkdirSync(d.agents, { recursive: true });
|
|
766
747
|
writeAgentFile(d, name, doc);
|