swarmdo 1.45.0 → 1.54.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/settings.local.json +7 -0
- package/README.md +6 -2
- 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/commands/agent.js +133 -1
- package/v3/@swarmdo/cli/dist/src/commands/config.js +32 -1
- package/v3/@swarmdo/cli/dist/src/commands/hidden-coupling.d.ts +19 -0
- package/v3/@swarmdo/cli/dist/src/commands/hidden-coupling.js +92 -0
- package/v3/@swarmdo/cli/dist/src/commands/index.js +14 -4
- package/v3/@swarmdo/cli/dist/src/commands/ownership.d.ts +18 -0
- package/v3/@swarmdo/cli/dist/src/commands/ownership.js +88 -0
- package/v3/@swarmdo/cli/dist/src/commands/pack.js +4 -4
- 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/config-lint/commands-lint.d.ts +31 -0
- package/v3/@swarmdo/cli/dist/src/config-lint/commands-lint.js +127 -0
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.d.ts +5 -0
- package/v3/@swarmdo/cli/dist/src/config-lint/lint.js +4 -0
- package/v3/@swarmdo/cli/dist/src/coupling/hidden.d.ts +45 -0
- package/v3/@swarmdo/cli/dist/src/coupling/hidden.js +66 -0
- package/v3/@swarmdo/cli/dist/src/init/helpers-generator.js +41 -1
- package/v3/@swarmdo/cli/dist/src/mcp-client.js +2 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/agent-tools.js +166 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/index.js +1 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/ownership-tools.d.ts +14 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/ownership-tools.js +51 -0
- package/v3/@swarmdo/cli/dist/src/mcp-tools/profiles.js +1 -0
- 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/ownership/ownership.d.ts +61 -0
- package/v3/@swarmdo/cli/dist/src/ownership/ownership.js +151 -0
- package/v3/@swarmdo/cli/dist/src/pack/pack.d.ts +1 -1
- package/v3/@swarmdo/cli/dist/src/pack/pack.js +12 -4
- package/v3/@swarmdo/cli/dist/src/redact/redact.js +28 -9
- 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/package.json +1 -1
- package/v3/@swarmdo/cli-core/dist/src/index.js +1 -1
- package/v3/@swarmdo/codex/dist/cli.js +0 -0
- package/v3/@swarmdo/hooks/dist/cli/guidance-cli.js +0 -0
- package/v3/@swarmdo/hooks/dist/statusline/index.d.ts +3 -3
- package/v3/@swarmdo/hooks/dist/statusline/index.js +3 -3
- package/v3/@swarmdo/integration/dist/__tests__/agentic-flow-agent.test.js +1 -4
- package/v3/@swarmdo/neural/dist/flash-attention.d.ts +1 -1
- package/v3/@swarmdo/neural/dist/flash-attention.js +1 -1
- package/v3/@swarmdo/plugin-agent-federation/dist/plugin.js +1 -1
- package/v3/@swarmdo/plugin-agent-federation/dist/transport/midstream-aware-loader.js +2 -2
- package/v3/@swarmdo/plugin-iot-cognitum/dist/application/index.d.ts +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/application/index.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/application/iot-coordinator.d.ts +138 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/application/iot-coordinator.js +418 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/bin.d.ts +15 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/bin.js +240 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/cli-commands.d.ts +7 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/cli-commands.js +508 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/device-agent.d.ts +54 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/device-agent.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/device-fleet.d.ts +46 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/device-fleet.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/device-trust-level.d.ts +18 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/device-trust-level.js +28 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/index.d.ts +5 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/index.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/telemetry.d.ts +35 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/entities/telemetry.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/device-repository.d.ts +11 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/device-repository.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/fleet-repository.d.ts +9 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/fleet-repository.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/index.d.ts +5 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/index.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/telemetry-repository.d.ts +12 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/telemetry-repository.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/trust-history-repository.d.ts +19 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/repositories/trust-history-repository.js +2 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/anomaly-detection-service.d.ts +44 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/anomaly-detection-service.js +117 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/device-lifecycle-service.d.ts +70 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/device-lifecycle-service.js +194 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/firmware-orchestration-service.d.ts +69 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/firmware-orchestration-service.js +139 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/fleet-topology-service.d.ts +34 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/fleet-topology-service.js +101 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/index.d.ts +10 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/index.js +10 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/mesh-service.d.ts +64 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/mesh-service.js +70 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/sona-integration-service.d.ts +44 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/sona-integration-service.js +79 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/telemetry-ingestion-service.d.ts +37 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/telemetry-ingestion-service.js +39 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/telemetry-service.d.ts +67 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/telemetry-service.js +58 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/witness-verification-service.d.ts +45 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/domain/services/witness-verification-service.js +63 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/index.d.ts +26 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/index.js +17 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/agentdb-telemetry-repository.d.ts +51 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/agentdb-telemetry-repository.js +110 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/in-memory-device-repository.d.ts +13 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/in-memory-device-repository.js +25 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/in-memory-fleet-repository.d.ts +11 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/in-memory-fleet-repository.js +19 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/in-memory-trust-history-repository.d.ts +11 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/in-memory-trust-history-repository.js +27 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/index.d.ts +6 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/index.js +6 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/seed-client-factory.d.ts +41 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/infrastructure/seed-client-factory.js +64 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/mcp-tools.d.ts +8 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/mcp-tools.js +703 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/plugin.d.ts +23 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/plugin.js +239 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/anomaly-scan-worker.d.ts +17 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/anomaly-scan-worker.js +42 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/firmware-watch-worker.d.ts +18 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/firmware-watch-worker.js +45 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/health-probe-worker.d.ts +19 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/health-probe-worker.js +50 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/mesh-sync-worker.d.ts +19 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/mesh-sync-worker.js +47 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/telemetry-ingest-worker.d.ts +17 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/telemetry-ingest-worker.js +40 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/witness-audit-worker.d.ts +18 -0
- package/v3/@swarmdo/plugin-iot-cognitum/dist/workers/witness-audit-worker.js +51 -0
- package/v3/@swarmdo/providers/dist/swarmvector-provider.d.ts +1 -1
- package/v3/@swarmdo/providers/dist/swarmvector-provider.js +1 -1
- package/v3/@swarmdo/security/dist/input-validator.d.ts +6 -6
- package/v3/@swarmdo/shared/dist/hooks/safety/git-commit.js +1 -1
- package/v3/@swarmdo/shared/dist/mcp/transport/http.js +1 -8
- package/v3/@swarmdo/testing/dist/fixtures/agent-fixtures.d.ts +214 -0
- package/v3/@swarmdo/testing/dist/fixtures/agent-fixtures.js +595 -0
- package/v3/@swarmdo/testing/dist/fixtures/agents.d.ts +76 -0
- package/v3/@swarmdo/testing/dist/fixtures/agents.js +158 -0
- package/v3/@swarmdo/testing/dist/fixtures/configurations.d.ts +157 -0
- package/v3/@swarmdo/testing/dist/fixtures/configurations.js +337 -0
- package/v3/@swarmdo/testing/dist/fixtures/index.d.ts +10 -0
- package/v3/@swarmdo/testing/dist/fixtures/index.js +17 -0
- package/v3/@swarmdo/testing/dist/fixtures/mcp-fixtures.d.ts +329 -0
- package/v3/@swarmdo/testing/dist/fixtures/mcp-fixtures.js +714 -0
- package/v3/@swarmdo/testing/dist/fixtures/memory-entries.d.ts +159 -0
- package/v3/@swarmdo/testing/dist/fixtures/memory-entries.js +262 -0
- package/v3/@swarmdo/testing/dist/fixtures/memory-fixtures.d.ts +290 -0
- package/v3/@swarmdo/testing/dist/fixtures/memory-fixtures.js +547 -0
- package/v3/@swarmdo/testing/dist/fixtures/swarm-fixtures.d.ts +289 -0
- package/v3/@swarmdo/testing/dist/fixtures/swarm-fixtures.js +534 -0
- package/v3/@swarmdo/testing/dist/fixtures/tasks.d.ts +106 -0
- package/v3/@swarmdo/testing/dist/fixtures/tasks.js +229 -0
- package/v3/@swarmdo/testing/dist/helpers/assertion-helpers.d.ts +213 -0
- package/v3/@swarmdo/testing/dist/helpers/assertion-helpers.js +435 -0
- package/v3/@swarmdo/testing/dist/helpers/assertions.d.ts +135 -0
- package/v3/@swarmdo/testing/dist/helpers/assertions.js +217 -0
- package/v3/@swarmdo/testing/dist/helpers/create-mock.d.ts +96 -0
- package/v3/@swarmdo/testing/dist/helpers/create-mock.js +164 -0
- package/v3/@swarmdo/testing/dist/helpers/index.d.ts +14 -0
- package/v3/@swarmdo/testing/dist/helpers/index.js +22 -0
- package/v3/@swarmdo/testing/dist/helpers/mock-factory.d.ts +323 -0
- package/v3/@swarmdo/testing/dist/helpers/mock-factory.js +350 -0
- package/v3/@swarmdo/testing/dist/helpers/setup-teardown.d.ts +271 -0
- package/v3/@swarmdo/testing/dist/helpers/setup-teardown.js +446 -0
- package/v3/@swarmdo/testing/dist/helpers/swarm-instance.d.ts +142 -0
- package/v3/@swarmdo/testing/dist/helpers/swarm-instance.js +205 -0
- package/v3/@swarmdo/testing/dist/helpers/test-application.d.ts +190 -0
- package/v3/@swarmdo/testing/dist/helpers/test-application.js +138 -0
- package/v3/@swarmdo/testing/dist/helpers/test-utils.d.ts +274 -0
- package/v3/@swarmdo/testing/dist/helpers/test-utils.js +456 -0
- package/v3/@swarmdo/testing/dist/index.d.ts +42 -0
- package/v3/@swarmdo/testing/dist/index.js +66 -0
- package/v3/@swarmdo/testing/dist/mocks/index.d.ts +8 -0
- package/v3/@swarmdo/testing/dist/mocks/index.js +10 -0
- package/v3/@swarmdo/testing/dist/mocks/mock-mcp-client.d.ts +206 -0
- package/v3/@swarmdo/testing/dist/mocks/mock-mcp-client.js +575 -0
- package/v3/@swarmdo/testing/dist/mocks/mock-services.d.ts +287 -0
- package/v3/@swarmdo/testing/dist/mocks/mock-services.js +563 -0
- package/v3/@swarmdo/testing/dist/regression/api-contract.d.ts +109 -0
- package/v3/@swarmdo/testing/dist/regression/api-contract.js +374 -0
- package/v3/@swarmdo/testing/dist/regression/index.d.ts +18 -0
- package/v3/@swarmdo/testing/dist/regression/index.js +18 -0
- package/v3/@swarmdo/testing/dist/regression/integration-regression.d.ts +67 -0
- package/v3/@swarmdo/testing/dist/regression/integration-regression.js +334 -0
- package/v3/@swarmdo/testing/dist/regression/performance-baseline.d.ts +106 -0
- package/v3/@swarmdo/testing/dist/regression/performance-baseline.js +268 -0
- package/v3/@swarmdo/testing/dist/regression/regression-runner.d.ts +124 -0
- package/v3/@swarmdo/testing/dist/regression/regression-runner.js +227 -0
- package/v3/@swarmdo/testing/dist/regression/security-regression.d.ts +73 -0
- package/v3/@swarmdo/testing/dist/regression/security-regression.js +258 -0
- package/v3/@swarmdo/testing/dist/setup.d.ts +43 -0
- package/v3/@swarmdo/testing/dist/setup.js +91 -0
- package/v3/@swarmdo/testing/dist/v2-compat/api-compat.test.d.ts +10 -0
- package/v3/@swarmdo/testing/dist/v2-compat/api-compat.test.js +433 -0
- package/v3/@swarmdo/testing/dist/v2-compat/cli-compat.test.d.ts +10 -0
- package/v3/@swarmdo/testing/dist/v2-compat/cli-compat.test.js +352 -0
- package/v3/@swarmdo/testing/dist/v2-compat/compatibility-validator.d.ts +206 -0
- package/v3/@swarmdo/testing/dist/v2-compat/compatibility-validator.js +839 -0
- package/v3/@swarmdo/testing/dist/v2-compat/hooks-compat.test.d.ts +10 -0
- package/v3/@swarmdo/testing/dist/v2-compat/hooks-compat.test.js +428 -0
- package/v3/@swarmdo/testing/dist/v2-compat/index.d.ts +38 -0
- package/v3/@swarmdo/testing/dist/v2-compat/index.js +41 -0
- package/v3/@swarmdo/testing/dist/v2-compat/mcp-compat.test.d.ts +10 -0
- package/v3/@swarmdo/testing/dist/v2-compat/mcp-compat.test.js +419 -0
- package/v3/@swarmdo/testing/dist/v2-compat/report-generator.d.ts +34 -0
- package/v3/@swarmdo/testing/dist/v2-compat/report-generator.js +372 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo hidden-coupling` — files that CHANGE TOGETHER but have no import edge.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo hidden-coupling # co-change pairs the code doesn't explain
|
|
5
|
+
* swarmdo hidden-coupling --since 6mo --min-shared 3
|
|
6
|
+
* swarmdo hidden-coupling --format json | --csv
|
|
7
|
+
*
|
|
8
|
+
* "Logical coupling minus structural coupling": pairs with a high co-change
|
|
9
|
+
* `degree` (from `coupling`) yet NO import edge connecting them (from
|
|
10
|
+
* `codegraph`). A dependency real enough to move two files in lockstep but
|
|
11
|
+
* invisible in the code — the co-edit an agent following imports would miss, or
|
|
12
|
+
* a missing abstraction a reviewer should question. Composes the two capture
|
|
13
|
+
* paths verbatim: git-log + `computeCoupling`, and the codegraph index load.
|
|
14
|
+
* Engine (../coupling/hidden.ts) is pure + tested.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { output } from '../output.js';
|
|
18
|
+
import { parseGitLog } from '../hotspots/hotspots.js';
|
|
19
|
+
import { computeCoupling } from '../coupling/coupling.js';
|
|
20
|
+
import { computeHiddenCoupling, formatHiddenCoupling, hiddenCouplingToCsv } from '../coupling/hidden.js';
|
|
21
|
+
import { loadIndex, scanRepo, saveIndex } from '../codegraph/store.js';
|
|
22
|
+
import { normalizeSince } from '../util/since.js';
|
|
23
|
+
/** Read a numeric flag that the parser may deliver as a number OR a string. */
|
|
24
|
+
function numFlag(v, def) {
|
|
25
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' ? parseInt(v, 10) : NaN;
|
|
26
|
+
return Number.isFinite(n) ? n : def;
|
|
27
|
+
}
|
|
28
|
+
async function run(ctx) {
|
|
29
|
+
const root = ctx.cwd || process.cwd();
|
|
30
|
+
const since = typeof ctx.flags.since === 'string' ? ctx.flags.since : '1 year ago';
|
|
31
|
+
const top = numFlag(ctx.flags.top, 30);
|
|
32
|
+
const minShared = numFlag(ctx.flags['min-shared'], 2);
|
|
33
|
+
const maxFiles = numFlag(ctx.flags['max-files'], 30);
|
|
34
|
+
const asJson = ctx.flags.format === 'json';
|
|
35
|
+
const asCsv = ctx.flags.csv === true;
|
|
36
|
+
// Capture 1: the full co-change history (same `--numstat` dump `coupling` uses;
|
|
37
|
+
// NO pathspec filter — that would strip co-changed files from each commit).
|
|
38
|
+
const args = ['log', '--no-merges', '--numstat', `--since=${normalizeSince(since)}`, '--format=format:%x01%H%x1f%aN%x1f%aI'];
|
|
39
|
+
let raw;
|
|
40
|
+
try {
|
|
41
|
+
raw = execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 128 * 1024 * 1024 });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
output.printError('git log failed — is this a git repository?');
|
|
45
|
+
return { success: false, exitCode: 1 };
|
|
46
|
+
}
|
|
47
|
+
// Rank ALL coupling pairs (no top yet — we cap the HIDDEN set after filtering).
|
|
48
|
+
const pairs = computeCoupling(parseGitLog(raw), { minShared, maxFiles });
|
|
49
|
+
// Capture 2: the static import graph (prefer a saved index; build one if absent).
|
|
50
|
+
let index = loadIndex(root);
|
|
51
|
+
if (!index) {
|
|
52
|
+
index = scanRepo(root);
|
|
53
|
+
try {
|
|
54
|
+
saveIndex(root, index);
|
|
55
|
+
}
|
|
56
|
+
catch { /* read-only fs — in-memory is fine */ }
|
|
57
|
+
}
|
|
58
|
+
const hidden = computeHiddenCoupling(pairs, index.imports, { top: top > 0 ? top : undefined });
|
|
59
|
+
if (asCsv) {
|
|
60
|
+
process.stdout.write(hiddenCouplingToCsv(hidden) + '\n');
|
|
61
|
+
}
|
|
62
|
+
else if (asJson) {
|
|
63
|
+
process.stdout.write(JSON.stringify({ generated: new Date().toISOString(), since, minShared, coupled: pairs.length, count: hidden.length, hidden }, null, 2) + '\n');
|
|
64
|
+
}
|
|
65
|
+
else if (hidden.length === 0) {
|
|
66
|
+
output.writeln(output.dim(`no hidden coupling found — every co-change pair has an import edge (or none met --min-shared ${minShared} / --since ${since})`));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
output.writeln(output.bold(`Hidden coupling (co-change with NO import edge, since ${since}, min-shared ${minShared})`));
|
|
70
|
+
output.writeln(formatHiddenCoupling(hidden));
|
|
71
|
+
output.writeln(output.dim(`${hidden.length} hidden of ${pairs.length} coupled pair(s) — these move together but nothing in the code links them`));
|
|
72
|
+
}
|
|
73
|
+
return { success: true, exitCode: 0 };
|
|
74
|
+
}
|
|
75
|
+
export const hiddenCouplingCommand = {
|
|
76
|
+
name: 'hidden-coupling',
|
|
77
|
+
description: 'Rank file pairs that change together in git history but have NO import edge (logical minus structural coupling) — the co-edit `affected` can\'t see',
|
|
78
|
+
options: [
|
|
79
|
+
{ name: 'since', description: 'history window, e.g. 90d or "3 months ago" (default 1 year)', type: 'string' },
|
|
80
|
+
{ name: 'min-shared', description: 'drop pairs sharing fewer than N commits (default 2)', type: 'string' },
|
|
81
|
+
{ name: 'max-files', description: 'skip commits touching more than N files (default 30; 0 = no cap)', type: 'string' },
|
|
82
|
+
{ name: 'top', description: 'keep only the top N hidden pairs (default 30; 0 = all)', type: 'string' },
|
|
83
|
+
{ name: 'csv', description: 'export the ranking as CSV (for spreadsheets)', type: 'boolean' },
|
|
84
|
+
],
|
|
85
|
+
examples: [
|
|
86
|
+
{ command: 'swarmdo hidden-coupling --since 6mo --min-shared 3', description: 'Strong co-change pairs with no code link, last 6 months' },
|
|
87
|
+
{ command: 'swarmdo hidden-coupling --format json', description: 'Machine-readable hidden-coupling report' },
|
|
88
|
+
],
|
|
89
|
+
action: run,
|
|
90
|
+
};
|
|
91
|
+
export default hiddenCouplingCommand;
|
|
92
|
+
//# sourceMappingURL=hidden-coupling.js.map
|
|
@@ -54,6 +54,16 @@ const commandLoaders = {
|
|
|
54
54
|
// Temporal (co-change) coupling from git history (code-maat demand) — file
|
|
55
55
|
// pairs that change together; the empirical complement to `affected`.
|
|
56
56
|
coupling: () => import('./coupling.js'),
|
|
57
|
+
// Per-file knowledge map + bus factor from git history (code-maat main-dev
|
|
58
|
+
// demand) — who owns each file, and what breaks if they leave.
|
|
59
|
+
ownership: () => import('./ownership.js'),
|
|
60
|
+
// Hidden coupling — co-change pairs with NO import edge (logical minus
|
|
61
|
+
// structural coupling); joins `coupling`'s git capture with codegraph's graph.
|
|
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
|
|
57
67
|
// JUnit/TAP test-result parser (dorny/test-reporter demand) — raw results →
|
|
58
68
|
// failing test + file:line + message; the front-half of the test→fix loop.
|
|
59
69
|
testreport: () => import('./testreport.js'),
|
|
@@ -296,7 +306,7 @@ export async function getCommandsByCategory() {
|
|
|
296
306
|
// three slots from 'statusline' onward (completionsCmd received the
|
|
297
307
|
// statusline module, analyzeCmd received completions, …), scrambling the
|
|
298
308
|
// categorized help. Every load now has a named slot.
|
|
299
|
-
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, testreportCmd, compactSnapshotCmd,] = await Promise.all([
|
|
309
|
+
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,] = await Promise.all([
|
|
300
310
|
loadCommand('daemon'), loadCommand('doctor'), loadCommand('embeddings'), loadCommand('neural'),
|
|
301
311
|
loadCommand('performance'), loadCommand('security'), loadCommand('swarmvector'), loadCommand('hive-mind'),
|
|
302
312
|
loadCommand('config'), loadCommand('statusline'), loadCommand('compress'), loadCommand('efficiency'),
|
|
@@ -304,7 +314,7 @@ export async function getCommandsByCategory() {
|
|
|
304
314
|
loadCommand('analyze'), loadCommand('route'), loadCommand('progress'), loadCommand('providers'),
|
|
305
315
|
loadCommand('plugins'), loadCommand('deployment'), loadCommand('claims'), loadCommand('issues'),
|
|
306
316
|
loadCommand('update'), loadCommand('process'), loadCommand('guidance'), loadCommand('appliance'),
|
|
307
|
-
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('testreport'), loadCommand('compact-snapshot'),
|
|
317
|
+
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'),
|
|
308
318
|
]);
|
|
309
319
|
return {
|
|
310
320
|
primary: [
|
|
@@ -320,10 +330,10 @@ export async function getCommandsByCategory() {
|
|
|
320
330
|
utility: [
|
|
321
331
|
configCmd, doctorCmd, daemonCmd, completionsCmd,
|
|
322
332
|
migrateCmd, workflowCmd, demoCmd,
|
|
323
|
-
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, couplingCmd, testreportCmd, compactSnapshotCmd, efficiencyCmd,
|
|
333
|
+
statuslineCmd, compressCmd, compactCmd, redactCmd, packCmd, envCmd, licenseCmd, sbomCmd, applyCmd, hotspotsCmd, affectedCmd, cyclesCmd, couplingCmd, ownershipCmd, hiddenCouplingCmd, testreportCmd, compactSnapshotCmd, efficiencyCmd,
|
|
324
334
|
].filter(Boolean),
|
|
325
335
|
analysis: [
|
|
326
|
-
analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd,
|
|
336
|
+
analyzeCmd, routeCmd, progressCmd, usageCmd, hudCmd, codegraphCmd, standupCmd,
|
|
327
337
|
].filter(Boolean),
|
|
328
338
|
management: [
|
|
329
339
|
providersCmd, pluginsCmd, deploymentCmd, claimsCmd,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo ownership` — per-file knowledge map + BUS FACTOR mined from git history.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo ownership # knowledge map, most fragile files first
|
|
5
|
+
* swarmdo ownership src --since 90d # scope to a path + window
|
|
6
|
+
* swarmdo ownership --top 10 --format json
|
|
7
|
+
* swarmdo ownership --csv > ownership.csv
|
|
8
|
+
*
|
|
9
|
+
* "Who owns each file, and what breaks if they leave?" — the code-maat
|
|
10
|
+
* main-dev / bus-factor analysis. Files where one author owns nearly all the
|
|
11
|
+
* churn (bus factor 1) are key-person risks. Pairs with `hotspots` (change-risk)
|
|
12
|
+
* and `coupling` (co-change). Engine (../ownership/ownership.ts) is pure +
|
|
13
|
+
* tested; this captures the git log (the same `--numstat` dump those use).
|
|
14
|
+
*/
|
|
15
|
+
import type { Command } from '../types.js';
|
|
16
|
+
export declare const ownershipCommand: Command;
|
|
17
|
+
export default ownershipCommand;
|
|
18
|
+
//# sourceMappingURL=ownership.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `swarmdo ownership` — per-file knowledge map + BUS FACTOR mined from git history.
|
|
3
|
+
*
|
|
4
|
+
* swarmdo ownership # knowledge map, most fragile files first
|
|
5
|
+
* swarmdo ownership src --since 90d # scope to a path + window
|
|
6
|
+
* swarmdo ownership --top 10 --format json
|
|
7
|
+
* swarmdo ownership --csv > ownership.csv
|
|
8
|
+
*
|
|
9
|
+
* "Who owns each file, and what breaks if they leave?" — the code-maat
|
|
10
|
+
* main-dev / bus-factor analysis. Files where one author owns nearly all the
|
|
11
|
+
* churn (bus factor 1) are key-person risks. Pairs with `hotspots` (change-risk)
|
|
12
|
+
* and `coupling` (co-change). Engine (../ownership/ownership.ts) is pure +
|
|
13
|
+
* tested; this captures the git log (the same `--numstat` dump those use).
|
|
14
|
+
*/
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { output } from '../output.js';
|
|
17
|
+
import { parseGitLog } from '../hotspots/hotspots.js';
|
|
18
|
+
import { computeOwnership, repoBusFactor, formatOwnership, ownershipToCsv } from '../ownership/ownership.js';
|
|
19
|
+
import { normalizeSince } from '../util/since.js';
|
|
20
|
+
/** Read a numeric flag that the parser may deliver as a number OR a string. */
|
|
21
|
+
function numFlag(v, def) {
|
|
22
|
+
const n = typeof v === 'number' ? v : typeof v === 'string' ? parseInt(v, 10) : NaN;
|
|
23
|
+
return Number.isFinite(n) ? n : def;
|
|
24
|
+
}
|
|
25
|
+
async function run(ctx) {
|
|
26
|
+
const root = ctx.cwd || process.cwd();
|
|
27
|
+
const pathArg = ctx.args[0];
|
|
28
|
+
const since = typeof ctx.flags.since === 'string' ? ctx.flags.since : '1 year ago';
|
|
29
|
+
const top = numFlag(ctx.flags.top, 40);
|
|
30
|
+
const minChurn = numFlag(ctx.flags['min-churn'], 1);
|
|
31
|
+
const asJson = ctx.flags.format === 'json';
|
|
32
|
+
const asCsv = ctx.flags.csv === true;
|
|
33
|
+
// Same SOH-delimited numstat capture `hotspots` uses. `%aN` folds author
|
|
34
|
+
// name/email variants through `.mailmap` so one person is one owner. A
|
|
35
|
+
// positional pathspec scopes BOTH the file rows and the truck factor to that
|
|
36
|
+
// subtree (unlike `coupling`, per-file ownership survives a pathspec fine).
|
|
37
|
+
const args = ['log', '--no-merges', '--numstat', `--since=${normalizeSince(since)}`, '--format=format:%x01%H%x1f%aN%x1f%aI'];
|
|
38
|
+
if (pathArg)
|
|
39
|
+
args.push('--', pathArg);
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 128 * 1024 * 1024 });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
output.printError('git log failed — is this a git repository?');
|
|
46
|
+
return { success: false, exitCode: 1 };
|
|
47
|
+
}
|
|
48
|
+
const commits = parseGitLog(raw);
|
|
49
|
+
const files = computeOwnership(commits, { minChurn, top: top > 0 ? top : undefined });
|
|
50
|
+
const repo = repoBusFactor(commits);
|
|
51
|
+
if (asCsv) {
|
|
52
|
+
process.stdout.write(ownershipToCsv(files) + '\n');
|
|
53
|
+
}
|
|
54
|
+
else if (asJson) {
|
|
55
|
+
process.stdout.write(JSON.stringify({ generated: new Date().toISOString(), since, minChurn, repoBusFactor: repo, count: files.length, ownership: files }, null, 2) + '\n');
|
|
56
|
+
}
|
|
57
|
+
else if (files.length === 0) {
|
|
58
|
+
output.writeln(output.dim('no ownership data found — no matching git history in the window'));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
output.writeln(output.bold(`File ownership & bus factor (since ${since})`));
|
|
62
|
+
if (repo.factor > 0) {
|
|
63
|
+
const who = repo.authors.length <= 3 ? repo.authors.join(', ') : `${repo.authors.slice(0, 3).join(', ')} +${repo.authors.length - 3}`;
|
|
64
|
+
const flag = repo.factor === 1 ? ' ⚠ single point of knowledge' : '';
|
|
65
|
+
output.writeln(output.dim(`Repo truck factor: ${repo.factor} (${who} own >50% of churn)${flag}`));
|
|
66
|
+
}
|
|
67
|
+
output.writeln(formatOwnership(files));
|
|
68
|
+
}
|
|
69
|
+
return { success: true, exitCode: 0 };
|
|
70
|
+
}
|
|
71
|
+
export const ownershipCommand = {
|
|
72
|
+
name: 'ownership',
|
|
73
|
+
description: 'Map per-file authorship concentration + bus factor from git history — find the key-person risks (code-maat main-dev / knowledge map)',
|
|
74
|
+
options: [
|
|
75
|
+
{ name: 'since', description: 'history window, e.g. 90d or "3 months ago" (default 1 year)', type: 'string' },
|
|
76
|
+
{ name: 'top', description: 'keep only the top N files (default 40; 0 = all)', type: 'string' },
|
|
77
|
+
{ name: 'min-churn', description: 'drop files with total churn below N (default 1)', type: 'string' },
|
|
78
|
+
{ name: 'csv', description: 'export the knowledge map as CSV (for spreadsheets)', type: 'boolean' },
|
|
79
|
+
],
|
|
80
|
+
examples: [
|
|
81
|
+
{ command: 'swarmdo ownership src --since 6mo', description: 'Knowledge map under src/ in the last 6 months' },
|
|
82
|
+
{ command: 'swarmdo ownership --top 10 --format json', description: 'Ten most fragile files as JSON, with repo truck factor' },
|
|
83
|
+
{ command: 'swarmdo ownership --csv > ownership.csv', description: 'Export the ownership map to CSV' },
|
|
84
|
+
],
|
|
85
|
+
action: run,
|
|
86
|
+
};
|
|
87
|
+
export default ownershipCommand;
|
|
88
|
+
//# sourceMappingURL=ownership.js.map
|
|
@@ -78,13 +78,13 @@ function walk(o) {
|
|
|
78
78
|
if (e.isDirectory()) {
|
|
79
79
|
if (SKIP_DIRS.has(e.name))
|
|
80
80
|
continue;
|
|
81
|
-
if (o.gitignore?.(rel
|
|
82
|
-
continue;
|
|
81
|
+
if (o.gitignore?.(rel, true))
|
|
82
|
+
continue; // isDir=true → dir-only patterns (build/) prune the dir + its contents
|
|
83
83
|
stack.push(full);
|
|
84
84
|
}
|
|
85
85
|
else if (e.isFile()) {
|
|
86
|
-
if (o.gitignore?.(rel))
|
|
87
|
-
continue;
|
|
86
|
+
if (o.gitignore?.(rel, false))
|
|
87
|
+
continue; // isDir=false → a dir-only pattern (build/) must NOT drop a file named `build`
|
|
88
88
|
if (o.exclude?.(rel))
|
|
89
89
|
continue;
|
|
90
90
|
if (o.include && !o.include(rel))
|
|
@@ -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;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands-lint.ts — static validation of Claude Code custom slash commands
|
|
3
|
+
* (the `.claude/commands` tree) and skills (`.claude/skills/<name>/SKILL.md`).
|
|
4
|
+
*
|
|
5
|
+
* Slash commands / skills are the most-copied Claude Code power-user artifact,
|
|
6
|
+
* and CC swallows their footguns silently: malformed YAML frontmatter makes CC
|
|
7
|
+
* load the body with EMPTY metadata (so `/cmd` "works" typed but Claude can
|
|
8
|
+
* never auto-match it), and two body rules bite in ways manual review misses —
|
|
9
|
+
* an inline `` !`cmd` `` that isn't at a line start / after whitespace is left as
|
|
10
|
+
* literal text and NEVER runs, and one whose command no `allowed-tools` Bash
|
|
11
|
+
* rule covers prompts/blocks instead of running pre-approved. This catches those
|
|
12
|
+
* before CC does. Sibling of agents-lint; same `Finding` shape.
|
|
13
|
+
*
|
|
14
|
+
* Pure: parsed text in, findings out. Reuses the permission-rule matcher
|
|
15
|
+
* (parseRule/covers) so allowed-tools coverage matches Claude Code's own glob
|
|
16
|
+
* semantics. Only truly-bounded fields are enum-checked (NOT `model`, which
|
|
17
|
+
* accepts aliases and full IDs) to stay false-positive-safe.
|
|
18
|
+
*/
|
|
19
|
+
import type { Finding } from './lint.js';
|
|
20
|
+
/** Reasoning-effort levels a command/skill `effort:` field may name. */
|
|
21
|
+
export declare const EFFORT_LEVELS: string[];
|
|
22
|
+
/** Lint ONE command/skill file's raw text. `kind` toggles the skill-only rule
|
|
23
|
+
* that frontmatter (with name + description) is REQUIRED. Pure. */
|
|
24
|
+
export declare function lintCommandFile(file: string, raw: string, kind?: 'command' | 'skill'): Finding[];
|
|
25
|
+
export interface CommandFile {
|
|
26
|
+
file: string;
|
|
27
|
+
raw: string;
|
|
28
|
+
}
|
|
29
|
+
/** Lint a set of command files and skill files. Pure. */
|
|
30
|
+
export declare function lintCommandFiles(commands: CommandFile[], skills?: CommandFile[]): Finding[];
|
|
31
|
+
//# sourceMappingURL=commands-lint.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commands-lint.ts — static validation of Claude Code custom slash commands
|
|
3
|
+
* (the `.claude/commands` tree) and skills (`.claude/skills/<name>/SKILL.md`).
|
|
4
|
+
*
|
|
5
|
+
* Slash commands / skills are the most-copied Claude Code power-user artifact,
|
|
6
|
+
* and CC swallows their footguns silently: malformed YAML frontmatter makes CC
|
|
7
|
+
* load the body with EMPTY metadata (so `/cmd` "works" typed but Claude can
|
|
8
|
+
* never auto-match it), and two body rules bite in ways manual review misses —
|
|
9
|
+
* an inline `` !`cmd` `` that isn't at a line start / after whitespace is left as
|
|
10
|
+
* literal text and NEVER runs, and one whose command no `allowed-tools` Bash
|
|
11
|
+
* rule covers prompts/blocks instead of running pre-approved. This catches those
|
|
12
|
+
* before CC does. Sibling of agents-lint; same `Finding` shape.
|
|
13
|
+
*
|
|
14
|
+
* Pure: parsed text in, findings out. Reuses the permission-rule matcher
|
|
15
|
+
* (parseRule/covers) so allowed-tools coverage matches Claude Code's own glob
|
|
16
|
+
* semantics. Only truly-bounded fields are enum-checked (NOT `model`, which
|
|
17
|
+
* accepts aliases and full IDs) to stay false-positive-safe.
|
|
18
|
+
*/
|
|
19
|
+
import { parse as parseYaml } from 'yaml';
|
|
20
|
+
import { parseRule, covers } from '../permissions/audit.js';
|
|
21
|
+
const f = (file, severity, rule, message) => ({ file, severity, rule, message });
|
|
22
|
+
/** Reasoning-effort levels a command/skill `effort:` field may name. */
|
|
23
|
+
export const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
24
|
+
const FRONTMATTER_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n([\s\S]*))?$/;
|
|
25
|
+
function parseFile(raw) {
|
|
26
|
+
const text = raw.replace(/^\uFEFF/, '');
|
|
27
|
+
const m = text.match(FRONTMATTER_RE);
|
|
28
|
+
if (!m) {
|
|
29
|
+
// An opening `---` with no closing fence is malformed; anything else is a
|
|
30
|
+
// plain-markdown command (no frontmatter — perfectly valid).
|
|
31
|
+
if (/^---[ \t]*\r?\n/.test(text))
|
|
32
|
+
return { meta: null, body: text, malformed: 'has an opening `---` but no closing `---` fence' };
|
|
33
|
+
return { meta: null, body: text };
|
|
34
|
+
}
|
|
35
|
+
const body = m[2] ?? '';
|
|
36
|
+
let fm;
|
|
37
|
+
try {
|
|
38
|
+
fm = parseYaml(m[1]);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
return { meta: null, body, malformed: `frontmatter is not valid YAML: ${e.message}` };
|
|
42
|
+
}
|
|
43
|
+
if (fm === null || typeof fm !== 'object' || Array.isArray(fm)) {
|
|
44
|
+
return { meta: null, body, malformed: 'frontmatter must be a YAML mapping of key: value' };
|
|
45
|
+
}
|
|
46
|
+
return { meta: fm, body };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Inline bash-injection placeholders `` !`cmd` `` in the body. Claude Code only
|
|
50
|
+
* RUNS one whose `!` is at a line start or immediately after whitespace; if `!`
|
|
51
|
+
* follows another character (e.g. `` KEY=!`cmd` ``) it is left as literal text.
|
|
52
|
+
*/
|
|
53
|
+
function bashInjections(body) {
|
|
54
|
+
const out = [];
|
|
55
|
+
const re = /!`([^`\n]+)`/g;
|
|
56
|
+
let m;
|
|
57
|
+
while ((m = re.exec(body)) !== null) {
|
|
58
|
+
const at = m.index;
|
|
59
|
+
const active = at === 0 || /\s/.test(body[at - 1]);
|
|
60
|
+
out.push({ cmd: m[1].trim(), active });
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** Parse the `allowed-tools` frontmatter (string list or YAML array) into rules;
|
|
65
|
+
* null = the key is absent (no pre-approval declared, so coverage isn't checked). */
|
|
66
|
+
function allowedToolRules(meta) {
|
|
67
|
+
const at = meta['allowed-tools'] ?? meta['allowedTools'];
|
|
68
|
+
if (at === undefined)
|
|
69
|
+
return null;
|
|
70
|
+
const parts = Array.isArray(at) ? at.map(String) : typeof at === 'string' ? at.split(',') : [];
|
|
71
|
+
const rules = [];
|
|
72
|
+
for (const p of parts) {
|
|
73
|
+
const r = parseRule(p.trim());
|
|
74
|
+
if (r.tool !== undefined)
|
|
75
|
+
rules.push(r);
|
|
76
|
+
}
|
|
77
|
+
return rules;
|
|
78
|
+
}
|
|
79
|
+
/** Lint ONE command/skill file's raw text. `kind` toggles the skill-only rule
|
|
80
|
+
* that frontmatter (with name + description) is REQUIRED. Pure. */
|
|
81
|
+
export function lintCommandFile(file, raw, kind = 'command') {
|
|
82
|
+
const p = parseFile(raw);
|
|
83
|
+
const out = [];
|
|
84
|
+
if (p.malformed) {
|
|
85
|
+
out.push(f(file, 'error', 'command-malformed-frontmatter', p.malformed));
|
|
86
|
+
}
|
|
87
|
+
else if (kind === 'skill' && !p.meta) {
|
|
88
|
+
out.push(f(file, 'error', 'skill-missing-frontmatter', 'a SKILL.md must start with a `---` … `---` frontmatter block declaring `name` and `description`'));
|
|
89
|
+
}
|
|
90
|
+
const meta = p.meta;
|
|
91
|
+
if (meta) {
|
|
92
|
+
if (kind === 'skill' && (typeof meta.name !== 'string' || meta.name.trim() === '')) {
|
|
93
|
+
out.push(f(file, 'error', 'skill-missing-name', 'frontmatter is missing a non-empty `name`'));
|
|
94
|
+
}
|
|
95
|
+
if (typeof meta.description !== 'string' || meta.description.trim() === '') {
|
|
96
|
+
const how = kind === 'skill' ? 'decide when to invoke the skill' : 'list the command and model-invoke it';
|
|
97
|
+
out.push(f(file, kind === 'skill' ? 'error' : 'warn', 'command-missing-description', `frontmatter has no non-empty \`description\` (Claude Code uses it to ${how})`));
|
|
98
|
+
}
|
|
99
|
+
if (meta.effort !== undefined && !EFFORT_LEVELS.includes(String(meta.effort))) {
|
|
100
|
+
out.push(f(file, 'error', 'command-bad-effort', `effort "${String(meta.effort)}" is not one of: ${EFFORT_LEVELS.join(', ')}`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Body cross-checks (both kinds). Coverage is only checked when allowed-tools
|
|
104
|
+
// is declared — otherwise the author never opted into pre-approval.
|
|
105
|
+
const rules = meta ? allowedToolRules(meta) : null;
|
|
106
|
+
for (const inj of bashInjections(p.body)) {
|
|
107
|
+
const tok = '!`' + inj.cmd + '`';
|
|
108
|
+
if (!inj.active) {
|
|
109
|
+
out.push(f(file, 'warn', 'command-inert-bash-injection', `inline bash ${tok} follows a non-whitespace character — Claude Code leaves it as literal text and never runs it (put \`!\` at a line start or after whitespace)`));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (rules && rules.length > 0 && !rules.some((r) => covers(r, { tool: 'Bash', specifier: inj.cmd }))) {
|
|
113
|
+
out.push(f(file, 'warn', 'command-uncovered-bash-injection', `inline bash ${tok} is not covered by any \`allowed-tools\` Bash rule — it will prompt/block instead of running pre-approved`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/** Lint a set of command files and skill files. Pure. */
|
|
119
|
+
export function lintCommandFiles(commands, skills = []) {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const c of commands)
|
|
122
|
+
out.push(...lintCommandFile(c.file, c.raw, 'command'));
|
|
123
|
+
for (const s of skills)
|
|
124
|
+
out.push(...lintCommandFile(s.file, s.raw, 'skill'));
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=commands-lint.js.map
|
|
@@ -18,6 +18,7 @@ export interface Finding {
|
|
|
18
18
|
export declare const TOPOLOGIES: string[];
|
|
19
19
|
export declare const MEMORY_BACKENDS: string[];
|
|
20
20
|
import { type AgentFile } from './agents-lint.js';
|
|
21
|
+
import { type CommandFile } from './commands-lint.js';
|
|
21
22
|
export declare const KNOWN_CONFIG_KEYS: string[];
|
|
22
23
|
export declare const HOOK_EVENTS: string[];
|
|
23
24
|
/** Parse a JSON file's raw text; a null raw means "file absent" (fine). */
|
|
@@ -52,6 +53,10 @@ export interface LintInput {
|
|
|
52
53
|
skills?: string[];
|
|
53
54
|
/** `.claude/agents/*.md` subagent definitions (raw text per file) */
|
|
54
55
|
agentFiles?: AgentFile[];
|
|
56
|
+
/** custom slash commands under the `.claude/commands` tree (raw text per file) */
|
|
57
|
+
commandFiles?: CommandFile[];
|
|
58
|
+
/** skill definitions at `.claude/skills/<name>/SKILL.md` (raw text per file) */
|
|
59
|
+
skillFiles?: CommandFile[];
|
|
55
60
|
}
|
|
56
61
|
export interface LintReport {
|
|
57
62
|
findings: Finding[];
|
|
@@ -13,6 +13,7 @@ export const TOPOLOGIES = ['hierarchical', 'mesh', 'hierarchical-mesh', 'ring',
|
|
|
13
13
|
export const MEMORY_BACKENDS = ['agentdb', 'sqlite', 'hybrid', 'memory'];
|
|
14
14
|
import { parseOpenRouterConfig } from '../providers/openrouter-config.js';
|
|
15
15
|
import { lintAgents } from './agents-lint.js';
|
|
16
|
+
import { lintCommandFiles } from './commands-lint.js';
|
|
16
17
|
export const KNOWN_CONFIG_KEYS = ['topology', 'maxAgents', 'strategy', 'consensus', 'memory', 'memoryBackend', 'hnsw', 'neural', 'embeddings', 'providers', 'mcp', 'logging', 'daemon', 'hooks', 'version', 'openrouter', '$schema'];
|
|
17
18
|
// Current Claude Code hook events (source: code.claude.com/docs/en/hooks).
|
|
18
19
|
// Kept in sync with the runtime; a stale list here false-warns on valid hooks.
|
|
@@ -197,6 +198,9 @@ export function lintAll(input) {
|
|
|
197
198
|
findings.push(...lintLegacyLayout(input.commandsRoot ?? [], input.skills ?? [], input.sdoCommands ?? []));
|
|
198
199
|
if (input.agentFiles?.length)
|
|
199
200
|
findings.push(...lintAgents(input.agentFiles));
|
|
201
|
+
if (input.commandFiles?.length || input.skillFiles?.length) {
|
|
202
|
+
findings.push(...lintCommandFiles(input.commandFiles ?? [], input.skillFiles ?? []));
|
|
203
|
+
}
|
|
200
204
|
return {
|
|
201
205
|
findings,
|
|
202
206
|
errors: findings.filter((x) => x.severity === 'error').length,
|