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,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hidden.ts — HIDDEN coupling: files that change together but have no import
|
|
3
|
+
* edge to explain it. "Logical coupling minus structural coupling."
|
|
4
|
+
*
|
|
5
|
+
* `coupling` mines temporal (co-change) coupling from git; `codegraph` builds
|
|
6
|
+
* the static import graph. Join them and emit the SET DIFFERENCE: pairs with a
|
|
7
|
+
* high co-change degree yet NO import edge connecting them in either direction.
|
|
8
|
+
* That's a dependency real enough to move two files in lockstep but invisible in
|
|
9
|
+
* the code — a JSON schema and the type that mirrors it, a serializer/parser
|
|
10
|
+
* split across modules, a config file and its consumers, a doc that must track
|
|
11
|
+
* an API. An agent editing A won't reach B by following imports, so it forgets
|
|
12
|
+
* the co-edit; a reviewer sees a missing abstraction / architectural smell.
|
|
13
|
+
*
|
|
14
|
+
* This is the divergence between evolutionary coupling (Gall et al., "Detection
|
|
15
|
+
* of Logical Coupling," ICSM 1998) and structural coupling — CodeScene flags
|
|
16
|
+
* change-coupling that crosses architectural boundaries as "surprising." swarmdo
|
|
17
|
+
* is uniquely positioned to compute it because it already owns BOTH captures.
|
|
18
|
+
*
|
|
19
|
+
* Pure + deterministic: a set-difference over two already-built inputs (the
|
|
20
|
+
* ranked CouplingPair[] from computeCoupling + the ImportEdge[] from the index).
|
|
21
|
+
* No git, no fs — fully fixture-testable.
|
|
22
|
+
*/
|
|
23
|
+
import type { CouplingPair } from './coupling.js';
|
|
24
|
+
import type { ImportEdge } from '../codegraph/codegraph.js';
|
|
25
|
+
export interface AnnotatedPair extends CouplingPair {
|
|
26
|
+
/** true iff an import edge connects a↔b in EITHER direction (a structural link
|
|
27
|
+
* that explains the co-change); the returned pairs are all `false`. */
|
|
28
|
+
importLinked: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface HiddenCouplingOptions {
|
|
31
|
+
/** keep only the top N hidden pairs after ranking (default: all) */
|
|
32
|
+
top?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Return the co-change pairs that NO import edge explains, ranked as they came
|
|
36
|
+
* from `computeCoupling` (degree desc, shared desc, path order). A type-only
|
|
37
|
+
* import still counts as a structural link — it's a real code reference that
|
|
38
|
+
* explains the co-change — so those pairs are excluded too. Pure.
|
|
39
|
+
*/
|
|
40
|
+
export declare function computeHiddenCoupling(pairs: CouplingPair[], imports: ImportEdge[], opts?: HiddenCouplingOptions): AnnotatedPair[];
|
|
41
|
+
/** Export the hidden-coupling ranking as CSV for review. Pure. */
|
|
42
|
+
export declare function hiddenCouplingToCsv(pairs: AnnotatedPair[]): string;
|
|
43
|
+
/** Human-readable table. Pure. */
|
|
44
|
+
export declare function formatHiddenCoupling(pairs: AnnotatedPair[]): string;
|
|
45
|
+
//# sourceMappingURL=hidden.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hidden.ts — HIDDEN coupling: files that change together but have no import
|
|
3
|
+
* edge to explain it. "Logical coupling minus structural coupling."
|
|
4
|
+
*
|
|
5
|
+
* `coupling` mines temporal (co-change) coupling from git; `codegraph` builds
|
|
6
|
+
* the static import graph. Join them and emit the SET DIFFERENCE: pairs with a
|
|
7
|
+
* high co-change degree yet NO import edge connecting them in either direction.
|
|
8
|
+
* That's a dependency real enough to move two files in lockstep but invisible in
|
|
9
|
+
* the code — a JSON schema and the type that mirrors it, a serializer/parser
|
|
10
|
+
* split across modules, a config file and its consumers, a doc that must track
|
|
11
|
+
* an API. An agent editing A won't reach B by following imports, so it forgets
|
|
12
|
+
* the co-edit; a reviewer sees a missing abstraction / architectural smell.
|
|
13
|
+
*
|
|
14
|
+
* This is the divergence between evolutionary coupling (Gall et al., "Detection
|
|
15
|
+
* of Logical Coupling," ICSM 1998) and structural coupling — CodeScene flags
|
|
16
|
+
* change-coupling that crosses architectural boundaries as "surprising." swarmdo
|
|
17
|
+
* is uniquely positioned to compute it because it already owns BOTH captures.
|
|
18
|
+
*
|
|
19
|
+
* Pure + deterministic: a set-difference over two already-built inputs (the
|
|
20
|
+
* ranked CouplingPair[] from computeCoupling + the ImportEdge[] from the index).
|
|
21
|
+
* No git, no fs — fully fixture-testable.
|
|
22
|
+
*/
|
|
23
|
+
import { toCsv } from '../util/csv.js';
|
|
24
|
+
const US = '\x1f'; // pair-key separator (never appears in a file path)
|
|
25
|
+
/** Canonical undirected pair key so direction doesn't matter. */
|
|
26
|
+
const pairKey = (a, b) => (a < b ? a + US + b : b + US + a);
|
|
27
|
+
/**
|
|
28
|
+
* Return the co-change pairs that NO import edge explains, ranked as they came
|
|
29
|
+
* from `computeCoupling` (degree desc, shared desc, path order). A type-only
|
|
30
|
+
* import still counts as a structural link — it's a real code reference that
|
|
31
|
+
* explains the co-change — so those pairs are excluded too. Pure.
|
|
32
|
+
*/
|
|
33
|
+
export function computeHiddenCoupling(pairs, imports, opts = {}) {
|
|
34
|
+
// Every file pair connected by a resolved import edge, either direction.
|
|
35
|
+
const linked = new Set();
|
|
36
|
+
for (const e of imports) {
|
|
37
|
+
if (e.resolved)
|
|
38
|
+
linked.add(pairKey(e.from, e.resolved));
|
|
39
|
+
}
|
|
40
|
+
const hidden = pairs
|
|
41
|
+
.map((p) => ({ ...p, importLinked: linked.has(pairKey(p.a, p.b)) }))
|
|
42
|
+
.filter((p) => !p.importLinked);
|
|
43
|
+
return opts.top && opts.top > 0 ? hidden.slice(0, opts.top) : hidden;
|
|
44
|
+
}
|
|
45
|
+
/** Export the hidden-coupling ranking as CSV for review. Pure. */
|
|
46
|
+
export function hiddenCouplingToCsv(pairs) {
|
|
47
|
+
const headers = ['fileA', 'fileB', 'degree', 'shared', 'commitsA', 'commitsB'];
|
|
48
|
+
const rows = pairs.map((p) => [p.a, p.b, p.degree, p.shared, p.aCommits, p.bCommits]);
|
|
49
|
+
return toCsv(headers, rows);
|
|
50
|
+
}
|
|
51
|
+
/** Human-readable table. Pure. */
|
|
52
|
+
export function formatHiddenCoupling(pairs) {
|
|
53
|
+
if (pairs.length === 0)
|
|
54
|
+
return 'no hidden coupling found (every co-change pair has an import edge, or none met the threshold)';
|
|
55
|
+
const lines = [];
|
|
56
|
+
lines.push('rank degree shared commits(A/B) files (co-change, no import edge)');
|
|
57
|
+
pairs.forEach((p, i) => {
|
|
58
|
+
const rank = String(i + 1).padStart(4);
|
|
59
|
+
const degree = `${Math.round(p.degree * 100)}%`.padStart(6);
|
|
60
|
+
const shared = String(p.shared).padStart(6);
|
|
61
|
+
const ab = `${p.aCommits}/${p.bCommits}`.padStart(11);
|
|
62
|
+
lines.push(`${rank} ${degree} ${shared} ${ab} ${p.a} ⇢ ${p.b}`);
|
|
63
|
+
});
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=hidden.js.map
|
|
@@ -277,6 +277,31 @@ function routeTask(task) {
|
|
|
277
277
|
};
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
+
// Mirror of src/agent-bridge/bridge.ts classifyPrompt — decides whether a
|
|
281
|
+
// prompt warrants spinning up a bound-agent swarm (and which roles), so the
|
|
282
|
+
// UserPromptSubmit hook can tell the main agent to spawn + bridge agents
|
|
283
|
+
// (making Swarmdo used by default instead of sitting idle).
|
|
284
|
+
const AGENTIC_RE = /\\b(implement|build|create|add(?:ing)?|develop|refactor\\w*|migrat\\w*|redesign|re-?architect\\w*|architect\\w*|feature|integrat\\w*|overhaul|rewrite|port|scaffold|end-to-end|multi-file|test suite|coverage|audit|harden\\w*|vulnerab\\w*|cve|fix\\w*|debug\\w*|optimi[sz]e|performance)\\b/i;
|
|
285
|
+
const TRIVIAL_RE = /\\b(what|why|how|explain|describe|show|list|find|search|where|which|typo|readme|comment|one-?liner|quick question|rename|bump (?:the )?version|status|help)\\b/i;
|
|
286
|
+
const ROLE_SIGNALS = [
|
|
287
|
+
{ re: /\\b(security|vulnerab\\w*|cve|auth\\w*|inject\\w*|xss|ssrf)\\b/i, roles: ['security-auditor', 'coder', 'reviewer'] },
|
|
288
|
+
{ re: /\\b(refactor\\w*|migrat\\w*|redesign|re-?architect\\w*|overhaul|rewrite)\\b/i, roles: ['system-architect', 'coder', 'reviewer'] },
|
|
289
|
+
{ re: /\\b(perf\\w*|optimi[sz]e|benchmark|latency|throughput)\\b/i, roles: ['perf-analyzer', 'coder', 'tester'] },
|
|
290
|
+
{ re: /\\b(test|coverage|tdd|spec)\\b/i, roles: ['tester', 'coder', 'reviewer'] },
|
|
291
|
+
{ re: /\\b(feature|implement|build|integrat\\w*|end-to-end|api)\\b/i, roles: ['researcher', 'system-architect', 'coder', 'tester', 'reviewer'] },
|
|
292
|
+
];
|
|
293
|
+
function classifyAgentic(task) {
|
|
294
|
+
const p = String(task == null ? '' : task).trim();
|
|
295
|
+
if (!p) return { requiresAgents: false, roles: [] };
|
|
296
|
+
if (!AGENTIC_RE.test(p)) return { requiresAgents: false, roles: [] };
|
|
297
|
+
if (TRIVIAL_RE.test(p) && p.length < 40) return { requiresAgents: false, roles: [] };
|
|
298
|
+
let roles = ['coder'];
|
|
299
|
+
for (const s of ROLE_SIGNALS) {
|
|
300
|
+
if (s.re.test(p)) { roles = s.roles; break; }
|
|
301
|
+
}
|
|
302
|
+
return { requiresAgents: true, roles };
|
|
303
|
+
}
|
|
304
|
+
|
|
280
305
|
// CLI
|
|
281
306
|
const task = process.argv.slice(2).join(' ');
|
|
282
307
|
|
|
@@ -288,7 +313,7 @@ if (task) {
|
|
|
288
313
|
console.log('\\nAvailable agents:', Object.keys(AGENT_CAPABILITIES).join(', '));
|
|
289
314
|
}
|
|
290
315
|
|
|
291
|
-
module.exports = { routeTask, AGENT_CAPABILITIES, TASK_PATTERNS, buildPattern };
|
|
316
|
+
module.exports = { routeTask, classifyAgentic, AGENT_CAPABILITIES, TASK_PATTERNS, buildPattern };
|
|
292
317
|
`;
|
|
293
318
|
}
|
|
294
319
|
/**
|
|
@@ -482,6 +507,21 @@ export function generateHookHandler() {
|
|
|
482
507
|
' } else {',
|
|
483
508
|
" console.log('[INFO] Router not available, using default routing');",
|
|
484
509
|
' }',
|
|
510
|
+
' // Agentic prompts → remind the main agent to actually USE Swarmdo:',
|
|
511
|
+
' // spawn Claude Code agents AND bridge them (auto-forms a swarm).',
|
|
512
|
+
' // Advisory + fully guarded — never breaks the prompt.',
|
|
513
|
+
' try {',
|
|
514
|
+
' if (router && router.classifyAgentic) {',
|
|
515
|
+
' var intent = router.classifyAgentic(prompt);',
|
|
516
|
+
' if (intent.requiresAgents) {',
|
|
517
|
+
" console.log('');",
|
|
518
|
+
" console.log('[SWARMDO] Agentic task — use the swarm: after you spawn Claude Code agents,');",
|
|
519
|
+
" console.log(' register each so Swarmdo tracks them and auto-forms a swarm from your config:');",
|
|
520
|
+
" console.log(' swarmdo agent bridge register -n <agent-name> -t <role> -s <session>');",
|
|
521
|
+
" console.log(' suggested roles: ' + intent.roles.join(', ') + ' then: swarmdo agent bridge list / swarm status');",
|
|
522
|
+
' }',
|
|
523
|
+
' }',
|
|
524
|
+
' } catch (e) { /* advisory only — never break the prompt */ }',
|
|
485
525
|
' },',
|
|
486
526
|
'',
|
|
487
527
|
" 'pre-bash': () => {",
|
|
@@ -27,6 +27,7 @@ import { licenseTools } from './mcp-tools/license-tools.js';
|
|
|
27
27
|
import { applyTools } from './mcp-tools/apply-tools.js';
|
|
28
28
|
import { hotspotsTools } from './mcp-tools/hotspots-tools.js';
|
|
29
29
|
import { couplingTools } from './mcp-tools/coupling-tools.js';
|
|
30
|
+
import { ownershipTools } from './mcp-tools/ownership-tools.js';
|
|
30
31
|
import { affectedTools } from './mcp-tools/affected-tools.js';
|
|
31
32
|
import { cyclesTools } from './mcp-tools/cycles-tools.js';
|
|
32
33
|
import { testreportTools } from './mcp-tools/testreport-tools.js';
|
|
@@ -121,6 +122,7 @@ const TOOL_GROUPS = {
|
|
|
121
122
|
apply: () => applyTools,
|
|
122
123
|
hotspots: () => hotspotsTools,
|
|
123
124
|
coupling: () => couplingTools,
|
|
125
|
+
ownership: () => ownershipTools,
|
|
124
126
|
affected: () => affectedTools,
|
|
125
127
|
cycles: () => cyclesTools,
|
|
126
128
|
testreport: () => testreportTools,
|
|
@@ -9,6 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import { getProjectCwd } from './types.js';
|
|
10
10
|
import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
|
|
11
11
|
import { executeAgentTask } from './agent-execute-core.js';
|
|
12
|
+
import { buildSpawnInput, reconcile, isBound, orphanedAgentIds } from '../agent-bridge/bridge.js';
|
|
12
13
|
// Storage paths
|
|
13
14
|
const STORAGE_DIR = '.swarmdo';
|
|
14
15
|
const AGENT_DIR = 'agents';
|
|
@@ -74,6 +75,27 @@ function loadHiveAgents() {
|
|
|
74
75
|
function loadAllAgents() {
|
|
75
76
|
return { ...loadHiveAgents(), ...loadAgentStore().agents };
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Read the project's swarm topology defaults from swarmdo.config.json for the
|
|
80
|
+
* agent bridge's auto-swarm. Defensive — returns {} on any error so the bridge
|
|
81
|
+
* falls back to the anti-drift defaults (hierarchical / 8 / specialized).
|
|
82
|
+
* Accepts either a top-level `swarm` block or the root object.
|
|
83
|
+
*/
|
|
84
|
+
function readSwarmConfigDefaults() {
|
|
85
|
+
try {
|
|
86
|
+
const p = join(getProjectCwd(), 'swarmdo.config.json');
|
|
87
|
+
if (!existsSync(p))
|
|
88
|
+
return {};
|
|
89
|
+
const j = JSON.parse(readFileSync(p, 'utf-8'));
|
|
90
|
+
const sw = (j.swarm || j) ?? {};
|
|
91
|
+
const str = (v) => (typeof v === 'string' ? v : undefined);
|
|
92
|
+
const num = (v) => (typeof v === 'number' ? v : undefined);
|
|
93
|
+
return { topology: str(sw.topology), maxAgents: num(sw.maxAgents), strategy: str(sw.strategy) };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
77
99
|
// Default model mappings for agent types (can be overridden)
|
|
78
100
|
const AGENT_TYPE_MODEL_DEFAULTS = {
|
|
79
101
|
// Complex agents → opus
|
|
@@ -323,6 +345,150 @@ export const agentTools = [
|
|
|
323
345
|
return response;
|
|
324
346
|
},
|
|
325
347
|
},
|
|
348
|
+
{
|
|
349
|
+
name: 'agent_bridge_register',
|
|
350
|
+
description: "Register a REAL Claude Code Agent-tool agent into Swarmdo's registry so `swarmdo agent list` and `swarm_status` reflect it. Call this right AFTER you spawn a Claude Code subagent (Task/Agent tool): pass its name, session id, subagent type, and a one-line task. Idempotent — re-registering the same name+session UPDATES the bound record instead of duplicating. This is the bridge that stops Swarmdo sitting empty while real agents run: unlike agent_spawn (which only registers coordination metadata), this binds the record to a worker that actually exists.",
|
|
351
|
+
category: 'agent',
|
|
352
|
+
inputSchema: {
|
|
353
|
+
type: 'object',
|
|
354
|
+
properties: {
|
|
355
|
+
name: { type: 'string', description: 'The Claude Code agent name (e.g. "research-ccgap")' },
|
|
356
|
+
sessionId: { type: 'string', description: 'The Claude Code session id (from name@session-…)' },
|
|
357
|
+
agentType: { type: 'string', description: 'The subagent_type (e.g. general-purpose, coder). Default general-purpose.' },
|
|
358
|
+
task: { type: 'string', description: 'One-line task/prompt summary' },
|
|
359
|
+
},
|
|
360
|
+
required: ['name'],
|
|
361
|
+
},
|
|
362
|
+
handler: async (input) => {
|
|
363
|
+
const src = input;
|
|
364
|
+
const name = String(src.name || '').trim();
|
|
365
|
+
if (!name)
|
|
366
|
+
return { success: false, error: 'name is required (the Claude Code agent name)' };
|
|
367
|
+
const descriptor = {
|
|
368
|
+
name,
|
|
369
|
+
sessionId: src.sessionId ? String(src.sessionId) : undefined,
|
|
370
|
+
agentType: String(src.agentType || 'general-purpose'),
|
|
371
|
+
task: src.task ? String(src.task) : undefined,
|
|
372
|
+
};
|
|
373
|
+
const spawnInput = buildSpawnInput(descriptor, new Date().toISOString());
|
|
374
|
+
// Auto-swarm: registering a Claude Code agent spins up a swarm from the
|
|
375
|
+
// project config (anti-drift defaults if absent) when none is running, and
|
|
376
|
+
// enrolls this agent into it — so bridged agents coordinate instead of
|
|
377
|
+
// floating unattached. registerAgent honors spawnInput.swarmId to join.
|
|
378
|
+
let swarm;
|
|
379
|
+
try {
|
|
380
|
+
const { ensureActiveSwarm } = await import('./swarm-tools.js');
|
|
381
|
+
const cfg = readSwarmConfigDefaults();
|
|
382
|
+
swarm = ensureActiveSwarm({
|
|
383
|
+
topology: cfg.topology || 'hierarchical',
|
|
384
|
+
maxAgents: cfg.maxAgents || 8,
|
|
385
|
+
strategy: cfg.strategy || 'specialized',
|
|
386
|
+
});
|
|
387
|
+
spawnInput.swarmId = swarm.swarmId;
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
/* swarm optional — the agent still registers in the global store */
|
|
391
|
+
}
|
|
392
|
+
const res = await registerAgent(spawnInput);
|
|
393
|
+
if (!res.success)
|
|
394
|
+
return res;
|
|
395
|
+
return {
|
|
396
|
+
...res,
|
|
397
|
+
bound: true,
|
|
398
|
+
origin: 'claude-code',
|
|
399
|
+
claudeName: descriptor.name,
|
|
400
|
+
status: 'registered',
|
|
401
|
+
...(swarm ? { swarm } : {}),
|
|
402
|
+
};
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'agent_bridge_list',
|
|
407
|
+
description: 'List Swarmdo agent records split into Claude-Code-BOUND (each mirrors a real Task/Agent-tool agent) vs NATIVE, with binding detail. Pass `live` (array of current Claude Code agent names) to also get a reconciliation: which live agents are unmirrored (need agent_bridge_register), and which bound records are orphaned (their Claude agent is gone).',
|
|
408
|
+
category: 'agent',
|
|
409
|
+
inputSchema: {
|
|
410
|
+
type: 'object',
|
|
411
|
+
properties: {
|
|
412
|
+
live: {
|
|
413
|
+
type: 'array',
|
|
414
|
+
items: { type: 'string' },
|
|
415
|
+
description: 'Current live Claude Code agent names, for reconciliation',
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
handler: async (input) => {
|
|
420
|
+
const all = Object.values(loadAllAgents());
|
|
421
|
+
const summarize = (a) => ({
|
|
422
|
+
agentId: a.agentId,
|
|
423
|
+
agentType: a.agentType,
|
|
424
|
+
status: a.status,
|
|
425
|
+
...(isBound(a) ? { binding: a.config.binding } : {}),
|
|
426
|
+
});
|
|
427
|
+
const bound = all.filter(isBound).map(summarize);
|
|
428
|
+
const native = all.filter((a) => !isBound(a)).map(summarize);
|
|
429
|
+
const liveRaw = input.live;
|
|
430
|
+
const live = Array.isArray(liveRaw) ? liveRaw.map(String) : undefined;
|
|
431
|
+
const reconciliation = live ? reconcile(all, live) : undefined;
|
|
432
|
+
return {
|
|
433
|
+
total: all.length,
|
|
434
|
+
boundCount: bound.length,
|
|
435
|
+
nativeCount: native.length,
|
|
436
|
+
bound,
|
|
437
|
+
native,
|
|
438
|
+
...(reconciliation ? { reconciliation } : {}),
|
|
439
|
+
};
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
name: 'agent_bridge_prune',
|
|
444
|
+
description: "Reap orphaned Claude-Code-bound records — bound agents whose Claude Code agent is no longer live. Pass the CURRENT live Claude Code agent names as `live`; any bound record NOT in that list is removed from the agent store and from every swarm roster. Native (unbound) Swarmdo agents are never touched. Idempotent — use it to stop stale bindings accumulating across sessions.",
|
|
445
|
+
category: 'agent',
|
|
446
|
+
inputSchema: {
|
|
447
|
+
type: 'object',
|
|
448
|
+
properties: {
|
|
449
|
+
live: {
|
|
450
|
+
type: 'array',
|
|
451
|
+
items: { type: 'string' },
|
|
452
|
+
description: 'Current live Claude Code agent names; bound records not in this list are pruned',
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
required: ['live'],
|
|
456
|
+
},
|
|
457
|
+
handler: async (input) => {
|
|
458
|
+
const liveRaw = input.live;
|
|
459
|
+
const live = Array.isArray(liveRaw) ? liveRaw.map(String) : [];
|
|
460
|
+
const all = Object.values(loadAllAgents());
|
|
461
|
+
const toPrune = orphanedAgentIds(all, live);
|
|
462
|
+
if (toPrune.length === 0)
|
|
463
|
+
return { pruned: [], count: 0 };
|
|
464
|
+
// Remove from the canonical agent store (bound records always live here).
|
|
465
|
+
const store = loadAgentStore();
|
|
466
|
+
for (const id of toPrune)
|
|
467
|
+
delete store.agents[id];
|
|
468
|
+
saveAgentStore(store);
|
|
469
|
+
// Remove from any swarm roster so swarm_status doesn't report ghosts.
|
|
470
|
+
try {
|
|
471
|
+
const { loadSwarmStore: _loadSwarmStore, saveSwarmStore: _saveSwarmStore } = await import('./swarm-tools.js');
|
|
472
|
+
const swarmStore = _loadSwarmStore();
|
|
473
|
+
const pruneSet = new Set(toPrune);
|
|
474
|
+
let changed = false;
|
|
475
|
+
for (const swarm of Object.values(swarmStore.swarms)) {
|
|
476
|
+
if (Array.isArray(swarm.agents)) {
|
|
477
|
+
const before = swarm.agents.length;
|
|
478
|
+
swarm.agents = swarm.agents.filter((a) => !pruneSet.has(a));
|
|
479
|
+
if (swarm.agents.length !== before)
|
|
480
|
+
changed = true;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (changed)
|
|
484
|
+
_saveSwarmStore(swarmStore);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
/* swarm store optional — records already removed from the agent store */
|
|
488
|
+
}
|
|
489
|
+
return { pruned: toPrune, count: toPrune.length };
|
|
490
|
+
},
|
|
491
|
+
},
|
|
326
492
|
{
|
|
327
493
|
// ADR-095 G3 — spawn + execute fused. Closes the #1 UX trap from the
|
|
328
494
|
// 2026-04 audit (@roman-rr): `agent_spawn` looks like it spawns a worker
|
|
@@ -23,6 +23,7 @@ export { licenseTools } from './license-tools.js';
|
|
|
23
23
|
export { applyTools } from './apply-tools.js';
|
|
24
24
|
export { hotspotsTools } from './hotspots-tools.js';
|
|
25
25
|
export { couplingTools } from './coupling-tools.js';
|
|
26
|
+
export { ownershipTools } from './ownership-tools.js';
|
|
26
27
|
export { affectedTools } from './affected-tools.js';
|
|
27
28
|
export { cyclesTools } from './cycles-tools.js';
|
|
28
29
|
export { testreportTools } from './testreport-tools.js';
|
|
@@ -22,6 +22,7 @@ export { licenseTools } from './license-tools.js';
|
|
|
22
22
|
export { applyTools } from './apply-tools.js';
|
|
23
23
|
export { hotspotsTools } from './hotspots-tools.js';
|
|
24
24
|
export { couplingTools } from './coupling-tools.js';
|
|
25
|
+
export { ownershipTools } from './ownership-tools.js';
|
|
25
26
|
export { affectedTools } from './affected-tools.js';
|
|
26
27
|
export { cyclesTools } from './cycles-tools.js';
|
|
27
28
|
export { testreportTools } from './testreport-tools.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership MCP Tool
|
|
3
|
+
*
|
|
4
|
+
* Let an agent ask "who owns this file, and is it a bus-factor-1 key-person
|
|
5
|
+
* file?" and get ranked JSON — the risk/reviewer dimension it is otherwise
|
|
6
|
+
* blind to. An agent about to refactor a fragile single-owner file gets the
|
|
7
|
+
* signal that the change needs extra care, and "who should review this" gets a
|
|
8
|
+
* data-backed answer (the file's main-dev). Completes the git-mining trio's
|
|
9
|
+
* agent-callable set (hotspots + coupling + ownership). Shares the pure engine
|
|
10
|
+
* in ../ownership/ownership.ts with the CLI; captures git history via subprocess.
|
|
11
|
+
*/
|
|
12
|
+
import type { MCPTool } from './types.js';
|
|
13
|
+
export declare const ownershipTools: MCPTool[];
|
|
14
|
+
//# sourceMappingURL=ownership-tools.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ownership MCP Tool
|
|
3
|
+
*
|
|
4
|
+
* Let an agent ask "who owns this file, and is it a bus-factor-1 key-person
|
|
5
|
+
* file?" and get ranked JSON — the risk/reviewer dimension it is otherwise
|
|
6
|
+
* blind to. An agent about to refactor a fragile single-owner file gets the
|
|
7
|
+
* signal that the change needs extra care, and "who should review this" gets a
|
|
8
|
+
* data-backed answer (the file's main-dev). Completes the git-mining trio's
|
|
9
|
+
* agent-callable set (hotspots + coupling + ownership). Shares the pure engine
|
|
10
|
+
* in ../ownership/ownership.ts with the CLI; captures git history via subprocess.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import { parseGitLog } from '../hotspots/hotspots.js';
|
|
14
|
+
import { computeOwnership, repoBusFactor } from '../ownership/ownership.js';
|
|
15
|
+
import { normalizeSince } from '../util/since.js';
|
|
16
|
+
const ownershipTool = {
|
|
17
|
+
name: 'ownership',
|
|
18
|
+
description: 'Map per-file authorship concentration + bus factor from git history — "who owns each file, and what breaks if they leave?" (code-maat main-dev / knowledge map). Returns ranked JSON, most-fragile-first, so an agent editing a file knows if it is a single-owner (bus factor 1) key-person risk that needs extra care, and who its main-dev reviewer is. Includes a repo-level truck factor. Runs in a git repository.',
|
|
19
|
+
category: 'ownership',
|
|
20
|
+
tags: ['git', 'ownership', 'bus-factor', 'knowledge-map', 'risk', 'review'],
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
path: { type: 'string', description: 'repo path to run in (default cwd)' },
|
|
25
|
+
since: { type: 'string', description: 'history window, e.g. 90d or "3 months ago" (default 1 year)' },
|
|
26
|
+
top: { type: 'number', description: 'keep only the top N files (default 40; 0 = all)' },
|
|
27
|
+
minChurn: { type: 'number', description: 'drop files with total churn below N (default 1)' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
handler: async (params) => {
|
|
31
|
+
const cwd = typeof params.path === 'string' ? params.path : process.cwd();
|
|
32
|
+
const since = typeof params.since === 'string' ? params.since : '1 year ago';
|
|
33
|
+
const top = typeof params.top === 'number' ? params.top : 40;
|
|
34
|
+
const minChurn = typeof params.minChurn === 'number' ? params.minChurn : 1;
|
|
35
|
+
// Same `--numstat` dump `hotspots`/`coupling` parse; `%aN` folds author
|
|
36
|
+
// name/email variants through .mailmap so one person is one owner.
|
|
37
|
+
const args = ['log', '--no-merges', '--numstat', `--since=${normalizeSince(since)}`, '--format=format:%x01%H%x1f%aN%x1f%aI'];
|
|
38
|
+
let raw;
|
|
39
|
+
try {
|
|
40
|
+
raw = execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 128 * 1024 * 1024 });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return { error: true, message: 'git log failed — not a git repository?' };
|
|
44
|
+
}
|
|
45
|
+
const commits = parseGitLog(raw);
|
|
46
|
+
const ownership = computeOwnership(commits, { minChurn, top: top > 0 ? top : undefined });
|
|
47
|
+
return { since, minChurn, repoBusFactor: repoBusFactor(commits), count: ownership.length, ownership };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
export const ownershipTools = [ownershipTool];
|
|
51
|
+
//# sourceMappingURL=ownership-tools.js.map
|
|
@@ -28,6 +28,7 @@ const LEAN_GROUPS = [
|
|
|
28
28
|
'apply', // apply_patch — fuzzy unified-diff applier for agent edits
|
|
29
29
|
'hotspots', // hotspots — git-history change-risk ranking ("where's the debt?")
|
|
30
30
|
'coupling', // coupling — files that change together (temporal co-change), complements affected
|
|
31
|
+
'ownership', // ownership — per-file bus factor / knowledge map (who owns it, key-person risk)
|
|
31
32
|
'affected', // affected — impacted files/tests from a change via the import graph
|
|
32
33
|
'cycles', // cycles — circular-import detection via the import graph
|
|
33
34
|
'testreport', // testreport — JUnit/TAP → structured failure digest (pairs w/ repair)
|
|
@@ -32,6 +32,39 @@ interface SwarmStore {
|
|
|
32
32
|
}
|
|
33
33
|
export declare function loadSwarmStore(): SwarmStore;
|
|
34
34
|
export declare function saveSwarmStore(store: SwarmStore): void;
|
|
35
|
+
export interface CreateSwarmOptions {
|
|
36
|
+
topology?: string;
|
|
37
|
+
maxAgents?: number;
|
|
38
|
+
strategy?: string;
|
|
39
|
+
config?: Record<string, unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* When true, do NOT stamp the host `pid` — the swarm is a persistent logical
|
|
42
|
+
* coordination record, not tied to one process's lifetime, so it survives
|
|
43
|
+
* ephemeral CLI invocations (the agent bridge) instead of being reaped by the
|
|
44
|
+
* #1799 PID-orphan check the instant the creating process exits. Such swarms
|
|
45
|
+
* still expire via the 24h idle-TTL fallback. Daemon-hosted swarms
|
|
46
|
+
* (`swarm_init`) leave this false so a dead host IS reaped promptly.
|
|
47
|
+
*/
|
|
48
|
+
persistent?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create + persist a swarm. Extracted from the `swarm_init` handler so other
|
|
52
|
+
* subsystems (the Claude-Code agent bridge) can spin up a swarm from config
|
|
53
|
+
* WITHOUT duplicating the store-write. Throws on an invalid topology. Shared
|
|
54
|
+
* single source of truth — no parallel swarm creation.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createSwarm(opts: CreateSwarmOptions): SwarmState;
|
|
57
|
+
/**
|
|
58
|
+
* Return the most-recent RUNNING swarm, or create one from `defaults` if none
|
|
59
|
+
* exists. Used by the agent bridge so registering a Claude Code agent
|
|
60
|
+
* auto-spins-up a swarm (from the project's anti-drift config) and enrolls it,
|
|
61
|
+
* instead of leaving the agent unattached.
|
|
62
|
+
*/
|
|
63
|
+
export declare function ensureActiveSwarm(defaults: CreateSwarmOptions): {
|
|
64
|
+
swarmId: string;
|
|
65
|
+
topology: string;
|
|
66
|
+
created: boolean;
|
|
67
|
+
};
|
|
35
68
|
export declare const swarmTools: MCPTool[];
|
|
36
69
|
export {};
|
|
37
70
|
//# sourceMappingURL=swarm-tools.d.ts.map
|
|
@@ -109,6 +109,64 @@ export function saveSwarmStore(store) {
|
|
|
109
109
|
const VALID_TOPOLOGIES = new Set([
|
|
110
110
|
'hierarchical', 'mesh', 'hierarchical-mesh', 'ring', 'star', 'hybrid', 'adaptive',
|
|
111
111
|
]);
|
|
112
|
+
/**
|
|
113
|
+
* Create + persist a swarm. Extracted from the `swarm_init` handler so other
|
|
114
|
+
* subsystems (the Claude-Code agent bridge) can spin up a swarm from config
|
|
115
|
+
* WITHOUT duplicating the store-write. Throws on an invalid topology. Shared
|
|
116
|
+
* single source of truth — no parallel swarm creation.
|
|
117
|
+
*/
|
|
118
|
+
export function createSwarm(opts) {
|
|
119
|
+
const topology = opts.topology || 'hierarchical-mesh';
|
|
120
|
+
const maxAgents = Math.min(Math.max(opts.maxAgents || 15, 1), 50);
|
|
121
|
+
const strategy = opts.strategy || 'specialized';
|
|
122
|
+
const config = opts.config || {};
|
|
123
|
+
if (!VALID_TOPOLOGIES.has(topology)) {
|
|
124
|
+
throw new Error(`Invalid topology: ${topology}. Valid: ${[...VALID_TOPOLOGIES].join(', ')}`);
|
|
125
|
+
}
|
|
126
|
+
const swarmId = `swarm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
127
|
+
const now = new Date().toISOString();
|
|
128
|
+
const swarmState = {
|
|
129
|
+
swarmId,
|
|
130
|
+
topology,
|
|
131
|
+
maxAgents,
|
|
132
|
+
status: 'running',
|
|
133
|
+
agents: [],
|
|
134
|
+
tasks: [],
|
|
135
|
+
config: {
|
|
136
|
+
topology,
|
|
137
|
+
maxAgents,
|
|
138
|
+
strategy,
|
|
139
|
+
communicationProtocol: config.communicationProtocol || 'message-bus',
|
|
140
|
+
autoScaling: config.autoScaling ?? true,
|
|
141
|
+
consensusMechanism: config.consensusMechanism || 'majority',
|
|
142
|
+
},
|
|
143
|
+
createdAt: now,
|
|
144
|
+
updatedAt: now,
|
|
145
|
+
// Persistent (bridge) swarms omit the pid so they aren't reaped on process
|
|
146
|
+
// exit; daemon-hosted swarms record it for prompt orphan reconciliation.
|
|
147
|
+
...(opts.persistent ? {} : { pid: process.pid }),
|
|
148
|
+
};
|
|
149
|
+
const store = loadSwarmStore();
|
|
150
|
+
store.swarms[swarmId] = swarmState;
|
|
151
|
+
saveSwarmStore(store);
|
|
152
|
+
return swarmState;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Return the most-recent RUNNING swarm, or create one from `defaults` if none
|
|
156
|
+
* exists. Used by the agent bridge so registering a Claude Code agent
|
|
157
|
+
* auto-spins-up a swarm (from the project's anti-drift config) and enrolls it,
|
|
158
|
+
* instead of leaving the agent unattached.
|
|
159
|
+
*/
|
|
160
|
+
export function ensureActiveSwarm(defaults) {
|
|
161
|
+
const store = loadSwarmStore();
|
|
162
|
+
const running = Object.values(store.swarms)
|
|
163
|
+
.filter((s) => s.status === 'running')
|
|
164
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())[0];
|
|
165
|
+
if (running)
|
|
166
|
+
return { swarmId: running.swarmId, topology: running.topology, created: false };
|
|
167
|
+
const s = createSwarm({ ...defaults, persistent: true });
|
|
168
|
+
return { swarmId: s.swarmId, topology: s.topology, created: true };
|
|
169
|
+
}
|
|
112
170
|
export const swarmTools = [
|
|
113
171
|
{
|
|
114
172
|
name: 'swarm_init',
|
|
@@ -135,49 +193,26 @@ export const swarmTools = [
|
|
|
135
193
|
if (!v.valid)
|
|
136
194
|
return { success: false, error: v.error };
|
|
137
195
|
}
|
|
138
|
-
const topology = input.topology || 'hierarchical-mesh';
|
|
139
|
-
const maxAgents = Math.min(Math.max(input.maxAgents || 15, 1), 50);
|
|
140
196
|
const strategy = input.strategy || 'specialized';
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
const swarmId = `swarm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
149
|
-
const now = new Date().toISOString();
|
|
150
|
-
const swarmState = {
|
|
151
|
-
swarmId,
|
|
152
|
-
topology,
|
|
153
|
-
maxAgents,
|
|
154
|
-
status: 'running',
|
|
155
|
-
agents: [],
|
|
156
|
-
tasks: [],
|
|
157
|
-
config: {
|
|
158
|
-
topology,
|
|
159
|
-
maxAgents,
|
|
197
|
+
let swarmState;
|
|
198
|
+
try {
|
|
199
|
+
swarmState = createSwarm({
|
|
200
|
+
topology: input.topology,
|
|
201
|
+
maxAgents: input.maxAgents,
|
|
160
202
|
strategy,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
// #1799 — record host PID so subsequent loads can detect orphans
|
|
168
|
-
// when this process exits without a graceful swarm_shutdown.
|
|
169
|
-
pid: process.pid,
|
|
170
|
-
};
|
|
171
|
-
const store = loadSwarmStore();
|
|
172
|
-
store.swarms[swarmId] = swarmState;
|
|
173
|
-
saveSwarmStore(store);
|
|
203
|
+
config: (input.config || {}),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return { success: false, error: e.message };
|
|
208
|
+
}
|
|
174
209
|
return {
|
|
175
210
|
success: true,
|
|
176
|
-
swarmId,
|
|
177
|
-
topology,
|
|
211
|
+
swarmId: swarmState.swarmId,
|
|
212
|
+
topology: swarmState.topology,
|
|
178
213
|
strategy,
|
|
179
|
-
maxAgents,
|
|
180
|
-
initializedAt:
|
|
214
|
+
maxAgents: swarmState.maxAgents,
|
|
215
|
+
initializedAt: swarmState.createdAt,
|
|
181
216
|
config: swarmState.config,
|
|
182
217
|
persisted: true,
|
|
183
218
|
};
|