knodin 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/bin/cli.js +163 -15
- package/dist/bin/launcher.js +0 -0
- package/dist/src/cli-model.js +31 -1
- package/dist/src/engine/index.js +325 -54
- package/dist/src/init.js +16 -0
- package/dist/src/repository-management.js +219 -3
- package/dist/src/skill-management.js +149 -0
- package/dist/src/system-config.js +176 -16
- package/dist/src/system-management.js +508 -0
- package/dist/src/tools/knodin-tools.js +108 -15
- package/docs/CLI.md +7 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +27 -6
- package/docs/prompts/declare-multi-repository-system.md +18 -0
- package/docs/releases/0.8.6.md +10 -0
- package/docs/releases/0.8.7.md +27 -0
- package/package.json +6 -2
- package/skills/knodin-debug/SKILL.md +12 -0
- package/skills/knodin-debug/agents/openai.yaml +4 -0
- package/skills/knodin-explore/SKILL.md +11 -0
- package/skills/knodin-explore/agents/openai.yaml +4 -0
- package/skills/knodin-impact/SKILL.md +11 -0
- package/skills/knodin-impact/agents/openai.yaml +4 -0
- package/skills/knodin-lifecycle/SKILL.md +12 -0
- package/skills/knodin-lifecycle/agents/openai.yaml +4 -0
- package/skills/knodin-refactor/SKILL.md +12 -0
- package/skills/knodin-refactor/agents/openai.yaml +4 -0
- package/skills/knodin-review/SKILL.md +11 -0
- package/skills/knodin-review/agents/openai.yaml +4 -0
- package/skills/knodin-systems/SKILL.md +14 -0
- package/skills/knodin-systems/agents/openai.yaml +4 -0
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# knodin
|
|
2
2
|
|
|
3
|
+
For an approval-gated, evidence-first way to declare a multi-repository system, use the sanitized [assistant prompt](docs/prompts/declare-multi-repository-system.md).
|
|
4
|
+
|
|
5
|
+
`knodin init` installs focused `knodin-*` assistant skills for detected Codex,
|
|
6
|
+
Claude, and Gemini clients. Use `knodin skills list|install|remove|doctor` to
|
|
7
|
+
inspect or manage them explicitly; CLI-only scope installs none.
|
|
8
|
+
|
|
3
9
|
<img src="https://cdn.jsdelivr.net/npm/knodin@latest/docs/assets/knodin-favicon.svg" alt="knodin caret-beak logo" width="96" height="96">
|
|
4
10
|
|
|
5
11
|
> Local code intelligence that remembers how your code connects—and shows what
|
package/dist/bin/cli.js
CHANGED
|
@@ -46,7 +46,9 @@ import { runRepositoryInitializationProcess } from "../src/repository-init-proce
|
|
|
46
46
|
import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, repositorySignalInspectionLimit, searchRepositories, withRepositorySignals, } from "../src/repository-management.js";
|
|
47
47
|
import { applyResponseBudget } from "../src/response-budget.js";
|
|
48
48
|
import { appendSessionEvent, clearSessionTelemetry, disableSessionTelemetry, enableSessionTelemetry, readSessionEvents, sessionTelemetryStatus, } from "../src/session-telemetry.js";
|
|
49
|
+
import { inspectKnodinSkills, installKnodinSkills, KNODIN_SKILLS, removeKnodinSkills, } from "../src/skill-management.js";
|
|
49
50
|
import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
|
|
51
|
+
import { applySystemPlan, planSystemImport, planSystemLink, planSystemRelate, planSystemUnlink, planSystemUnrelate, } from "../src/system-management.js";
|
|
50
52
|
import { applyTrustedUpdate, checkTrustedUpdate, claimScheduledUpdateCheck, detectUpdateInstallMethod, explainTrustedUpdate, releaseScheduledUpdateCheck, rollbackTrustedUpdate, trustedUpdateStatus, } from "../src/update-policy.js";
|
|
51
53
|
import { KNODIN_VERSION } from "../src/version.js";
|
|
52
54
|
import { writeVisualization, } from "../src/visualization.js";
|
|
@@ -422,10 +424,16 @@ function formatStatusHuman(result) {
|
|
|
422
424
|
`${result.freshness.workingTree.pendingPaths ?? "unknown"} pending path(s).\n` +
|
|
423
425
|
`Last successful refresh: ${result.freshness.lastSuccessfulRefresh ?? "never"}.\n` +
|
|
424
426
|
`Freshness mechanism: ${result.freshnessMechanism.strategy}; watcher ${result.freshnessMechanism.watcher}${result.freshnessMechanism.diagnostic ? ` (${result.freshnessMechanism.diagnostic})` : ""}.\n`;
|
|
425
|
-
if (result.status === "healthy")
|
|
426
|
-
|
|
427
|
+
if (result.status === "healthy") {
|
|
428
|
+
const auditNote = result.verification.mode === "persisted-audit"
|
|
429
|
+
? ` Cached deep-audit evidence from ${result.verification.auditVerifiedAt ?? "an earlier run"}; freshness probed ${result.verification.verifiedAt ?? "now"}. Run \`knodin status --deep\` for an exact current audit.`
|
|
430
|
+
: "";
|
|
431
|
+
return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).${auditNote}\n${freshnessLine}${lifecycleLine}${integrationLine}`;
|
|
432
|
+
}
|
|
427
433
|
if (result.status === "stale")
|
|
428
|
-
return `Graph content is intact but evidence is stale (${coverage})
|
|
434
|
+
return `Graph content is intact but evidence is stale (${coverage}).${result.verification.mode === "persisted-audit"
|
|
435
|
+
? ` Cached deep-audit evidence is from ${result.verification.auditVerifiedAt ?? "an earlier run"}; freshness was probed ${result.verification.verifiedAt ?? "now"}. Run \`knodin status --deep\` for an exact current audit.`
|
|
436
|
+
: ""}\n${freshnessLine}${lifecycleLine}${integrationLine}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.\n`;
|
|
429
437
|
const outstanding = result.missing.files.length + result.missing.records.length;
|
|
430
438
|
const firstIssue = result.missing.files[0] ?? result.missing.records[0];
|
|
431
439
|
const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
|
|
@@ -637,7 +645,6 @@ async function main() {
|
|
|
637
645
|
process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
|
|
638
646
|
const engine = createEngine({ watcher: "disabled" });
|
|
639
647
|
try {
|
|
640
|
-
const systemConfig = loadSystemConfiguration(repository);
|
|
641
648
|
const summary = await initializeRepositories([repository], {
|
|
642
649
|
// A portfolio worker owns exactly one already-discovered worktree.
|
|
643
650
|
// Do not recursively initialize nested repositories inside the same
|
|
@@ -648,7 +655,7 @@ async function main() {
|
|
|
648
655
|
index: (target) => engine.index(target),
|
|
649
656
|
status: (target) => engine.status(target),
|
|
650
657
|
agents: detectSupportedAgents(),
|
|
651
|
-
indexMode: (target) => indexModeForPath(
|
|
658
|
+
indexMode: (target) => indexModeForPath(loadSystemConfiguration(target), target),
|
|
652
659
|
});
|
|
653
660
|
const result = summary.results.find(({ repository: target }) => target === repository);
|
|
654
661
|
if (!result)
|
|
@@ -799,7 +806,6 @@ async function main() {
|
|
|
799
806
|
linkedWorktrees: plan.linkedWorktrees,
|
|
800
807
|
});
|
|
801
808
|
const engine = createEngine({ watcher: "disabled" });
|
|
802
|
-
const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
|
|
803
809
|
const repositories = [];
|
|
804
810
|
const discoveryRecords = plan.signals
|
|
805
811
|
? discovery.repositories.slice(0, repositorySignalInspectionLimit(plan))
|
|
@@ -807,7 +813,7 @@ async function main() {
|
|
|
807
813
|
for (const record of discoveryRecords) {
|
|
808
814
|
const inventory = await inventoryRepository(record, {
|
|
809
815
|
status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
|
|
810
|
-
systemMemberships: (target) => systemMembershipsForPath(
|
|
816
|
+
systemMemberships: (target) => systemMembershipsForPath(loadSystemConfiguration(target), target),
|
|
811
817
|
});
|
|
812
818
|
repositories.push(withRepositorySignals(inventory, plan.signals, plan.signals ? await detectRepositorySignals(record.path) : {}));
|
|
813
819
|
}
|
|
@@ -904,7 +910,6 @@ async function main() {
|
|
|
904
910
|
}
|
|
905
911
|
if (plan.command === "search") {
|
|
906
912
|
const engine = createEngine({ watcher: "disabled" });
|
|
907
|
-
const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
|
|
908
913
|
const output = await searchRepositories(plan.roots, plan.query ?? "", {
|
|
909
914
|
depth: plan.depth,
|
|
910
915
|
linkedWorktrees: plan.linkedWorktrees,
|
|
@@ -921,7 +926,7 @@ async function main() {
|
|
|
921
926
|
includeSource: true,
|
|
922
927
|
federate: false,
|
|
923
928
|
}),
|
|
924
|
-
systemMemberships: (target) => systemMembershipsForPath(
|
|
929
|
+
systemMemberships: (target) => systemMembershipsForPath(loadSystemConfiguration(target), target),
|
|
925
930
|
});
|
|
926
931
|
await engine.close();
|
|
927
932
|
process.stdout.write(`${JSON.stringify(output, null, plan.json ? 0 : 2)}\n`);
|
|
@@ -1622,13 +1627,119 @@ async function main() {
|
|
|
1622
1627
|
break;
|
|
1623
1628
|
}
|
|
1624
1629
|
case "system": {
|
|
1625
|
-
const
|
|
1626
|
-
|
|
1630
|
+
const requestedAction = invocation.commandPath[1];
|
|
1631
|
+
if (requestedAction === "link") {
|
|
1632
|
+
const [systemId, ...repositories] = invocation.positionals;
|
|
1633
|
+
if (!systemId)
|
|
1634
|
+
throw new Error("knodin system link requires <system-id>");
|
|
1635
|
+
const overrides = Object.fromEntries((Array.isArray(invocation.options.repositoryId) ? invocation.options.repositoryId : [])
|
|
1636
|
+
.map(String)
|
|
1637
|
+
.map((value) => {
|
|
1638
|
+
const at = value.lastIndexOf("=");
|
|
1639
|
+
if (at < 1)
|
|
1640
|
+
throw new Error("--repository-id must be <path>=<id>");
|
|
1641
|
+
return [value.slice(0, at), value.slice(at + 1)];
|
|
1642
|
+
}));
|
|
1643
|
+
const componentOverrides = Object.fromEntries((Array.isArray(invocation.options.component) ? invocation.options.component : [])
|
|
1644
|
+
.map(String)
|
|
1645
|
+
.map((value) => {
|
|
1646
|
+
const at = value.indexOf("=");
|
|
1647
|
+
if (at < 1)
|
|
1648
|
+
throw new Error("--component must be <repo-id>=<component-id>[:type]");
|
|
1649
|
+
const repositoryId = value.slice(0, at);
|
|
1650
|
+
const [id, componentType] = value.slice(at + 1).split(":");
|
|
1651
|
+
if (!id)
|
|
1652
|
+
throw new Error("--component requires a component id");
|
|
1653
|
+
if (componentType &&
|
|
1654
|
+
!["component", "api", "resource", "artifact"].includes(componentType))
|
|
1655
|
+
throw new Error(`--component: unsupported type ${componentType}`);
|
|
1656
|
+
return [
|
|
1657
|
+
repositoryId,
|
|
1658
|
+
{
|
|
1659
|
+
id,
|
|
1660
|
+
type: componentType,
|
|
1661
|
+
},
|
|
1662
|
+
];
|
|
1663
|
+
}));
|
|
1664
|
+
if (invocation.options.initMissing === true && invocation.options.dryRun !== true) {
|
|
1665
|
+
for (const checkout of repositories.map((value) => path.resolve(repo, value))) {
|
|
1666
|
+
if (fs.existsSync(path.join(checkout, ".knodin")))
|
|
1667
|
+
continue;
|
|
1668
|
+
await initializeRepository(checkout, {
|
|
1669
|
+
command: runtimeCommand,
|
|
1670
|
+
index: (target, indexOptions) => engine.index(target, undefined, false, indexOptions),
|
|
1671
|
+
scope: "personal",
|
|
1672
|
+
agents: [],
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
const plan = planSystemLink({
|
|
1677
|
+
systemId,
|
|
1678
|
+
repositories,
|
|
1679
|
+
scope: invocation.options.scope === "team" ? "team" : "personal",
|
|
1680
|
+
distributed: invocation.options.distributed === true,
|
|
1681
|
+
repositoryIds: overrides,
|
|
1682
|
+
components: componentOverrides,
|
|
1683
|
+
});
|
|
1684
|
+
result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
|
|
1685
|
+
break;
|
|
1686
|
+
}
|
|
1687
|
+
if (requestedAction === "relate") {
|
|
1688
|
+
const [systemId] = invocation.positionals;
|
|
1689
|
+
if (!systemId)
|
|
1690
|
+
throw new Error("knodin system relate requires <system-id>");
|
|
1691
|
+
const plan = planSystemRelate({
|
|
1692
|
+
systemId,
|
|
1693
|
+
type: String(invocation.options.type),
|
|
1694
|
+
from: String(invocation.options.from),
|
|
1695
|
+
to: String(invocation.options.to),
|
|
1696
|
+
evidence: String(invocation.options.evidence),
|
|
1697
|
+
repoPath: repo,
|
|
1698
|
+
});
|
|
1699
|
+
result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
if (requestedAction === "unlink") {
|
|
1703
|
+
const [systemId, ...repositories] = invocation.positionals;
|
|
1704
|
+
if (!systemId)
|
|
1705
|
+
throw new Error("knodin system unlink requires <system-id>");
|
|
1706
|
+
const plan = planSystemUnlink({ systemId, repositories, repoPath: repo });
|
|
1707
|
+
result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
if (requestedAction === "unrelate") {
|
|
1711
|
+
const [systemId] = invocation.positionals;
|
|
1712
|
+
if (!systemId)
|
|
1713
|
+
throw new Error("knodin system unrelate requires <system-id>");
|
|
1714
|
+
const plan = planSystemUnrelate({
|
|
1715
|
+
systemId,
|
|
1716
|
+
type: String(invocation.options.type),
|
|
1717
|
+
from: String(invocation.options.from),
|
|
1718
|
+
to: String(invocation.options.to),
|
|
1719
|
+
repoPath: repo,
|
|
1720
|
+
});
|
|
1721
|
+
result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
|
|
1722
|
+
break;
|
|
1723
|
+
}
|
|
1724
|
+
if (requestedAction === "import") {
|
|
1725
|
+
const [manifestPath] = invocation.positionals;
|
|
1726
|
+
if (!manifestPath)
|
|
1727
|
+
throw new Error("knodin system import requires <path>");
|
|
1728
|
+
const plan = planSystemImport({
|
|
1729
|
+
manifestPath,
|
|
1730
|
+
scope: invocation.options.scope === "team" ? "team" : "personal",
|
|
1731
|
+
repoPath: repo,
|
|
1732
|
+
});
|
|
1733
|
+
result = invocation.options.dryRun === true ? plan : applySystemPlan(plan);
|
|
1734
|
+
break;
|
|
1735
|
+
}
|
|
1736
|
+
const requireComplete = rest.includes("--require-complete");
|
|
1737
|
+
const [action, systemId, ...unsupported] = rest.filter((argument) => argument !== "--allow-partial" && argument !== "--require-complete");
|
|
1627
1738
|
if (!action || !["list", "show", "validate", "query"].includes(action)) {
|
|
1628
1739
|
throw new Error("knodin system requires list, show, validate, or query");
|
|
1629
1740
|
}
|
|
1630
|
-
if (
|
|
1631
|
-
throw new Error(`knodin system ${action}: --
|
|
1741
|
+
if (requireComplete && action !== "query")
|
|
1742
|
+
throw new Error(`knodin system ${action}: --require-complete applies only to query`);
|
|
1632
1743
|
if (unsupported.length > 0)
|
|
1633
1744
|
throw new Error(`knodin system ${action}: unknown argument ${unsupported[0]}`);
|
|
1634
1745
|
const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
|
|
@@ -1667,12 +1778,49 @@ async function main() {
|
|
|
1667
1778
|
}
|
|
1668
1779
|
else {
|
|
1669
1780
|
const validation = await validateSystemHealth(config, systemId, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })), repo);
|
|
1670
|
-
result = queryConfiguredSystem(config, systemId,
|
|
1781
|
+
result = queryConfiguredSystem(config, systemId, requireComplete, validation);
|
|
1671
1782
|
if (result.status === "unavailable")
|
|
1672
1783
|
process.exitCode = 1;
|
|
1673
1784
|
}
|
|
1674
1785
|
break;
|
|
1675
1786
|
}
|
|
1787
|
+
case "skills": {
|
|
1788
|
+
const action = invocation.commandPath[1];
|
|
1789
|
+
const requested = Array.isArray(invocation.options.client)
|
|
1790
|
+
? invocation.options.client.map(String)
|
|
1791
|
+
: [];
|
|
1792
|
+
const supported = new Set(["codex", "claude", "gemini"]);
|
|
1793
|
+
for (const client of requested)
|
|
1794
|
+
if (!supported.has(client))
|
|
1795
|
+
throw new Error(`knodin skills: unsupported client ${client}`);
|
|
1796
|
+
const clients = (requested.length ? requested : ["codex", "claude", "gemini"]);
|
|
1797
|
+
const scope = typeof invocation.options.scope === "string"
|
|
1798
|
+
? parseInitScope(invocation.options.scope)
|
|
1799
|
+
: "personal";
|
|
1800
|
+
if (action === "list") {
|
|
1801
|
+
result = { skills: KNODIN_SKILLS, clients, scope };
|
|
1802
|
+
break;
|
|
1803
|
+
}
|
|
1804
|
+
if (action === "install")
|
|
1805
|
+
result = installKnodinSkills({
|
|
1806
|
+
repo,
|
|
1807
|
+
scope,
|
|
1808
|
+
clients,
|
|
1809
|
+
dryRun: invocation.options.dryRun === true,
|
|
1810
|
+
});
|
|
1811
|
+
else if (action === "remove")
|
|
1812
|
+
result = removeKnodinSkills({
|
|
1813
|
+
repo,
|
|
1814
|
+
scope,
|
|
1815
|
+
clients,
|
|
1816
|
+
dryRun: invocation.options.dryRun === true,
|
|
1817
|
+
});
|
|
1818
|
+
else if (action === "doctor")
|
|
1819
|
+
result = inspectKnodinSkills({ repo, scope, clients });
|
|
1820
|
+
else
|
|
1821
|
+
throw new Error("knodin skills requires list, install, remove, or doctor");
|
|
1822
|
+
break;
|
|
1823
|
+
}
|
|
1676
1824
|
case "hook-refresh": {
|
|
1677
1825
|
const [kind, first, second] = rest;
|
|
1678
1826
|
let event;
|
|
@@ -1864,7 +2012,7 @@ async function main() {
|
|
|
1864
2012
|
case "status": {
|
|
1865
2013
|
const snapshot = async () => ({
|
|
1866
2014
|
...attachLifecycleHealth(repo, await engine.status(repo, {
|
|
1867
|
-
audit: rest.includes("--deep") ? "deep" : "
|
|
2015
|
+
audit: rest.includes("--deep") ? "deep" : "adaptive",
|
|
1868
2016
|
})),
|
|
1869
2017
|
integration: inspectRepositoryIntegrationStatus(repo),
|
|
1870
2018
|
update: trustedUpdateStatus({
|
package/dist/bin/launcher.js
CHANGED
|
File without changes
|
package/dist/src/cli-model.js
CHANGED
|
@@ -295,8 +295,38 @@ function createCliProgram(capture = () => { }) {
|
|
|
295
295
|
for (const action of ["show", "validate", "query"]) {
|
|
296
296
|
const command = leaf(system, `${action} <system-id>`, `${action} one configured system`, capture);
|
|
297
297
|
if (action === "query")
|
|
298
|
-
command
|
|
298
|
+
command
|
|
299
|
+
.option("--require-complete", "fail unless every declared repository is available")
|
|
300
|
+
.option("--allow-partial", "compatibility alias; partial results are now the default");
|
|
299
301
|
}
|
|
302
|
+
leaf(system, "link <system-id> <repos...>", "declare repositories as one system", capture)
|
|
303
|
+
.option("--scope <scope>", "personal or team", "personal")
|
|
304
|
+
.option("--distributed", "write a team fragment to every selected repository")
|
|
305
|
+
.addOption(option("--repository-id <path=id>", "override a derived repository id", "collect"))
|
|
306
|
+
.addOption(option("--component <repo=id[:type]>", "override a component id and type", "collect"))
|
|
307
|
+
.option("--init-missing", "initialize repositories that do not yet have knodin lifecycle state")
|
|
308
|
+
.option("--dry-run", "preview exact files and semantic changes");
|
|
309
|
+
leaf(system, "relate <system-id>", "add an explicit evidenced relationship", capture)
|
|
310
|
+
.requiredOption("--type <type>", "relationship type")
|
|
311
|
+
.requiredOption("--from <component>", "source component")
|
|
312
|
+
.requiredOption("--to <component>", "target component")
|
|
313
|
+
.requiredOption("--evidence <repo:path:line>", "checked-in relationship evidence")
|
|
314
|
+
.option("--dry-run", "preview exact files and semantic changes");
|
|
315
|
+
leaf(system, "unlink <system-id> [repos...]", "remove repositories or a whole system", capture).option("--dry-run", "preview exact files and semantic changes");
|
|
316
|
+
leaf(system, "unrelate <system-id>", "remove an explicit relationship", capture)
|
|
317
|
+
.requiredOption("--type <type>", "relationship type")
|
|
318
|
+
.requiredOption("--from <component>", "source component")
|
|
319
|
+
.requiredOption("--to <component>", "target component")
|
|
320
|
+
.option("--dry-run", "preview exact files and semantic changes");
|
|
321
|
+
leaf(system, "import <path>", "register an existing system manifest", capture)
|
|
322
|
+
.option("--scope <scope>", "personal or team", "personal")
|
|
323
|
+
.option("--dry-run", "preview exact files and semantic changes");
|
|
324
|
+
const skills = program.command("skills").description("manage packaged knodin assistant skills");
|
|
325
|
+
for (const action of ["list", "install", "remove", "doctor"])
|
|
326
|
+
leaf(skills, action, `${action} knodin assistant skills`, capture)
|
|
327
|
+
.option("--scope <scope>", "personal, team, or cli-only")
|
|
328
|
+
.addOption(option("--client <client>", "codex, claude, or gemini", "collect"))
|
|
329
|
+
.option("--dry-run", "preview skill changes without writing");
|
|
300
330
|
const update = program
|
|
301
331
|
.command("update")
|
|
302
332
|
.description("inspect or apply threshold-signed updates")
|