@event4u/agent-config 17.3.0 → 17.4.0

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.
Files changed (137) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +164 -342
  5. package/CREDITS.md +2 -2
  6. package/README.md +1 -1
  7. package/dist/agent-src/skills/code-intelligence/SKILL.md +3 -0
  8. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  9. package/dist/cli/registry.js +2 -2
  10. package/dist/cli-delegate/{chunk-DHSSP7V3.js → chunk-5AYJGUA3.js} +0 -2
  11. package/dist/cli-delegate/chunk-DIFZBP4G.js +1336 -0
  12. package/dist/cli-delegate/{chunk-MT2WOOCY.js → chunk-WY5ZZPGL.js} +2 -2
  13. package/dist/cli-delegate/{chunk-ORJPBER2.js → chunk-X6LZ7W5A.js} +73 -10
  14. package/dist/cli-delegate/cmd_conformance.js +175 -183
  15. package/dist/cli-delegate/cmd_doctor.js +5 -5
  16. package/dist/cli-delegate/cmd_doctor_neighbours.js +3 -3
  17. package/dist/cli-delegate/cmd_explain.js +1 -1
  18. package/dist/cli-delegate/cmd_handoff.js +2 -2
  19. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  20. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  21. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  22. package/dist/cli-delegate/cmd_update.js +24 -13
  23. package/dist/cli-delegate/cmd_upgrade.js +2 -2
  24. package/dist/cli-delegate/dist-25AZAHOP.js +3240 -0
  25. package/dist/cli-delegate/dist-P7MFPMF3.js +411 -0
  26. package/dist/cli-delegate/dist-XAS5ZLQB.js +6 -0
  27. package/dist/cli-delegate/{upgrade_unattended-VVL6JLSX.js → upgrade_unattended-QCWOKTUN.js} +178 -163
  28. package/dist/discovery/deprecation-report.md +1 -1
  29. package/dist/discovery/discovery-manifest.json +3 -3
  30. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  31. package/dist/discovery/discovery-manifest.summary.md +1 -1
  32. package/dist/discovery/dormancy-report.md +1 -1
  33. package/dist/discovery/orphan-report.md +1 -1
  34. package/dist/discovery/packs.json +2 -2
  35. package/dist/discovery/trust-report.md +1 -1
  36. package/dist/discovery/workspaces.json +2 -2
  37. package/dist/hooks/code_graph_refresh.chunk.cjs +56 -27
  38. package/dist/hooks/code_graph_refresh.chunk.cjs.sha256 +1 -1
  39. package/dist/hooks/dispatch.js +982 -929
  40. package/dist/hooks/dispatch.sha256 +1 -1
  41. package/dist/hooks/egress_engine.chunk.cjs +56 -6
  42. package/dist/hooks/egress_engine.chunk.cjs.sha256 +1 -1
  43. package/dist/hooks/update_notice.chunk.cjs +914 -0
  44. package/dist/hooks/update_notice.chunk.cjs.sha256 +1 -0
  45. package/dist/mcp/registry-manifest.json +1 -1
  46. package/dist/mcp/server.json +2 -2
  47. package/dist/mcp/server.mjs +71259 -66437
  48. package/dist/scripts/condense.js +9 -6
  49. package/package.json +4 -1
  50. package/src/config/evaluator-budgets.json +9 -8
  51. package/src/config/gate-violation-baselines.json +2 -2
  52. package/src/config/hook-bundle-budget.json +31 -6
  53. package/src/config/packed-binary-manifest.json +9 -0
  54. package/src/config/sigstore/npm_keys.json +50 -0
  55. package/src/config/sigstore/trust-material.json +9 -0
  56. package/src/config/sigstore/trusted_root.json +126 -0
  57. package/src/config/skill-body-budget.json +7 -1
  58. package/src/scripts/_cli/cmd_update.ts +48 -20
  59. package/src/scripts/_cli/cmd_upgrade.ts +7 -1
  60. package/src/scripts/_cli/upgrade_unattended.ts +90 -76
  61. package/src/scripts/_dispatch.bash +3 -2
  62. package/src/scripts/_lib/confusables.ts +0 -19
  63. package/src/scripts/_lib/egress_normalize.ts +53 -23
  64. package/src/scripts/_lib/egress_text.ts +5 -3
  65. package/src/scripts/_lib/egress_token.ts +7 -5
  66. package/src/scripts/_lib/gate_collector_experimental.ts +633 -0
  67. package/src/scripts/_lib/git_common_dir.ts +10 -0
  68. package/src/scripts/_lib/graph_feeder_record.ts +3 -2
  69. package/src/scripts/_lib/install_reach_checks.ts +32 -2
  70. package/src/scripts/_lib/lineage_backfill.ts +136 -0
  71. package/src/scripts/_lib/lineage_ledger.ts +211 -0
  72. package/src/scripts/_lib/lineage_map.ts +55 -0
  73. package/src/scripts/_lib/lineage_query.ts +140 -0
  74. package/src/scripts/_lib/lineage_seed.ts +116 -0
  75. package/src/scripts/_lib/lineage_watch.ts +249 -0
  76. package/src/scripts/_lib/link_crypto.ts +8 -4
  77. package/src/scripts/_lib/production_reach.ts +3 -3
  78. package/src/scripts/_lib/project_registry.ts +227 -0
  79. package/src/scripts/_lib/provenance_verify.ts +14 -242
  80. package/src/scripts/_lib/sigstore_provenance.ts +177 -0
  81. package/src/scripts/_lib/source_id.ts +48 -0
  82. package/src/scripts/_lib/source_shape.ts +4 -0
  83. package/src/scripts/_lib/unattended_gates.ts +6 -1
  84. package/src/scripts/advisories.ts +26 -10
  85. package/src/scripts/ai_council/council_egress.ts +5 -6
  86. package/src/scripts/bench_graph_feeder_latency.ts +10 -3
  87. package/src/scripts/check_dependency_floors.ts +7 -0
  88. package/src/scripts/check_egress_mutations.ts +1 -0
  89. package/src/scripts/code_graph/build.ts +67 -3
  90. package/src/scripts/code_graph/cli.ts +131 -28
  91. package/src/scripts/code_graph/conventions_default.ts +20 -1
  92. package/src/scripts/code_graph/detect.ts +5 -5
  93. package/src/scripts/code_graph/estate_cli.ts +135 -0
  94. package/src/scripts/code_graph/estate_index.ts +394 -0
  95. package/src/scripts/code_graph/estate_manifests.ts +223 -0
  96. package/src/scripts/code_graph/estate_map.ts +141 -0
  97. package/src/scripts/code_graph/extract.ts +4 -1
  98. package/src/scripts/code_graph/extract_more.ts +137 -2
  99. package/src/scripts/code_graph/graph_budget.ts +158 -0
  100. package/src/scripts/code_graph/graph_home.ts +80 -0
  101. package/src/scripts/code_graph/lineage_cli.ts +315 -0
  102. package/src/scripts/code_graph/map.ts +22 -13
  103. package/src/scripts/code_graph/map_model.ts +67 -12
  104. package/src/scripts/code_graph/map_verify.ts +68 -5
  105. package/src/scripts/code_graph/map_viewer/viewer.css +12 -0
  106. package/src/scripts/code_graph/map_viewer/viewer.js +45 -15
  107. package/src/scripts/code_graph/project_ref.ts +25 -0
  108. package/src/scripts/code_graph/projects_cli.ts +254 -0
  109. package/src/scripts/code_graph/providers/catalog.ts +14 -5
  110. package/src/scripts/code_graph/providers/django.ts +52 -9
  111. package/src/scripts/code_graph/providers/index.ts +2 -0
  112. package/src/scripts/code_graph/providers/rails.ts +204 -0
  113. package/src/scripts/code_graph/providers/spring.ts +38 -1
  114. package/src/scripts/code_graph/query.ts +9 -9
  115. package/src/scripts/code_graph/refresh_lock.ts +12 -8
  116. package/src/scripts/code_graph/repo_identity.ts +169 -0
  117. package/src/scripts/code_graph/sqlite_store.ts +22 -25
  118. package/src/scripts/code_graph/types.ts +6 -2
  119. package/src/scripts/condense.ts +6 -6
  120. package/src/scripts/hook_chunks.mjs +7 -0
  121. package/src/scripts/hook_manifest.json +1 -1
  122. package/src/scripts/hook_manifest.yaml +20 -17
  123. package/src/scripts/hooks/code_graph_refresh_hook.ts +10 -7
  124. package/src/scripts/hooks/dispatch_hook.ts +6 -7
  125. package/src/scripts/hooks/host_semantics.ts +72 -0
  126. package/src/scripts/hooks/lazy_concern.ts +4 -0
  127. package/src/scripts/hooks/user_notice.ts +105 -0
  128. package/src/scripts/lint_harvest_provenance.ts +137 -11
  129. package/src/scripts/mcp_server/consumer_tool_catalog.json +69 -15
  130. package/src/scripts/mcp_server/graph_tools.ts +161 -41
  131. package/src/scripts/network_sources.ts +24 -6
  132. package/src/scripts/schemas/provenance-mechanism.schema.json +46 -0
  133. package/src/scripts/update_notice_hook.ts +75 -7
  134. package/src/vendor/grammars/README.md +7 -6
  135. package/src/vendor/grammars/tree-sitter-ruby.wasm +0 -0
  136. /package/dist/cli-delegate/{chunk-FKYZOWVD.js → chunk-QBAFP67O.js} +0 -0
  137. /package/dist/cli-delegate/{chunk-BMGUGSY6.js → chunk-TQG6UMPC.js} +0 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * The storage budget for user-global graph homes (ADR-299, council O4).
3
+ *
4
+ * Only directories under `runtime/graphs/` are ever evicted — a graph under a
5
+ * repository's own `agents/` is the project's, not this machine's cache.
6
+ * Eviction is least-recently-QUERIED first: a query touches its home's
7
+ * `.last-query` marker, a build or refresh records it in the registry, and a
8
+ * registry scan changes neither. A pinned project's homes, a home whose
9
+ * refresh lock is held, and a home written to within a stale-lock window or
10
+ * holding a temp output are never evicted, so a budget can stay exceeded. The
11
+ * last guard exists because a plain `code-graph build` takes no lock.
12
+ */
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+
16
+ import { readRegistry, updateRegistry } from '../_lib/project_registry.js';
17
+ import { graphHome, graphsRoot } from './graph_home.js';
18
+ import { isStale, STALE_LOCK_MS } from './refresh_lock.js';
19
+
20
+ const MB = 1024 * 1024;
21
+ const LAST_QUERY = '.last-query';
22
+
23
+ /** Record that a query just read `root`'s graph. Only user-global homes keep recency. */
24
+ export function touchHome(root: string): void {
25
+ try {
26
+ const home = graphHome(root);
27
+ if (home.kind !== 'global' || !fs.existsSync(home.dir)) return;
28
+ const marker = path.join(home.dir, LAST_QUERY);
29
+ const now = new Date();
30
+ try {
31
+ fs.utimesSync(marker, now, now);
32
+ } catch {
33
+ fs.writeFileSync(marker, '');
34
+ }
35
+ } catch {
36
+ /* recency is a hint; a query never fails on it */
37
+ }
38
+ }
39
+
40
+ interface Walk {
41
+ bytes: number;
42
+ /** Newest mtime of anything below the home except the query marker. */
43
+ newest: number;
44
+ /** A build's or refresh's temp output is present. */
45
+ temp: boolean;
46
+ }
47
+
48
+ const TEMP_RE = /\.refresh-tmp-|\.tmp-/;
49
+
50
+ function walk(dir: string, top: boolean, acc: Walk = { bytes: 0, newest: 0, temp: false }): Walk {
51
+ let entries: fs.Dirent[];
52
+ try {
53
+ entries = fs.readdirSync(dir, { withFileTypes: true });
54
+ } catch {
55
+ return acc;
56
+ }
57
+ for (const e of entries) {
58
+ const p = path.join(dir, e.name);
59
+ if (TEMP_RE.test(e.name)) acc.temp = true;
60
+ let st: fs.Stats;
61
+ try {
62
+ st = fs.lstatSync(p);
63
+ } catch {
64
+ continue;
65
+ }
66
+ if (!(top && e.name === LAST_QUERY)) acc.newest = Math.max(acc.newest, st.mtimeMs);
67
+ if (e.isDirectory()) walk(p, false, acc);
68
+ else acc.bytes += st.size;
69
+ }
70
+ return acc;
71
+ }
72
+
73
+ export interface HomeUsage {
74
+ dir: string;
75
+ bytes: number;
76
+ /** Last query, else last build or refresh, else the directory's own mtime. */
77
+ used_at: number;
78
+ pinned: boolean;
79
+ locked: boolean;
80
+ /** Written within the last {@link STALE_LOCK_MS}, or holding a temp output: a build may be mid-write. */
81
+ busy: boolean;
82
+ project: string | null;
83
+ }
84
+
85
+ /** Every user-global graph home with its size and why it may or may not go. */
86
+ export function homeUsage(): HomeUsage[] {
87
+ const base = graphsRoot();
88
+ let names: string[];
89
+ try {
90
+ names = fs.readdirSync(base);
91
+ } catch {
92
+ return [];
93
+ }
94
+ const reg = readRegistry();
95
+ const owner = new Map<string, { id: string; pinned: boolean; at: number }>();
96
+ for (const [id, e] of Object.entries(reg.projects)) {
97
+ for (const [dir, at] of Object.entries(e.homes)) owner.set(dir, { id, pinned: e.pinned, at: Date.parse(at) || 0 });
98
+ }
99
+ const out: HomeUsage[] = [];
100
+ const now = Date.now();
101
+ for (const name of names) {
102
+ const dir = path.join(base, name);
103
+ let st: fs.Stats;
104
+ try {
105
+ st = fs.statSync(dir);
106
+ } catch {
107
+ continue;
108
+ }
109
+ if (!st.isDirectory()) continue;
110
+ let queried = 0;
111
+ try {
112
+ queried = fs.statSync(path.join(dir, LAST_QUERY)).mtimeMs;
113
+ } catch {
114
+ queried = 0;
115
+ }
116
+ const o = owner.get(dir);
117
+ const lock = path.join(dir, 'state', 'code-graph-refresh.lock');
118
+ const w = walk(dir, true);
119
+ out.push({
120
+ dir,
121
+ bytes: w.bytes,
122
+ used_at: Math.max(queried, o?.at ?? 0) || st.mtimeMs,
123
+ pinned: o?.pinned ?? false,
124
+ locked: fs.existsSync(lock) && !isStale(lock),
125
+ busy: w.temp || now - w.newest < STALE_LOCK_MS,
126
+ project: o?.id ?? null,
127
+ });
128
+ }
129
+ return out;
130
+ }
131
+
132
+ /**
133
+ * Evict the least recently used homes until the total fits `budget_mb`;
134
+ * returns what went. `keep` is the home a build just wrote, which is never its
135
+ * own victim.
136
+ */
137
+ export function enforceBudget(keep?: string): { evicted: HomeUsage[]; total: number; budget: number } {
138
+ const budget = readRegistry().budget_mb * MB;
139
+ const homes = homeUsage();
140
+ let total = homes.reduce((n, h) => n + h.bytes, 0);
141
+ const evicted: HomeUsage[] = [];
142
+ for (const h of [...homes].sort((a, b) => a.used_at - b.used_at)) {
143
+ if (total <= budget) break;
144
+ if (h.pinned || h.locked || h.busy || h.dir === keep) continue;
145
+ fs.rmSync(h.dir, { recursive: true, force: true });
146
+ total -= h.bytes;
147
+ evicted.push(h);
148
+ }
149
+ if (evicted.length) {
150
+ const gone = new Set(evicted.map((h) => h.dir));
151
+ updateRegistry((reg) => {
152
+ for (const e of Object.values(reg.projects)) {
153
+ for (const dir of Object.keys(e.homes)) if (gone.has(dir)) delete e.homes[dir];
154
+ }
155
+ });
156
+ }
157
+ return { evicted, total, budget };
158
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Where a root's graph, map and refresh lock live (ADR-299).
3
+ *
4
+ * A root that has `agents/` or a project install keeps today's paths under
5
+ * `<root>/agents/`, byte-identical, so a map shared through the repository is
6
+ * unchanged. Every other root gets a home under the user-global
7
+ * `runtime/graphs/<repo id>-<checkout | instance>/`, so building a graph of
8
+ * somebody else's repository writes nothing into it, and two clones of one
9
+ * origin never share a cache, lock or budget slot. Resolving never creates `agents/`.
10
+ */
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+
14
+ import { type EnvMap, event4u_root } from '../_lib/user_global_paths.js';
15
+ import { repoIdentity } from './repo_identity.js';
16
+
17
+ export const STATE_DIR_REL = path.join('agents', 'runtime', 'state');
18
+ export const NATIVE_CACHE_REL = path.join(STATE_DIR_REL, 'code-graph-v1.json');
19
+ export const MAP_DIR_REL = path.join('agents', 'map');
20
+ export const LOCK_REL = path.join(STATE_DIR_REL, 'code-graph-refresh.lock');
21
+ /** Written by `code-graph auto-refresh on`; the session concern acts only while it exists. */
22
+ export const AUTO_REFRESH_REL = path.join(STATE_DIR_REL, 'code-graph-auto-refresh.json');
23
+
24
+ export interface GraphHome {
25
+ kind: 'project' | 'global';
26
+ root: string;
27
+ /** `<root>/agents` for a project home, the per-repository directory otherwise. */
28
+ dir: string;
29
+ stateDir: string;
30
+ cachePath: string;
31
+ mapDir: string;
32
+ lockPath: string;
33
+ autoRefreshPath: string;
34
+ repoId: string | null;
35
+ instance: string | null;
36
+ }
37
+
38
+ export function graphsRoot(env?: EnvMap | null): string {
39
+ return path.join(event4u_root(env), 'runtime', 'graphs');
40
+ }
41
+
42
+ /** `agents/` exists, or the installer's settings file marks a project install. */
43
+ export function hasProjectHome(root: string): boolean {
44
+ return fs.existsSync(path.join(root, 'agents')) || fs.existsSync(path.join(root, '.agent-settings.yml'));
45
+ }
46
+
47
+ /** Today's in-repository paths; callers that serve project homes only use this alone. */
48
+ export function projectHome(root: string): GraphHome {
49
+ return {
50
+ kind: 'project',
51
+ root,
52
+ dir: path.join(root, 'agents'),
53
+ stateDir: path.join(root, STATE_DIR_REL),
54
+ cachePath: path.join(root, NATIVE_CACHE_REL),
55
+ mapDir: path.join(root, MAP_DIR_REL),
56
+ lockPath: path.join(root, LOCK_REL),
57
+ autoRefreshPath: path.join(root, AUTO_REFRESH_REL),
58
+ repoId: null,
59
+ instance: null,
60
+ };
61
+ }
62
+
63
+ export function graphHome(root: string, env?: EnvMap | null): GraphHome {
64
+ if (hasProjectHome(root)) return projectHome(root);
65
+ const id = repoIdentity(root, env);
66
+ const dir = path.join(graphsRoot(env), `${id.repoId}-${id.instance ?? id.checkout}`);
67
+ const stateDir = path.join(dir, 'state');
68
+ return {
69
+ kind: 'global',
70
+ root,
71
+ dir,
72
+ stateDir,
73
+ cachePath: path.join(stateDir, path.basename(NATIVE_CACHE_REL)),
74
+ mapDir: path.join(dir, 'map'),
75
+ lockPath: path.join(stateDir, path.basename(LOCK_REL)),
76
+ autoRefreshPath: path.join(stateDir, path.basename(AUTO_REFRESH_REL)),
77
+ repoId: id.repoId,
78
+ instance: id.instance,
79
+ };
80
+ }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * `code-graph lineage <verb>` — where harvested mechanisms came from, and what is still missing.
3
+ *
4
+ * Two ledgers answer, never one (council O5): the tracked, source-free decision ledger
5
+ * `provenance/mechanisms.jsonl` of the repo at `--root`, and the local source ledger under
6
+ * the user-global `runtime/lineage/` (`_lib/lineage_ledger.ts`). A verb that needs the
7
+ * local side and the key refuses without them instead of answering from half the data.
8
+ *
9
+ * Exit codes: 0 ok · 1 findings · 2 usage or refused.
10
+ */
11
+
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+
15
+ import {
16
+ assertOutsideWorkTree,
17
+ checkLedger,
18
+ hasLedger,
19
+ LineageError,
20
+ lineageDir,
21
+ mergeIntoLedger,
22
+ readLedger,
23
+ writeLedger,
24
+ writePrivateFile,
25
+ } from '../_lib/lineage_ledger.js';
26
+ import { BACKFILL_DIR_REL, isIgnored, scanTree, writeBackfill } from '../_lib/lineage_backfill.js';
27
+ import { LINEAGE_VIEW_REL, renderSourceLineage, renderTrackedLineage } from '../_lib/lineage_map.js';
28
+ import { refuseUnderCi, renderWatchReport, renderWatchStub, runWatch, systemGit } from '../_lib/lineage_watch.js';
29
+ import { gaps, lineageFrom, lineageOf, readDecisions, rejected } from '../_lib/lineage_query.js';
30
+ import { buildLocalImport } from '../_lib/lineage_seed.js';
31
+ import { resolve_keys } from '../_lib/link_crypto.js';
32
+ import type { EnvMap } from '../_lib/user_global_paths.js';
33
+
34
+ export interface LineageDeps {
35
+ readonly env?: EnvMap;
36
+ /** Link keys; resolved from settings when absent. Tests pass them explicitly. */
37
+ readonly keys?: readonly string[];
38
+ readonly now?: () => Date;
39
+ readonly stdout?: (s: string) => void;
40
+ readonly stderr?: (s: string) => void;
41
+ }
42
+
43
+ export interface Io {
44
+ readonly env: EnvMap;
45
+ readonly root: string;
46
+ readonly keys: () => readonly string[];
47
+ readonly now: () => Date;
48
+ readonly out: (s: string) => void;
49
+ readonly err: (s: string) => void;
50
+ }
51
+
52
+ export function flag(argv: readonly string[], name: string): string | undefined {
53
+ const i = argv.indexOf(name);
54
+ return i === -1 ? undefined : argv[i + 1];
55
+ }
56
+
57
+ function cmdCheck(io: Io): number {
58
+ const dir = lineageDir(io.env);
59
+ if (!hasLedger(dir)) {
60
+ io.err(`code-graph lineage: no local lineage ledger at ${dir} — run \`code-graph lineage import\` first\n`);
61
+ return 2;
62
+ }
63
+ const findings = checkLedger(dir, io.keys());
64
+ const { sources, edges } = readLedger(dir);
65
+ if (findings.length > 0) {
66
+ for (const f of findings) io.err(`${f.file}:${String(f.line)} ${f.message}\n`);
67
+ io.err(`code-graph lineage check: ${String(findings.length)} finding(s)\n`);
68
+ return 1;
69
+ }
70
+ io.out(`code-graph lineage check: ${String(sources.length)} source(s), ${String(edges.length)} edge(s) OK at ${dir}\n`);
71
+ return 0;
72
+ }
73
+
74
+ /** Refuse, naming the setting, when no link key is configured. */
75
+ export function needKeys(io: Io, verb: string): readonly string[] | null {
76
+ const keys = io.keys();
77
+ if (keys.length > 0) return keys;
78
+ io.err(
79
+ `code-graph lineage ${verb}: no link_encryption_key configured (secrets.link_encryption_key) — ` +
80
+ 'the local ledger is sealed with it, so nothing is read or written without it\n',
81
+ );
82
+ return null;
83
+ }
84
+
85
+ function cmdImport(io: Io): number {
86
+ const keys = needKeys(io, 'import');
87
+ if (keys === null) return 2;
88
+ const { ledger, refused } = buildLocalImport(io.root, keys);
89
+ const dir = lineageDir(io.env);
90
+ const added = mergeIntoLedger(dir, ledger, keys);
91
+ for (const r of refused) io.err(`code-graph lineage import: skipped ${r}\n`);
92
+ io.out(
93
+ `code-graph lineage import: ${String(added.sources)} new source(s), ${String(added.edges)} new edge(s) into ${dir}` +
94
+ `${refused.length > 0 ? ` · ${String(refused.length)} skipped` : ''}\n`,
95
+ );
96
+ return refused.length > 0 ? 1 : 0;
97
+ }
98
+
99
+ /** The graph's node lookup for `root`, or null when no graph has been built. */
100
+ async function graphLookup(io: Io, explicit: string | undefined): Promise<{ has(id: string): boolean } | null> {
101
+ // Dynamic: cli.ts dispatches into this module, and the default cache path is its to own.
102
+ const graphPath = explicit ?? (await import('./cli.js')).cacheFor(io.root);
103
+ if (!fs.existsSync(graphPath)) return null;
104
+ const { loadGraph } = await import('./query.js');
105
+ return loadGraph(graphPath).byId;
106
+ }
107
+
108
+ async function cmdOf(io: Io, query: string | undefined, graphFlag: string | undefined): Promise<number> {
109
+ if (query === undefined || query.startsWith('--')) {
110
+ io.err('usage: code-graph lineage of <mechanism-id|path> [--root P] [--graph P]\n');
111
+ return 2;
112
+ }
113
+ const entries = lineageOf(io.root, query, await graphLookup(io, graphFlag));
114
+ if (entries.length === 0) {
115
+ io.err(`code-graph lineage of: no mechanism matches ${query} in provenance/mechanisms.jsonl\n`);
116
+ return 1;
117
+ }
118
+ let broken = 0;
119
+ for (const e of entries) {
120
+ io.out(`${e.mechanism_id} ${e.verdict} ${e.decided_at} (${String(e.decisions)} decision(s))\n`);
121
+ for (const r of e.realised) {
122
+ if (r.status === 'broken') broken += 1;
123
+ const g = r.in_graph === null ? 'no graph built' : r.in_graph ? 'indexed' : 'not indexed';
124
+ io.out(` realised_in ${r.path} ${r.status === 'ok' ? 'ok' : 'BROKEN'} graph: ${g}\n`);
125
+ }
126
+ }
127
+ if (broken > 0) io.err(`code-graph lineage of: ${String(broken)} broken edge(s) — the file moved or is gone\n`);
128
+ return broken > 0 ? 1 : 0;
129
+ }
130
+
131
+ function cmdGaps(io: Io): number {
132
+ const view = gaps(readDecisions(io.root));
133
+ for (const g of view) {
134
+ io.out(`${g.mechanism_id} ${g.verdict} ${g.owner}${g.reopen_if ? ` reopen if: ${g.reopen_if}` : ''}\n`);
135
+ }
136
+ io.out(`code-graph lineage gaps: ${String(view.length)} open mechanism(s)\n`);
137
+ return 0;
138
+ }
139
+
140
+ function cmdRejected(io: Io): number {
141
+ const view = rejected(readDecisions(io.root));
142
+ for (const r of view) {
143
+ io.out(`${r.mechanism_id} ${r.reason ?? ''}${r.reopen_if ? ` reopen if: ${r.reopen_if}` : ''}\n`);
144
+ }
145
+ io.out(`code-graph lineage rejected: ${String(view.length)} rejected mechanism(s)\n`);
146
+ return 0;
147
+ }
148
+
149
+ function cmdFrom(io: Io, query: string | undefined): number {
150
+ if (query === undefined || query.startsWith('--')) {
151
+ io.err('usage: code-graph lineage from <source-id|identity> [--root P]\n');
152
+ return 2;
153
+ }
154
+ const dir = lineageDir(io.env);
155
+ if (!hasLedger(dir)) {
156
+ io.err(`code-graph lineage from: no local lineage ledger at ${dir} — the source side exists only on the owner's machine\n`);
157
+ return 2;
158
+ }
159
+ const keys = needKeys(io, 'from');
160
+ if (keys === null) return 2;
161
+ const found = lineageFrom(readLedger(dir), query, keys, readDecisions(io.root));
162
+ if (found === null) {
163
+ io.err('code-graph lineage from: no such source in the local ledger\n');
164
+ return 1;
165
+ }
166
+ io.out(`${found.source.source_id} ${found.source.kind} ${found.identity} license ${found.source.license} watch ${found.source.watch ? 'on' : 'off'}\n`);
167
+ for (const m of found.mechanisms) {
168
+ io.out(` ${m.mechanism_id} ${m.verdict ?? '(no decision)'} pin ${m.pin}${m.locator ? ` at ${m.locator}` : ''}\n`);
169
+ }
170
+ return 0;
171
+ }
172
+
173
+ /**
174
+ * `map --lineage` (tracked view, verdicts only) or `map --lineage --sources` (local view).
175
+ * The tracked view is written only into a repo that already has `agents/`, so a foreign
176
+ * repository never gains a directory from it.
177
+ */
178
+ function cmdMap(io: Io, argv: readonly string[]): number {
179
+ const outFlag = flag(argv, '--out');
180
+ if (argv.includes('--sources')) {
181
+ const out = path.resolve(outFlag ?? path.join(lineageDir(io.env), 'map', 'sources.md'));
182
+ assertOutsideWorkTree(out);
183
+ const dir = lineageDir(io.env);
184
+ if (!hasLedger(dir)) {
185
+ io.err(`code-graph lineage map --sources: no local lineage ledger at ${dir}\n`);
186
+ return 2;
187
+ }
188
+ const keys = needKeys(io, 'map --sources');
189
+ if (keys === null) return 2;
190
+ writePrivateFile(out, renderSourceLineage(readLedger(dir), keys, readDecisions(io.root)));
191
+ io.out(`code-graph lineage map: source view written to ${out}\n`);
192
+ return 0;
193
+ }
194
+ const out = path.resolve(outFlag ?? path.join(io.root, LINEAGE_VIEW_REL));
195
+ if (outFlag === undefined && !fs.existsSync(path.join(io.root, 'agents'))) {
196
+ io.err(`code-graph lineage map: ${io.root} has no agents/ directory — pass --out to choose where the view goes\n`);
197
+ return 2;
198
+ }
199
+ fs.mkdirSync(path.dirname(out), { recursive: true });
200
+ fs.writeFileSync(out, renderTrackedLineage(readDecisions(io.root)));
201
+ io.out(`code-graph lineage map: tracked view written to ${out}\n`);
202
+ return 0;
203
+ }
204
+
205
+ function cmdBackfill(io: Io, argv: readonly string[]): number {
206
+ if (!argv.includes('--draft')) {
207
+ io.err('code-graph lineage backfill: only --draft exists — the backfill proposes drafts and never writes a ledger row\n');
208
+ return 2;
209
+ }
210
+ if (!isIgnored(io.root, BACKFILL_DIR_REL)) {
211
+ io.err(`code-graph lineage backfill: ${BACKFILL_DIR_REL}/ is not ignored by git in ${io.root} — decrypted identities would become trackable\n`);
212
+ return 2;
213
+ }
214
+ const keys = needKeys(io, 'backfill');
215
+ if (keys === null) return 2;
216
+ const result = scanTree(io.root, keys);
217
+ const out = writeBackfill(io.root, result);
218
+ for (const u of result.unclear) io.out(`unclear ${u}\n`);
219
+ for (const u of result.undecryptable) io.out(`undecryptable ${u}\n`);
220
+ io.out(
221
+ `code-graph lineage backfill: ${String(result.drafts.length)} draft(s), ${String(result.unclear.length)} unclear, ` +
222
+ `${String(result.undecryptable.length)} undecryptable — written to ${out}; no ledger row written\n`,
223
+ );
224
+ return 0;
225
+ }
226
+
227
+ function cmdWatchFlag(io: Io, on: boolean, id: string | undefined): number {
228
+ const dir = lineageDir(io.env);
229
+ if (id === undefined || !hasLedger(dir)) {
230
+ io.err(`usage: code-graph lineage watch ${on ? 'enable' : 'disable'} <source-id> (needs the local ledger at ${dir})\n`);
231
+ return 2;
232
+ }
233
+ const ledger = readLedger(dir);
234
+ const hit = ledger.sources.find((s) => s.source_id === id);
235
+ if (hit === undefined) {
236
+ io.err(`code-graph lineage watch: no source ${id} in the local ledger\n`);
237
+ return 1;
238
+ }
239
+ writeLedger(dir, { sources: ledger.sources.map((s) => (s.source_id === id ? { ...s, watch: on } : s)), edges: ledger.edges });
240
+ io.out(`code-graph lineage watch: ${id} ${on ? 'on' : 'off'}\n`);
241
+ return 0;
242
+ }
243
+
244
+ function cmdWatch(io: Io, argv: readonly string[]): number {
245
+ if (argv[1] === 'enable' || argv[1] === 'disable') return cmdWatchFlag(io, argv[1] === 'enable', argv[2]);
246
+ refuseUnderCi(io.env);
247
+ const dir = lineageDir(io.env);
248
+ if (!hasLedger(dir)) {
249
+ io.err(`code-graph lineage watch: no local lineage ledger at ${dir}\n`);
250
+ return 2;
251
+ }
252
+ const keys = needKeys(io, 'watch');
253
+ if (keys === null) return 2;
254
+ const reports = runWatch({ dir, keys, rows: readDecisions(io.root), git: systemGit, env: io.env });
255
+ for (const r of reports) {
256
+ io.out(`${r.source_id} ${r.status}${r.note ? ` ${r.note}` : ''}\n`);
257
+ for (const f of r.findings) io.out(` ${f.class} ${f.mechanism_id ?? '(no mechanism)'} ${f.locator}\n`);
258
+ }
259
+ io.out(`code-graph lineage watch: ${String(reports.length)} watched source(s)\n`);
260
+ if (argv.includes('--dry-run')) return 0;
261
+
262
+ const date = io.now().toISOString().slice(0, 10);
263
+ if (reports.length > 0) {
264
+ const reportPath = path.join(dir, 'watch', `report-${date}.md`);
265
+ writePrivateFile(reportPath, renderWatchReport(reports, date));
266
+ io.out(`code-graph lineage watch: report written to ${reportPath}\n`);
267
+ }
268
+ const stub = renderWatchStub(reports, date, keys[0] as string);
269
+ if (stub === null) return 0;
270
+ const stubDir = path.join(io.root, 'agents', 'roadmaps', 'stubs');
271
+ if (!fs.existsSync(stubDir)) {
272
+ io.err(`code-graph lineage watch: ${io.root} has no agents/roadmaps/stubs/ — the stub was not written\n`);
273
+ return 2;
274
+ }
275
+ const stubPath = path.join(stubDir, `road-to-lineage-delta-${date}.md`);
276
+ fs.writeFileSync(stubPath, stub);
277
+ io.out(`code-graph lineage watch: stub written to ${stubPath}\n`);
278
+ return 0;
279
+ }
280
+
281
+ const USAGE = 'usage: code-graph lineage <check|import|of|gaps|rejected|from|map|backfill|watch> [args] [--root P]\n';
282
+
283
+ export async function main(argv: readonly string[], deps: LineageDeps = {}): Promise<number> {
284
+ const env = deps.env ?? process.env;
285
+ const root = path.resolve(flag(argv, '--root') ?? process.cwd());
286
+ let cached: readonly string[] | null = null;
287
+ const io: Io = {
288
+ env,
289
+ root,
290
+ keys: () => (cached ??= deps.keys ?? resolve_keys(root)),
291
+ now: deps.now ?? (() => new Date()),
292
+ out: deps.stdout ?? ((s) => void process.stdout.write(s)),
293
+ err: deps.stderr ?? ((s) => void process.stderr.write(s)),
294
+ };
295
+ const verb = argv[0];
296
+ try {
297
+ if (verb === 'check') return cmdCheck(io);
298
+ if (verb === 'import') return cmdImport(io);
299
+ if (verb === 'of') return await cmdOf(io, argv[1], flag(argv, '--graph'));
300
+ if (verb === 'gaps') return cmdGaps(io);
301
+ if (verb === 'rejected') return cmdRejected(io);
302
+ if (verb === 'from') return cmdFrom(io, argv[1]);
303
+ if (verb === 'map') return cmdMap(io, argv);
304
+ if (verb === 'backfill') return cmdBackfill(io, argv);
305
+ if (verb === 'watch') return cmdWatch(io, argv);
306
+ } catch (e) {
307
+ if (e instanceof LineageError) {
308
+ io.err(`code-graph lineage: ${e.message}\n`);
309
+ return 2;
310
+ }
311
+ throw e;
312
+ }
313
+ io.err(USAGE);
314
+ return 2;
315
+ }
@@ -23,7 +23,8 @@ import { write_atomic } from '../_lib/fs_atomic.js';
23
23
  import { hardenedSpawnEnv } from '../_lib/spawn_env.js';
24
24
  import { buildFromRepo, readProjectProbe } from './build.js';
25
25
  import { acquire as acquireRefreshLock, heldByThisProcess } from './refresh_lock.js';
26
- import { computeVerdict, graphState, NATIVE_CACHE_REL } from './detect.js';
26
+ import { computeVerdict, graphState } from './detect.js';
27
+ import { graphHome } from './graph_home.js';
27
28
  import {
28
29
  areaDrift,
29
30
  buildMapModel,
@@ -39,7 +40,6 @@ import type { CodeGraph } from './types.js';
39
40
  const _HERE = path.dirname(fileURLToPath(import.meta.url));
40
41
  const VIEWER_DIR = path.join(_HERE, 'map_viewer');
41
42
 
42
- export const MAP_DIR_REL = path.join('agents', 'map');
43
43
  /** Rough characters-per-token used to hold MAP.md to its budget. */
44
44
  const CHARS_PER_TOKEN = 4;
45
45
  export const MAP_TOKEN_BUDGET = 2000;
@@ -233,12 +233,14 @@ export function diffFiles(root: string, rev: string): string[] | null {
233
233
  }
234
234
 
235
235
  /** Where `code-graph outcome` records which files a session found useful — local, never sent anywhere. */
236
- export const OUTCOMES_REL = path.join('agents', 'runtime', 'state', 'map-outcomes.jsonl');
236
+ export function outcomesPath(root: string): string {
237
+ return path.join(graphHome(root).stateDir, 'map-outcomes.jsonl');
238
+ }
237
239
 
238
240
  export function readOutcomes(root: string): OutcomeEvent[] {
239
241
  let text: string;
240
242
  try {
241
- text = fs.readFileSync(path.join(root, OUTCOMES_REL), 'utf-8');
243
+ text = fs.readFileSync(outcomesPath(root), 'utf-8');
242
244
  } catch {
243
245
  return [];
244
246
  }
@@ -259,7 +261,7 @@ export function readOutcomes(root: string): OutcomeEvent[] {
259
261
  }
260
262
 
261
263
  export function recordOutcome(root: string, file: string, outcome: OutcomeEvent['outcome'], at: Date = new Date()): void {
262
- const p = path.join(root, OUTCOMES_REL);
264
+ const p = outcomesPath(root);
263
265
  fs.mkdirSync(path.dirname(p), { recursive: true });
264
266
  const rel = path.relative(fs.realpathSync(root), fs.realpathSync(path.resolve(file)));
265
267
  if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) throw new Error(`not a file inside ${root}: ${file}`);
@@ -310,7 +312,8 @@ export async function buildMap(
310
312
  let drawing = false;
311
313
 
312
314
  async function drawGeneration(root: string, opts: Parameters<typeof buildMap>[1] = {}): Promise<MapBuildResult> {
313
- const outDir = opts.outDir ?? path.join(root, MAP_DIR_REL);
315
+ const home = graphHome(root);
316
+ const outDir = opts.outDir ?? home.mapDir;
314
317
  // A chosen --out directory is only ever written into when it is empty or
315
318
  // already holds a map: the writer clears stale area pages, and that must
316
319
  // never reach a directory of someone else's Markdown.
@@ -322,7 +325,7 @@ async function drawGeneration(root: string, opts: Parameters<typeof buildMap>[1]
322
325
  ) {
323
326
  throw new Error(`refusing to write a map into ${outDir}: it is not empty and holds no map.json`);
324
327
  }
325
- const cachePath = opts.cachePath ?? path.join(root, NATIVE_CACHE_REL);
328
+ const cachePath = opts.cachePath ?? home.cachePath;
326
329
  const { graph, built } = await loadOrBuildGraph(root, cachePath, opts.noBuild === true);
327
330
  let diff: { rev: string; changed: string[] } | undefined;
328
331
  if (opts.diffRev !== undefined) {
@@ -364,7 +367,7 @@ async function drawGeneration(root: string, opts: Parameters<typeof buildMap>[1]
364
367
  // The default map stages under the gitignored runtime dir, so a killed redraw
365
368
  // leaves nothing in agents/ itself; a chosen --out stages beside itself.
366
369
  const suffix = `${process.pid}-${randomBytes(4).toString('hex')}`;
367
- const stage = opts.outDir === undefined ? path.join(root, 'agents', 'runtime', 'state') : path.dirname(outDir);
370
+ const stage = opts.outDir === undefined ? home.stateDir : path.dirname(outDir);
368
371
  fs.mkdirSync(stage, { recursive: true });
369
372
  const next = path.join(stage, `.map-next-${suffix}`);
370
373
  const prev = path.join(stage, `.map-prev-${suffix}`);
@@ -633,7 +636,7 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
633
636
  const rank = (r: (typeof rp)[number]): number =>
634
637
  (r.route.startsWith('GET ') ? 0 : 2) + (r.views.length > 0 ? 0 : 1);
635
638
  const withChain = rp
636
- .filter((r) => r.views.length + r.tables.length > 0)
639
+ .filter((r) => r.views.length + r.tables.length + (r.inferredTables?.length ?? 0) > 0)
637
640
  .sort((a, b) => rank(a) - rank(b) || (a.route < b.route ? -1 : a.route > b.route ? 1 : 0));
638
641
  sections.push([
639
642
  'Request paths',
@@ -641,7 +644,8 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
641
644
  '## Request paths',
642
645
  '',
643
646
  `${m.analyses.requestPathsTotal} declared routes; ${m.analyses.requestPathsResolved} reach a handler in this tree. ` +
644
- 'Route → handler → views it renders · tables its models persist:',
647
+ 'Route → handler → views it renders · tables its models persist, up to three calls deep ' +
648
+ '(inferred: reached through a call bound by name alone):',
645
649
  '',
646
650
  ...(withChain.length ? withChain : rp).slice(0, 10).map((r) => {
647
651
  const handlerFile = path.posix.basename(f[r.handler]?.path ?? '');
@@ -649,6 +653,7 @@ export function renderMapMd(m: MapModel, meta: MapMeta): string {
649
653
  handlerFile.replace(/\.php$/, '') === r.handlerSymbol ? handlerFile : `${handlerFile}::${r.handlerSymbol}`,
650
654
  ...r.views.map((v) => path.posix.basename(f[v]?.path ?? '')),
651
655
  ...r.tables.map((t) => `table ${code(t)}`),
656
+ ...(r.inferredTables ?? []).map((t) => `table ${code(t)} (inferred)`),
652
657
  ];
653
658
  return `- ${code(r.route)} → ${chain.map((c) => (c.startsWith('table') ? c : `${code(c)}`)).join(' → ')}`;
654
659
  }),
@@ -872,7 +877,7 @@ export function renderHtml(m: MapModel, meta: MapMeta): string {
872
877
 
873
878
  /** The map a previous `code-graph map` wrote, or null when there is none. */
874
879
  export function readMap(root: string): { meta: MapMeta; model: MapModel } | null {
875
- return readMapAt(path.join(root, MAP_DIR_REL));
880
+ return readMapAt(graphHome(root).mapDir);
876
881
  }
877
882
 
878
883
  function readMapAt(dir: string): { meta: MapMeta; model: MapModel } | null {
@@ -918,7 +923,11 @@ export interface AreaReport {
918
923
  * One area, found by id (`a3`), by exact name, or by a file it contains —
919
924
  * exact matches only, so the answer is always about the area that was named.
920
925
  */
921
- export function areaReport(m: MapModel, query: string): AreaReport | { refusal: string; candidates: string[] } {
926
+ export function areaReport(
927
+ m: MapModel,
928
+ query: string,
929
+ pageDir: string = path.posix.join('agents', 'map'),
930
+ ): AreaReport | { refusal: string; candidates: string[] } {
922
931
  let i = m.areas.findIndex((a) => a.id === query || a.name === query);
923
932
  if (i < 0) {
924
933
  const f = m.files.findIndex((x) => x.path === query);
@@ -974,6 +983,6 @@ export function areaReport(m: MapModel, query: string): AreaReport | { refusal:
974
983
  .sort((x, y) => y[1] - x[1] || x[0] - y[0])
975
984
  .slice(0, cap)
976
985
  .map(([ai, w]) => ({ area: `${m.areas[ai]?.id ?? ''} ${m.areas[ai]?.name ?? ''}`, links: w })),
977
- page: a.files.length >= AREA_PAGE_MIN_FILES ? path.posix.join('agents', 'map', 'areas', `${a.id}.md`) : null,
986
+ page: a.files.length >= AREA_PAGE_MIN_FILES ? path.posix.join(pageDir, 'areas', `${a.id}.md`) : null,
978
987
  };
979
988
  }