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 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
- return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).\n${freshnessLine}${lifecycleLine}${integrationLine}`;
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}).\n${freshnessLine}${lifecycleLine}${integrationLine}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.\n`;
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(systemConfig, target),
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(systemConfig, target),
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(systemConfig, target),
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 allowPartial = rest.includes("--allow-partial");
1626
- const [action, systemId, ...unsupported] = rest.filter((argument) => argument !== "--allow-partial");
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 (allowPartial && action !== "query")
1631
- throw new Error(`knodin system ${action}: --allow-partial applies only to query`);
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, allowPartial, validation);
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" : "cached",
2015
+ audit: rest.includes("--deep") ? "deep" : "adaptive",
1868
2016
  })),
1869
2017
  integration: inspectRepositoryIntegrationStatus(repo),
1870
2018
  update: trustedUpdateStatus({
File without changes
@@ -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.option("--allow-partial", "return healthy components");
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")