swarmdo 1.50.0 → 1.58.1
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/.claude/helpers/hook-handler.cjs +16 -0
- package/.claude/helpers/router.cjs +27 -1
- package/.claude/helpers/statusline.cjs +10 -3
- package/README.md +6 -3
- package/package.json +1 -1
- package/v3/@swarmdo/cli/dist/src/agent-bridge/bridge.d.ts +104 -0
- package/v3/@swarmdo/cli/dist/src/agent-bridge/bridge.js +133 -0
- package/v3/@swarmdo/cli/dist/src/command-usage/usage.d.ts +74 -0
- package/v3/@swarmdo/cli/dist/src/command-usage/usage.js +161 -0
- package/v3/@swarmdo/cli/dist/src/commands/agent.js +133 -1
- package/v3/@swarmdo/cli/dist/src/commands/commands.d.ts +19 -0
- package/v3/@swarmdo/cli/dist/src/commands/commands.js +107 -0
- package/v3/@swarmdo/cli/dist/src/commands/index.js +11 -3
- package/v3/@swarmdo/cli/dist/src/commands/standup.d.ts +18 -0
- package/v3/@swarmdo/cli/dist/src/commands/standup.js +116 -0
- package/v3/@swarmdo/cli/dist/src/commands/swarm.js +29 -0
- package/v3/@swarmdo/cli/dist/src/commands/usage.js +57 -1
- package/v3/@swarmdo/cli/dist/src/comms/store.d.ts +11 -1
- package/v3/@swarmdo/cli/dist/src/comms/store.js +16 -2
- package/v3/@swarmdo/cli/dist/src/init/helpers-generator.js +41 -1
- package/v3/@swarmdo/cli/dist/src/init/statusline-generator.js +9 -2
- package/v3/@swarmdo/cli/dist/src/integrations/integrations.js +19 -8
- package/v3/@swarmdo/cli/dist/src/mcp-tools/agent-tools.js +166 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/comms-tools.js +4 -4
- package/v3/@swarmdo/cli/dist/src/mcp-tools/swarm-tools.d.ts +33 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/swarm-tools.js +73 -38
- package/v3/@swarmdo/cli/dist/src/standup/standup.d.ts +83 -0
- package/v3/@swarmdo/cli/dist/src/standup/standup.js +157 -0
- package/v3/@swarmdo/cli/dist/src/usage/transcript-friction.d.ts +77 -0
- package/v3/@swarmdo/cli/dist/src/usage/transcript-friction.js +151 -0
- package/v3/@swarmdo/cli/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { output } from '../output.js';
|
|
|
6
6
|
import { select, confirm, input } from '../prompt.js';
|
|
7
7
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
8
8
|
import { wasmSubcommands } from './agent-wasm.js';
|
|
9
|
+
import { classifyPrompt } from '../agent-bridge/bridge.js';
|
|
9
10
|
import * as fs from 'fs';
|
|
10
11
|
import * as path from 'path';
|
|
11
12
|
/**
|
|
@@ -854,11 +855,142 @@ function formatLogLevel(level) {
|
|
|
854
855
|
return `[${level.toUpperCase()}]`;
|
|
855
856
|
}
|
|
856
857
|
}
|
|
858
|
+
// Bridge subcommand — link Claude Code Agent-tool agents into Swarmdo's registry
|
|
859
|
+
const bridgeCommand = {
|
|
860
|
+
name: 'bridge',
|
|
861
|
+
description: "Link Claude Code Agent-tool agents into Swarmdo's registry so `swarmdo agent list` reflects the agents actually running (register/list/sync/advise). Registering auto-spins-up a swarm from config and enrolls the agent.",
|
|
862
|
+
options: [
|
|
863
|
+
{ name: 'name', short: 'n', description: 'Claude Code agent name (register)', type: 'string' },
|
|
864
|
+
{ name: 'session', short: 's', description: 'Claude Code session id (register)', type: 'string' },
|
|
865
|
+
{ name: 'type', short: 't', description: 'agent type (register; default general-purpose)', type: 'string' },
|
|
866
|
+
{ name: 'task', description: 'one-line task summary (register)', type: 'string' },
|
|
867
|
+
{ name: 'live', description: 'comma-separated live Claude Code agent names (sync)', type: 'string' },
|
|
868
|
+
{ name: 'prune', description: 'with sync: reap orphaned bound records (their Claude agent is gone)', type: 'boolean' },
|
|
869
|
+
{ name: 'format', description: 'output format: text|json', type: 'string' },
|
|
870
|
+
],
|
|
871
|
+
examples: [
|
|
872
|
+
{ command: 'swarmdo agent bridge register -n research-ccgap -s cec69c3c -t general-purpose', description: 'Bind a running Claude Code agent into Swarmdo (auto-creates a swarm)' },
|
|
873
|
+
{ command: 'swarmdo agent bridge list', description: 'Show Claude-Code-bound vs native Swarmdo agents' },
|
|
874
|
+
{ command: 'swarmdo agent bridge sync --live research-ccgap,research-mcp', description: 'Reconcile the live roster against the store' },
|
|
875
|
+
{ command: 'swarmdo agent bridge advise "Refactor the auth module across files"', description: 'Should this prompt spin up a swarm? Which roles?' },
|
|
876
|
+
],
|
|
877
|
+
action: async (ctx) => {
|
|
878
|
+
const sub = (ctx.args[0] || 'list').toLowerCase();
|
|
879
|
+
const asJson = ctx.flags.format === 'json';
|
|
880
|
+
try {
|
|
881
|
+
switch (sub) {
|
|
882
|
+
case 'register': {
|
|
883
|
+
const name = ctx.flags.name;
|
|
884
|
+
if (!name) {
|
|
885
|
+
output.printError('bridge register needs --name <claude-agent-name> (the running Claude Code agent)');
|
|
886
|
+
return { success: false, exitCode: 1 };
|
|
887
|
+
}
|
|
888
|
+
const res = await callMCPTool('agent_bridge_register', {
|
|
889
|
+
name,
|
|
890
|
+
sessionId: ctx.flags.session,
|
|
891
|
+
agentType: ctx.flags.type || 'general-purpose',
|
|
892
|
+
task: ctx.flags.task,
|
|
893
|
+
});
|
|
894
|
+
if (asJson) {
|
|
895
|
+
output.printJson(res);
|
|
896
|
+
return { success: true, data: res };
|
|
897
|
+
}
|
|
898
|
+
output.printSuccess(`Bound Claude Code agent '${name}' → Swarmdo agent ${String(res.agentId)}`);
|
|
899
|
+
const swarm = res.swarm;
|
|
900
|
+
if (swarm) {
|
|
901
|
+
output.writeln(` swarm: ${swarm.swarmId} [${swarm.topology}] ${swarm.created ? '(created)' : '(joined existing)'}`);
|
|
902
|
+
}
|
|
903
|
+
return { success: true, data: res };
|
|
904
|
+
}
|
|
905
|
+
case 'list': {
|
|
906
|
+
const res = await callMCPTool('agent_bridge_list', {});
|
|
907
|
+
if (asJson) {
|
|
908
|
+
output.printJson(res);
|
|
909
|
+
return { success: true, data: res };
|
|
910
|
+
}
|
|
911
|
+
const bound = res.bound || [];
|
|
912
|
+
const native = res.native || [];
|
|
913
|
+
output.writeln();
|
|
914
|
+
output.writeln(output.bold(`Swarmdo agents: ${res.total} (${res.boundCount} Claude-Code-bound, ${res.nativeCount} native)`));
|
|
915
|
+
if (bound.length) {
|
|
916
|
+
output.writeln();
|
|
917
|
+
output.writeln(output.bold('Claude-Code-bound:'));
|
|
918
|
+
for (const a of bound) {
|
|
919
|
+
const b = a.binding || {};
|
|
920
|
+
output.writeln(` ${a.agentId} [${a.agentType}] ⇠ ${b.claudeName}${b.sessionId ? '@' + b.sessionId : ''}`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (native.length) {
|
|
924
|
+
output.writeln();
|
|
925
|
+
output.writeln(output.bold('Native Swarmdo:'));
|
|
926
|
+
for (const a of native)
|
|
927
|
+
output.writeln(` ${a.agentId} [${a.agentType}] ${a.status ?? ''}`);
|
|
928
|
+
}
|
|
929
|
+
if (Number(res.total) === 0) {
|
|
930
|
+
output.printInfo('No agents registered. Spawn a Claude Code agent, then `swarmdo agent bridge register -n <name>`.');
|
|
931
|
+
}
|
|
932
|
+
return { success: true, data: res };
|
|
933
|
+
}
|
|
934
|
+
case 'sync': {
|
|
935
|
+
const live = typeof ctx.flags.live === 'string'
|
|
936
|
+
? ctx.flags.live.split(',').map((s) => s.trim()).filter(Boolean)
|
|
937
|
+
: [];
|
|
938
|
+
const res = await callMCPTool('agent_bridge_list', { live });
|
|
939
|
+
if (asJson) {
|
|
940
|
+
output.printJson(res);
|
|
941
|
+
return { success: true, data: res };
|
|
942
|
+
}
|
|
943
|
+
const rec = res.reconciliation || { mirrored: [], unmirrored: [], orphaned: [] };
|
|
944
|
+
output.writeln();
|
|
945
|
+
output.writeln(output.bold('Bridge reconciliation'));
|
|
946
|
+
output.writeln(` mirrored: ${rec.mirrored.join(', ') || '(none)'}`);
|
|
947
|
+
output.writeln(` unmirrored: ${rec.unmirrored.join(', ') || '(none)'}${rec.unmirrored.length ? ' → run `agent bridge register -n <name>` for each' : ''}`);
|
|
948
|
+
output.writeln(` orphaned: ${rec.orphaned.join(', ') || '(none)'}${rec.orphaned.length ? (ctx.flags.prune ? '' : ' → their Claude agent is gone (pass --prune to reap)') : ''}`);
|
|
949
|
+
if (ctx.flags.prune) {
|
|
950
|
+
const pruneRes = await callMCPTool('agent_bridge_prune', { live });
|
|
951
|
+
const pruned = pruneRes.pruned || [];
|
|
952
|
+
output.writeln();
|
|
953
|
+
output.printSuccess(`Pruned ${pruned.length} orphaned bound record${pruned.length === 1 ? '' : 's'}${pruned.length ? ': ' + pruned.join(', ') : ''}`);
|
|
954
|
+
return { success: true, data: { ...res, prune: pruneRes } };
|
|
955
|
+
}
|
|
956
|
+
return { success: true, data: res };
|
|
957
|
+
}
|
|
958
|
+
case 'advise': {
|
|
959
|
+
const prompt = ctx.args.slice(1).join(' ').trim();
|
|
960
|
+
const intent = classifyPrompt(prompt);
|
|
961
|
+
if (asJson) {
|
|
962
|
+
output.printJson(intent);
|
|
963
|
+
return { success: true, data: intent };
|
|
964
|
+
}
|
|
965
|
+
if (!intent.requiresAgents) {
|
|
966
|
+
output.printInfo(`No swarm needed: ${intent.reason}`);
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
output.writeln(output.bold(`Swarm recommended — ${intent.reason}`));
|
|
970
|
+
output.writeln(` roles: ${intent.suggestedRoles.join(', ')}`);
|
|
971
|
+
output.writeln(' After spawning each Claude Code agent, bind it:');
|
|
972
|
+
for (const role of intent.suggestedRoles) {
|
|
973
|
+
output.writeln(` swarmdo agent bridge register -n <name> -t ${role} -s <session>`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return { success: true, data: intent };
|
|
977
|
+
}
|
|
978
|
+
default:
|
|
979
|
+
output.printError(`unknown bridge subcommand '${sub}' (use register|list|sync|advise)`);
|
|
980
|
+
return { success: false, exitCode: 1 };
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
catch (e) {
|
|
984
|
+
output.printError(`bridge ${sub} failed: ${e instanceof MCPClientError ? e.message : String(e)}`);
|
|
985
|
+
return { success: false, exitCode: 1 };
|
|
986
|
+
}
|
|
987
|
+
},
|
|
988
|
+
};
|
|
857
989
|
// Main agent command
|
|
858
990
|
export const agentCommand = {
|
|
859
991
|
name: 'agent',
|
|
860
992
|
description: 'Agent management commands',
|
|
861
|
-
subcommands: [spawnCommand, listCommand, statusCommand, stopCommand, metricsCommand, poolCommand, healthCommand, logsCommand, ...wasmSubcommands],
|
|
993
|
+
subcommands: [spawnCommand, listCommand, statusCommand, stopCommand, metricsCommand, poolCommand, healthCommand, logsCommand, bridgeCommand, ...wasmSubcommands],
|
|
862
994
|
options: [],
|
|
863
995
|
examples: [
|
|
864
996
|
{ command: 'swarmdo agent spawn -t coder', description: 'Spawn a coder agent' },
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo commands` (alias `slash`) — a hot/cold/orphan USAGE report for the
|
|
3
|
+
* project's authored `.claude/` slash-commands + subagents. `config lint` says
|
|
4
|
+
* "are these valid?"; this says "are these used?" by joining defined files
|
|
5
|
+
* against invocation counts mined from the local transcripts. #101.
|
|
6
|
+
*
|
|
7
|
+
* swarmdo commands # this project's authored surface
|
|
8
|
+
* swarmdo commands --all # count invocations across every project
|
|
9
|
+
* swarmdo commands --unused # only the cold (never-invoked) items
|
|
10
|
+
* swarmdo commands --unused --strict # exit 1 if anything is unused (CI gate)
|
|
11
|
+
* swarmdo commands --json # machine-readable
|
|
12
|
+
*
|
|
13
|
+
* Engine (../command-usage/usage.ts) is pure + fixture-tested; this thin layer
|
|
14
|
+
* renders and applies the --unused/--strict policy.
|
|
15
|
+
*/
|
|
16
|
+
import type { Command } from '../types.js';
|
|
17
|
+
export declare const commandsCommand: Command;
|
|
18
|
+
export default commandsCommand;
|
|
19
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo commands` (alias `slash`) — a hot/cold/orphan USAGE report for the
|
|
3
|
+
* project's authored `.claude/` slash-commands + subagents. `config lint` says
|
|
4
|
+
* "are these valid?"; this says "are these used?" by joining defined files
|
|
5
|
+
* against invocation counts mined from the local transcripts. #101.
|
|
6
|
+
*
|
|
7
|
+
* swarmdo commands # this project's authored surface
|
|
8
|
+
* swarmdo commands --all # count invocations across every project
|
|
9
|
+
* swarmdo commands --unused # only the cold (never-invoked) items
|
|
10
|
+
* swarmdo commands --unused --strict # exit 1 if anything is unused (CI gate)
|
|
11
|
+
* swarmdo commands --json # machine-readable
|
|
12
|
+
*
|
|
13
|
+
* Engine (../command-usage/usage.ts) is pure + fixture-tested; this thin layer
|
|
14
|
+
* renders and applies the --unused/--strict policy.
|
|
15
|
+
*/
|
|
16
|
+
import { output } from '../output.js';
|
|
17
|
+
import { collectCommandUsage } from '../command-usage/usage.js';
|
|
18
|
+
/** Render one domain's hot/cold/orphan buckets. `slash` prefixes command names. */
|
|
19
|
+
function renderBuckets(label, defined, b, slash) {
|
|
20
|
+
const nm = (n) => (slash ? `/${n}` : n);
|
|
21
|
+
output.writeln('');
|
|
22
|
+
output.writeln(output.bold(`${label}`) + output.dim(` (${defined} defined · ${b.hot.length} hot · ${b.cold.length} cold · ${b.orphan.length} orphan)`));
|
|
23
|
+
if (b.hot.length > 0) {
|
|
24
|
+
output.printTable({
|
|
25
|
+
columns: [
|
|
26
|
+
{ key: 'name', header: 'Hot (used)', width: 34 },
|
|
27
|
+
{ key: 'count', header: 'Invocations', width: 12, align: 'right' },
|
|
28
|
+
],
|
|
29
|
+
data: b.hot.map((h) => ({ name: nm(h.name), count: h.count.toLocaleString('en-US') })),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (b.cold.length > 0) {
|
|
33
|
+
output.writeln(output.warning(` cold (never invoked — prune candidates): `) + b.cold.map(nm).join(' '));
|
|
34
|
+
}
|
|
35
|
+
if (b.orphan.length > 0) {
|
|
36
|
+
output.writeln(output.dim(` orphan (invoked, not defined — builtin or typo): `) + b.orphan.map(nm).join(' '));
|
|
37
|
+
}
|
|
38
|
+
if (b.hot.length === 0 && b.cold.length === 0 && b.orphan.length === 0) {
|
|
39
|
+
output.writeln(output.dim(' (none)'));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Print only the cold (unused) items — the prune list. Returns the cold count. */
|
|
43
|
+
function renderUnused(report) {
|
|
44
|
+
const coldCmds = report.commands.cold;
|
|
45
|
+
const coldAgents = report.agents.cold;
|
|
46
|
+
const total = coldCmds.length + coldAgents.length;
|
|
47
|
+
output.writeln(output.bold('Unused (cold) authored items') + output.dim(` (${report.scope} scope · ${report.filesScanned} transcript files)`));
|
|
48
|
+
if (total === 0) {
|
|
49
|
+
output.writeln(output.info(' none — every authored command and agent has been invoked 🎉'));
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
if (coldCmds.length > 0)
|
|
53
|
+
output.writeln(` commands: ${coldCmds.map((c) => `/${c}`).join(' ')}`);
|
|
54
|
+
if (coldAgents.length > 0)
|
|
55
|
+
output.writeln(` agents: ${coldAgents.join(' ')}`);
|
|
56
|
+
return total;
|
|
57
|
+
}
|
|
58
|
+
async function run(ctx) {
|
|
59
|
+
const cwd = ctx.cwd || process.cwd();
|
|
60
|
+
const all = ctx.flags.all === true;
|
|
61
|
+
const report = collectCommandUsage({ cwd, all });
|
|
62
|
+
if (ctx.flags.json === true) {
|
|
63
|
+
output.writeln(JSON.stringify(report, null, 2));
|
|
64
|
+
return { success: true, exitCode: 0 };
|
|
65
|
+
}
|
|
66
|
+
if (report.definedCommands === 0 && report.definedAgents === 0) {
|
|
67
|
+
output.writeln(output.info('nothing authored — no .claude/commands or .claude/agents in this project'));
|
|
68
|
+
return { success: true, exitCode: 0 };
|
|
69
|
+
}
|
|
70
|
+
if (ctx.flags.unused === true) {
|
|
71
|
+
const coldCount = renderUnused(report);
|
|
72
|
+
const strict = ctx.flags.strict === true;
|
|
73
|
+
const exitCode = strict && coldCount > 0 ? 1 : 0;
|
|
74
|
+
if (exitCode === 1)
|
|
75
|
+
output.writeln(output.dim(`exit 1 (--strict + ${coldCount} unused)`));
|
|
76
|
+
return { success: exitCode === 0, exitCode };
|
|
77
|
+
}
|
|
78
|
+
output.writeln(output.bold('Authored .claude/ surface — usage') + output.dim(` (${report.scope} scope · ${report.filesScanned} transcript files)`));
|
|
79
|
+
renderBuckets('Commands', report.definedCommands, report.commands, true);
|
|
80
|
+
renderBuckets('Agents', report.definedAgents, report.agents, false);
|
|
81
|
+
const totalCold = report.commands.cold.length + report.agents.cold.length;
|
|
82
|
+
if (totalCold > 0) {
|
|
83
|
+
output.writeln('');
|
|
84
|
+
output.writeln(output.dim(`${totalCold} unused — see \`swarmdo commands --unused\` (add --strict for a CI gate)`));
|
|
85
|
+
}
|
|
86
|
+
return { success: true, exitCode: 0 };
|
|
87
|
+
}
|
|
88
|
+
export const commandsCommand = {
|
|
89
|
+
name: 'commands',
|
|
90
|
+
aliases: ['slash'],
|
|
91
|
+
description: "Usage report (hot/cold/orphan) for the project's authored .claude/ slash-commands & subagents",
|
|
92
|
+
options: [
|
|
93
|
+
{ name: 'all', description: 'count invocations across every project (default: current project only)', type: 'boolean', default: false },
|
|
94
|
+
{ name: 'unused', description: 'print only the cold (never-invoked) items — the prune list', type: 'boolean', default: false },
|
|
95
|
+
{ name: 'strict', description: 'with --unused, exit 1 if anything is unused (CI gate)', type: 'boolean', default: false },
|
|
96
|
+
{ name: 'json', description: 'machine-readable output', type: 'boolean', default: false },
|
|
97
|
+
],
|
|
98
|
+
examples: [
|
|
99
|
+
{ command: 'swarmdo commands', description: 'Hot/cold/orphan report for your authored .claude/ surface' },
|
|
100
|
+
{ command: 'swarmdo commands --unused', description: 'List slash-commands & agents you defined but never invoke' },
|
|
101
|
+
{ command: 'swarmdo commands --unused --strict', description: 'CI gate: fail if any authored command/agent is unused' },
|
|
102
|
+
{ command: 'swarmdo slash --all --json', description: 'Cross-project invocation counts as JSON (alias)' },
|
|
103
|
+
],
|
|
104
|
+
action: run,
|
|
105
|
+
};
|
|
106
|
+
export default commandsCommand;
|
|
107
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -60,6 +60,14 @@ const commandLoaders = {
|
|
|
60
60
|
// Hidden coupling — co-change pairs with NO import edge (logical minus
|
|
61
61
|
// structural coupling); joins `coupling`'s git capture with codegraph's graph.
|
|
62
62
|
'hidden-coupling': () => import('./hidden-coupling.js'),
|
|
63
|
+
// Weekend-aware "what did I do?" recall from git history (git-standup demand)
|
|
64
|
+
// — commits since your last working day, grouped by day with a diffstat.
|
|
65
|
+
standup: () => import('./standup.js'),
|
|
66
|
+
mine: () => import('./standup.js'), // alias via loader key
|
|
67
|
+
// Hot/cold/orphan USAGE report for authored .claude/ slash-commands + subagents
|
|
68
|
+
// — the "dead-code report" for the authoring surface (config lint validates it).
|
|
69
|
+
commands: () => import('./commands.js'),
|
|
70
|
+
slash: () => import('./commands.js'), // alias via loader key
|
|
63
71
|
// JUnit/TAP test-result parser (dorny/test-reporter demand) — raw results →
|
|
64
72
|
// failing test + file:line + message; the front-half of the test→fix loop.
|
|
65
73
|
testreport: () => import('./testreport.js'),
|
|
@@ -302,7 +310,7 @@ export async function getCommandsByCategory() {
|
|
|
302
310
|
// three slots from 'statusline' onward (completionsCmd received the
|
|
303
311
|
// statusline module, analyzeCmd received completions, …), scrambling the
|
|
304
312
|
// categorized help. Every load now has a named slot.
|
|
305
|
-
const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, couplingCmd, ownershipCmd, hiddenCouplingCmd, testreportCmd, compactSnapshotCmd,] = await Promise.all([
|
|
313
|
+
const [daemonCmd, doctorCmd, embeddingsCmd, neuralCmd, performanceCmd, securityCmd, swarmvectorCmd, hiveMindCmd, configCmd, statuslineCmd, compressCmd, efficiencyCmd, completionsCmd, migrateCmd, workflowCmd, analyzeCmd, routeCmd, progressCmd, providersCmd, pluginsCmd, deploymentCmd, claimsCmd, issuesCmd, updateCmd, processCmd, guidanceCmd, applianceCmd, cleanupCmd, autopilotCmd, demoCmd, usageCmd, repairCmd, hudCmd, compactCmd, codegraphCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, couplingCmd, ownershipCmd, hiddenCouplingCmd, testreportCmd, compactSnapshotCmd, standupCmd, commandsUsageCmd,] = await Promise.all([
|
|
306
314
|
loadCommand('daemon'), loadCommand('doctor'), loadCommand('embeddings'), loadCommand('neural'),
|
|
307
315
|
loadCommand('performance'), loadCommand('security'), loadCommand('swarmvector'), loadCommand('hive-mind'),
|
|
308
316
|
loadCommand('config'), loadCommand('statusline'), loadCommand('compress'), loadCommand('efficiency'),
|
|
@@ -310,7 +318,7 @@ export async function getCommandsByCategory() {
|
|
|
310
318
|
loadCommand('analyze'), loadCommand('route'), loadCommand('progress'), loadCommand('providers'),
|
|
311
319
|
loadCommand('plugins'), loadCommand('deployment'), loadCommand('claims'), loadCommand('issues'),
|
|
312
320
|
loadCommand('update'), loadCommand('process'), loadCommand('guidance'), loadCommand('appliance'),
|
|
313
|
-
loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'), loadCommand('sbom'), loadCommand('apply'), loadCommand('hotspots'), loadCommand('affected'), loadCommand('cycles'), loadCommand('coupling'), loadCommand('ownership'), loadCommand('hidden-coupling'), loadCommand('testreport'), loadCommand('compact-snapshot'),
|
|
321
|
+
loadCommand('cleanup'), loadCommand('autopilot'), loadCommand('demo'), loadCommand('usage'), loadCommand('repair'), loadCommand('hud'), loadCommand('compact'), loadCommand('codegraph'), loadCommand('redact'), loadCommand('pack'), loadCommand('env'), loadCommand('license'), loadCommand('sbom'), loadCommand('apply'), loadCommand('hotspots'), loadCommand('affected'), loadCommand('cycles'), loadCommand('coupling'), loadCommand('ownership'), loadCommand('hidden-coupling'), loadCommand('testreport'), loadCommand('compact-snapshot'), loadCommand('standup'), loadCommand('commands'),
|
|
314
322
|
]);
|
|
315
323
|
return {
|
|
316
324
|
primary: [
|
|
@@ -329,7 +337,7 @@ export async function getCommandsByCategory() {
|
|
|
329
337
|
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, couplingCmd, ownershipCmd, hiddenCouplingCmd, testreportCmd, compactSnapshotCmd, efficiencyCmd,
|
|
330
338
|
].filter(Boolean),
|
|
331
339
|
analysis: [
|
|
332
|
-
analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd,
|
|
340
|
+
analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd, standupCmd, commandsUsageCmd,
|
|
333
341
|
].filter(Boolean),
|
|
334
342
|
management: [
|
|
335
343
|
providersCmd, pluginsCmd, deploymentCmd, claimsCmd,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo standup` — recall what you committed since your last working day.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo standup # your commits since the last working day
|
|
5
|
+
* swarmdo standup --all # everyone's commits
|
|
6
|
+
* swarmdo standup --author "Ada" # a specific author (or: swarmdo standup Ada)
|
|
7
|
+
* swarmdo standup --days 7 # explicit N-day window
|
|
8
|
+
* swarmdo standup --since 2w # explicit window (git approxidate)
|
|
9
|
+
* swarmdo standup --format json # machine-readable
|
|
10
|
+
*
|
|
11
|
+
* Weekend-aware (git-standup parity): on Monday it reaches back to Friday, on
|
|
12
|
+
* Sunday to Friday, otherwise to yesterday. Engine (../standup/standup.ts) is
|
|
13
|
+
* pure + tested; this thin layer captures the git log and renders.
|
|
14
|
+
*/
|
|
15
|
+
import type { Command } from '../types.js';
|
|
16
|
+
export declare const standupCommand: Command;
|
|
17
|
+
export default standupCommand;
|
|
18
|
+
//# sourceMappingURL=standup.d.ts.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo standup` — recall what you committed since your last working day.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo standup # your commits since the last working day
|
|
5
|
+
* swarmdo standup --all # everyone's commits
|
|
6
|
+
* swarmdo standup --author "Ada" # a specific author (or: swarmdo standup Ada)
|
|
7
|
+
* swarmdo standup --days 7 # explicit N-day window
|
|
8
|
+
* swarmdo standup --since 2w # explicit window (git approxidate)
|
|
9
|
+
* swarmdo standup --format json # machine-readable
|
|
10
|
+
*
|
|
11
|
+
* Weekend-aware (git-standup parity): on Monday it reaches back to Friday, on
|
|
12
|
+
* Sunday to Friday, otherwise to yesterday. Engine (../standup/standup.ts) is
|
|
13
|
+
* pure + tested; this thin layer captures the git log and renders.
|
|
14
|
+
*/
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { output } from '../output.js';
|
|
17
|
+
import { parseStandupLog, groupByDay, sinceLastWorkingDay, formatStandup } from '../standup/standup.js';
|
|
18
|
+
import { normalizeSince } from '../util/since.js';
|
|
19
|
+
/** Read a non-negative numeric flag the parser may deliver as number OR string. */
|
|
20
|
+
function numFlag(v) {
|
|
21
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' ? parseInt(v, 10) : NaN;
|
|
22
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
23
|
+
}
|
|
24
|
+
/** Resolve the default author (git config user.name); '' if unset/unavailable. */
|
|
25
|
+
function gitUserName(root) {
|
|
26
|
+
try {
|
|
27
|
+
return execFileSync('git', ['config', 'user.name'], {
|
|
28
|
+
cwd: root,
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
31
|
+
}).trim();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function run(ctx) {
|
|
38
|
+
const root = ctx.cwd || process.cwd();
|
|
39
|
+
const asJson = ctx.flags.format === 'json';
|
|
40
|
+
const all = ctx.flags.all === true;
|
|
41
|
+
// Author: explicit --author, else positional arg, else current git user
|
|
42
|
+
// (unless --all, which clears the filter to show everyone).
|
|
43
|
+
const authorFlag = typeof ctx.flags.author === 'string' ? ctx.flags.author : ctx.args[0];
|
|
44
|
+
let author = '';
|
|
45
|
+
if (!all)
|
|
46
|
+
author = ((authorFlag && authorFlag.trim()) || gitUserName(root)).trim();
|
|
47
|
+
// Window resolution (priority): --since (approxidate passthrough) >
|
|
48
|
+
// --days N > the weekend-aware default.
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const sinceExpr = typeof ctx.flags.since === 'string' ? ctx.flags.since.trim() : '';
|
|
51
|
+
const daysFlag = numFlag(ctx.flags.days);
|
|
52
|
+
let sinceArg;
|
|
53
|
+
let windowLabel;
|
|
54
|
+
if (sinceExpr) {
|
|
55
|
+
sinceArg = normalizeSince(sinceExpr);
|
|
56
|
+
windowLabel = sinceExpr;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const days = daysFlag ?? sinceLastWorkingDay(now).sinceDays;
|
|
60
|
+
// Cutoff = LOCAL midnight `days` ago, so the WHOLE last working day is
|
|
61
|
+
// included (a plain "3 days ago" would start mid-morning and miss commits).
|
|
62
|
+
const cutoff = new Date(now.getFullYear(), now.getMonth(), now.getDate() - days);
|
|
63
|
+
sinceArg = cutoff.toISOString();
|
|
64
|
+
windowLabel = days === 1 ? 'since yesterday' : `since ${days} days ago`;
|
|
65
|
+
}
|
|
66
|
+
const args = ['log', '--numstat', `--since=${sinceArg}`, '--format=format:%x01%H%x1f%aN%x1f%aI%x1f%s'];
|
|
67
|
+
if (author)
|
|
68
|
+
args.push(`--author=${author}`);
|
|
69
|
+
let raw;
|
|
70
|
+
try {
|
|
71
|
+
raw = execFileSync('git', args, {
|
|
72
|
+
cwd: root,
|
|
73
|
+
encoding: 'utf8',
|
|
74
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
+
maxBuffer: 128 * 1024 * 1024,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
output.printError('git log failed — is this a git repository?');
|
|
80
|
+
return { success: false, exitCode: 1 };
|
|
81
|
+
}
|
|
82
|
+
const buckets = groupByDay(parseStandupLog(raw));
|
|
83
|
+
if (asJson) {
|
|
84
|
+
const count = buckets.reduce((n, b) => n + b.commits.length, 0);
|
|
85
|
+
process.stdout.write(JSON.stringify({ generated: now.toISOString(), author: all ? null : author || null, since: sinceArg, count, buckets }, null, 2) + '\n');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const who = all ? 'everyone' : author || 'you';
|
|
89
|
+
if (buckets.length === 0) {
|
|
90
|
+
output.writeln(output.dim(`no commits for ${who} ${windowLabel} — nothing to report`));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
output.writeln(output.bold(`Standup for ${who} (${windowLabel})`));
|
|
94
|
+
output.writeln(formatStandup(buckets));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { success: true, exitCode: 0 };
|
|
98
|
+
}
|
|
99
|
+
export const standupCommand = {
|
|
100
|
+
name: 'standup',
|
|
101
|
+
description: 'Recall what you committed since your last working day (weekend-aware: Monday reaches back to Friday) — git-standup parity',
|
|
102
|
+
options: [
|
|
103
|
+
{ name: 'all', description: 'show commits from all authors (not just you)', type: 'boolean' },
|
|
104
|
+
{ name: 'author', description: 'filter to a specific author (default: git config user.name)', type: 'string' },
|
|
105
|
+
{ name: 'days', description: 'explicit N-day window (overrides the weekend-aware default)', type: 'string' },
|
|
106
|
+
{ name: 'since', description: 'explicit window, e.g. 2w or "last monday" (git approxidate)', type: 'string' },
|
|
107
|
+
],
|
|
108
|
+
examples: [
|
|
109
|
+
{ command: 'swarmdo standup', description: 'Your commits since the last working day' },
|
|
110
|
+
{ command: 'swarmdo standup --all --since 1w', description: "Everyone's commits in the last week" },
|
|
111
|
+
{ command: 'swarmdo standup --format json', description: 'Machine-readable per-day buckets' },
|
|
112
|
+
],
|
|
113
|
+
action: run,
|
|
114
|
+
};
|
|
115
|
+
export default standupCommand;
|
|
116
|
+
//# sourceMappingURL=standup.js.map
|
|
@@ -103,6 +103,35 @@ function getSwarmStatus(swarmId) {
|
|
|
103
103
|
// Ignore
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
+
// Canonical-store fallback: the `.swarm/*` paths above are the pre-#2085
|
|
107
|
+
// layout. The real swarm store written by swarm_init / agent_spawn / the
|
|
108
|
+
// agent bridge lives at `.swarmdo/swarm/swarm-state.json`. When the legacy
|
|
109
|
+
// paths are empty, read the canonical store so `swarm status` reflects a
|
|
110
|
+
// bridge-created (or MCP-created) swarm instead of falsely reporting "none".
|
|
111
|
+
if (!swarmState) {
|
|
112
|
+
try {
|
|
113
|
+
const swFile = path.join(process.cwd(), '.swarmdo', 'swarm', 'swarm-state.json');
|
|
114
|
+
if (fs.existsSync(swFile)) {
|
|
115
|
+
const swStore = JSON.parse(fs.readFileSync(swFile, 'utf-8'));
|
|
116
|
+
const running = Object.values(swStore.swarms || {})
|
|
117
|
+
.filter((s) => s.status === 'running')
|
|
118
|
+
.sort((a, b) => new Date(String(b.createdAt)).getTime() - new Date(String(a.createdAt)).getTime())[0];
|
|
119
|
+
if (running) {
|
|
120
|
+
const cfg = running.config || {};
|
|
121
|
+
swarmState = {
|
|
122
|
+
id: running.swarmId,
|
|
123
|
+
topology: running.topology,
|
|
124
|
+
strategy: cfg.strategy || 'specialized',
|
|
125
|
+
};
|
|
126
|
+
const enrolled = Array.isArray(running.agents) ? running.agents.length : 0;
|
|
127
|
+
totalAgents = Math.max(totalAgents, enrolled);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Canonical store unavailable — keep the legacy view.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
106
135
|
// Calculate dynamic progress based on actual state
|
|
107
136
|
// If no swarm state, show 0%. Otherwise calculate from completed tasks
|
|
108
137
|
const totalTasks = completedTasks + inProgressTasks + pendingTasks;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { output } from '../output.js';
|
|
16
16
|
import { aggregateBlocks, aggregateUsage, collectUsage, totalUsage, localDateKey, } from '../usage/transcript-usage.js';
|
|
17
17
|
import { collectToolErrors, delegationFromReport } from '../usage/transcript-errors.js';
|
|
18
|
+
import { collectFriction } from '../usage/transcript-friction.js';
|
|
18
19
|
import { computeCacheStats } from '../usage/cache-stats.js';
|
|
19
20
|
import { evaluateGuard } from '../usage/spend-guard.js';
|
|
20
21
|
import { resolvePeriodPair, parseRange, diffPeriods, modelMovers } from '../usage/diff.js';
|
|
@@ -201,6 +202,53 @@ function runErrorsView(ctx, report) {
|
|
|
201
202
|
}
|
|
202
203
|
return { success: true, exitCode: 0 };
|
|
203
204
|
}
|
|
205
|
+
/** Friction view — how often you interrupt Claude (ESC mid-turn), which tool was
|
|
206
|
+
* running when you did, and what KIND of errors dominate. The sniffly axes that
|
|
207
|
+
* `usage errors` (error rate) left on the table (#102). */
|
|
208
|
+
function runFrictionView(ctx, report) {
|
|
209
|
+
if (ctx.flags.json === true) {
|
|
210
|
+
output.writeln(JSON.stringify(report, null, 2));
|
|
211
|
+
return { success: true, exitCode: 0 };
|
|
212
|
+
}
|
|
213
|
+
const pct = (v) => `${(v * 100).toFixed(1)}%`;
|
|
214
|
+
if (ctx.flags.csv === true) {
|
|
215
|
+
// Category table export (parity with `usage --csv`).
|
|
216
|
+
const headers = ['category', 'count', 'share'];
|
|
217
|
+
const rows = report.categories.map((c) => [c.category, c.count, c.share.toFixed(4)]);
|
|
218
|
+
output.writeln(toCsv(headers, rows));
|
|
219
|
+
return { success: true, exitCode: 0 };
|
|
220
|
+
}
|
|
221
|
+
output.writeln(output.bold('Claude Code usage — friction'));
|
|
222
|
+
if (report.interruptions === 0 && report.totalErrors === 0) {
|
|
223
|
+
output.writeln(output.info(`no friction detected in ${report.filesScanned} transcript files`));
|
|
224
|
+
return { success: true, exitCode: 0 };
|
|
225
|
+
}
|
|
226
|
+
output.writeln(output.dim(`${report.filesScanned} files · ${fmtInt(report.interruptions)} interruption${report.interruptions === 1 ? '' : 's'} across ${fmtInt(report.assistantTurns)} assistant turns (${pct(report.interruptionRate)})`));
|
|
227
|
+
if (report.byTool.length > 0) {
|
|
228
|
+
output.writeln('');
|
|
229
|
+
output.writeln(output.bold('Interrupted while running'));
|
|
230
|
+
output.printTable({
|
|
231
|
+
columns: [
|
|
232
|
+
{ key: 'tool', header: 'Tool / context', width: 22 },
|
|
233
|
+
{ key: 'count', header: 'Interrupts', width: 12, align: 'right' },
|
|
234
|
+
],
|
|
235
|
+
data: report.byTool.slice(0, 15).map((t) => ({ tool: t.tool, count: fmtInt(t.interruptions) })),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (report.categories.length > 0) {
|
|
239
|
+
output.writeln('');
|
|
240
|
+
output.writeln(output.bold('Error categories') + output.dim(` (${fmtInt(report.totalErrors)} tool errors)`));
|
|
241
|
+
output.printTable({
|
|
242
|
+
columns: [
|
|
243
|
+
{ key: 'category', header: 'Category', width: 14 },
|
|
244
|
+
{ key: 'count', header: 'Count', width: 10, align: 'right' },
|
|
245
|
+
{ key: 'share', header: 'Share', width: 8, align: 'right' },
|
|
246
|
+
],
|
|
247
|
+
data: report.categories.map((c) => ({ category: c.category, count: fmtInt(c.count), share: pct(c.share) })),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return { success: true, exitCode: 0 };
|
|
251
|
+
}
|
|
204
252
|
/** Cache view — prompt-cache efficiency + $ saved by caching (the #1 cost lever). */
|
|
205
253
|
function runCacheView(ctx, collection) {
|
|
206
254
|
const stats = computeCacheStats(aggregateUsage(collection.events, 'model'));
|
|
@@ -547,6 +595,13 @@ async function run(ctx) {
|
|
|
547
595
|
const dirs = typeof dirFlag === 'string' ? [dirFlag] : Array.isArray(dirFlag) ? dirFlag.map(String) : undefined;
|
|
548
596
|
return runErrorsView(ctx, collectToolErrors({ dirs, since, until }));
|
|
549
597
|
}
|
|
598
|
+
if (viewName === 'friction') {
|
|
599
|
+
const since = typeof ctx.flags.since === 'string' ? ctx.flags.since : undefined;
|
|
600
|
+
const until = typeof ctx.flags.until === 'string' ? ctx.flags.until : undefined;
|
|
601
|
+
const dirFlag = ctx.flags.dir;
|
|
602
|
+
const dirs = typeof dirFlag === 'string' ? [dirFlag] : Array.isArray(dirFlag) ? dirFlag.map(String) : undefined;
|
|
603
|
+
return runFrictionView(ctx, collectFriction({ dirs, since, until }));
|
|
604
|
+
}
|
|
550
605
|
if (viewName === 'cache') {
|
|
551
606
|
const since = typeof ctx.flags.since === 'string' ? ctx.flags.since : undefined;
|
|
552
607
|
const until = typeof ctx.flags.until === 'string' ? ctx.flags.until : undefined;
|
|
@@ -574,7 +629,7 @@ async function run(ctx) {
|
|
|
574
629
|
}
|
|
575
630
|
const view = VIEWS[viewName];
|
|
576
631
|
if (!view) {
|
|
577
|
-
output.writeln(output.error(`unknown view: ${viewName} (expected ${Object.keys(VIEWS).join('|')}|blocks|errors|cache|guard|diff|reflect|limits)`));
|
|
632
|
+
output.writeln(output.error(`unknown view: ${viewName} (expected ${Object.keys(VIEWS).join('|')}|blocks|errors|friction|cache|guard|diff|reflect|limits)`));
|
|
578
633
|
return { success: false, exitCode: 1 };
|
|
579
634
|
}
|
|
580
635
|
const since = typeof ctx.flags.since === 'string' ? ctx.flags.since : undefined;
|
|
@@ -667,6 +722,7 @@ export const usageCommand = {
|
|
|
667
722
|
{ command: 'swarmdo usage projects --json', description: 'Per-project totals as JSON' },
|
|
668
723
|
{ command: 'swarmdo usage monthly --csv > usage.csv', description: 'Export monthly spend to CSV' },
|
|
669
724
|
{ command: 'swarmdo cost sessions --limit 10', description: 'Ten most expensive sessions (alias)' },
|
|
725
|
+
{ command: 'swarmdo usage friction', description: 'How often you interrupt Claude + which error kinds dominate' },
|
|
670
726
|
],
|
|
671
727
|
action: run,
|
|
672
728
|
};
|
|
@@ -14,7 +14,17 @@ export declare const RETENTION: {
|
|
|
14
14
|
export declare function storePath(cwd: string): string;
|
|
15
15
|
export declare function loadMailbox(cwd: string): Mailbox;
|
|
16
16
|
export declare function saveMailbox(cwd: string, box: Mailbox): void;
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* This session's name, in precedence order:
|
|
19
|
+
* explicit override (flag/param)
|
|
20
|
+
* → $SWARMDO_SESSION (set by Claude Code)
|
|
21
|
+
* → $SWARMDO_AGENT (harness-neutral — Codex/Copilot/pi agents export this)
|
|
22
|
+
* → hostname → "me".
|
|
23
|
+
*
|
|
24
|
+
* The $SWARMDO_AGENT fallback lets a non-Claude agent declare a stable identity
|
|
25
|
+
* once (instead of passing `from`/`--self` on every call); without it, every
|
|
26
|
+
* agent on a host collapses to the shared hostname and direct addressing breaks.
|
|
27
|
+
*/
|
|
18
28
|
export declare function resolveSelf(flag?: unknown): string;
|
|
19
29
|
/** Crypto-strong message id. */
|
|
20
30
|
export declare function newMessageId(): string;
|
|
@@ -32,10 +32,24 @@ export function saveMailbox(cwd, box) {
|
|
|
32
32
|
mkdirSync(dirname(p), { recursive: true });
|
|
33
33
|
writeFileSync(p, JSON.stringify(box, null, 2) + '\n', 'utf-8');
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* This session's name, in precedence order:
|
|
37
|
+
* explicit override (flag/param)
|
|
38
|
+
* → $SWARMDO_SESSION (set by Claude Code)
|
|
39
|
+
* → $SWARMDO_AGENT (harness-neutral — Codex/Copilot/pi agents export this)
|
|
40
|
+
* → hostname → "me".
|
|
41
|
+
*
|
|
42
|
+
* The $SWARMDO_AGENT fallback lets a non-Claude agent declare a stable identity
|
|
43
|
+
* once (instead of passing `from`/`--self` on every call); without it, every
|
|
44
|
+
* agent on a host collapses to the shared hostname and direct addressing breaks.
|
|
45
|
+
*/
|
|
36
46
|
export function resolveSelf(flag) {
|
|
37
47
|
const f = typeof flag === 'string' ? flag.trim() : '';
|
|
38
|
-
return f ||
|
|
48
|
+
return (f ||
|
|
49
|
+
(process.env.SWARMDO_SESSION || '').trim() ||
|
|
50
|
+
(process.env.SWARMDO_AGENT || '').trim() ||
|
|
51
|
+
hostname() ||
|
|
52
|
+
'me');
|
|
39
53
|
}
|
|
40
54
|
/** Crypto-strong message id. */
|
|
41
55
|
export function newMessageId() {
|