claude-spotter 1.4.4 → 1.4.6
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/CHANGELOG.md +59 -0
- package/README.ja.md +3 -3
- package/README.md +4 -4
- package/bin/spotter.mjs +0 -0
- package/package.json +1 -1
- package/src/cli/db-cmd.mjs +7 -8
- package/src/cli/doctor.mjs +12 -9
- package/src/cli/install.mjs +18 -6
- package/src/tool-db/loader.mjs +6 -3
- package/src/tool-db/refresh.mjs +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.6
|
|
4
|
+
|
|
5
|
+
**Codex 初回セッションが空 catalog に依存し得る穴を修正**。v1.4.5 までは
|
|
6
|
+
`spotter install` が Codex hooks を登録しても Codex host-local DB は seed せず、
|
|
7
|
+
初回 Codex セッションの `SessionStart` が detached `spotter db refresh --host-agent codex`
|
|
8
|
+
を起動するだけだった。そのため最初の `UserPromptSubmit` が refresh 完了前に走ると
|
|
9
|
+
`.spotter/tool-db.codex.json` が空 / 未作成のまま Codex auditor が動き得た。
|
|
10
|
+
|
|
11
|
+
### 変更点
|
|
12
|
+
|
|
13
|
+
- **編集 [src/cli/install.mjs](src/cli/install.mjs)**:
|
|
14
|
+
Codex CLI が見つかり Codex hooks を登録した project install では、Claude DB seed に続いて
|
|
15
|
+
`refresh({hostAgent:"codex"})` も同期実行し、`.spotter/tool-db.codex.json` と
|
|
16
|
+
`~/.spotter/tool-db.codex.json` を作るようにした。以降の Codex `SessionStart` bg refresh は
|
|
17
|
+
drift 追従用として残す。Codex CLI が見つからなかった場合の next steps も、Codex hooks が
|
|
18
|
+
active ではないことと `codex --version` が通る環境で再実行すべきことを明示する。
|
|
19
|
+
- **編集 [test/install.test.mjs](test/install.test.mjs)**:
|
|
20
|
+
Codex hooks 登録時に Claude / Codex の両 host DB refresh が順に走ること、Codex CLI
|
|
21
|
+
unavailable 時は Codex seed へ進まないことを固定。
|
|
22
|
+
- **編集 README / README.ja / docs**:
|
|
23
|
+
Codex DB は install 時に初回 seed され、SessionStart refresh は以後の drift 追従であることを明記。
|
|
24
|
+
|
|
25
|
+
### ユーザー側で必要な手順
|
|
26
|
+
|
|
27
|
+
1. `npm install -g claude-spotter@1.4.6`
|
|
28
|
+
2. Codex を使う各プロジェクトで `spotter install` を再実行
|
|
29
|
+
|
|
30
|
+
## 1.4.5
|
|
31
|
+
|
|
32
|
+
**Codex global tool-db を Claude global tool-db から分離**。v1.4.4 までは local DB は
|
|
33
|
+
`.spotter/tool-db.json` と `.spotter/tool-db.codex.json` に分かれていたが、refresh 時の
|
|
34
|
+
description 再利用 cache は Claude / Codex とも `~/.spotter/tool-db.json` を共有していた。
|
|
35
|
+
Claude 側で苦労して塞いだ「別環境の DB が監査視野に混ざる」設計事故を Codex 側で再発させないため、
|
|
36
|
+
host-global DB も分離した。
|
|
37
|
+
|
|
38
|
+
### 変更点
|
|
39
|
+
|
|
40
|
+
- **編集 [src/tool-db/loader.mjs](src/tool-db/loader.mjs)**:
|
|
41
|
+
`globalDbPath(hostAgent)` を host-aware にし、Claude は既存互換の
|
|
42
|
+
`~/.spotter/tool-db.json`、Codex は `~/.spotter/tool-db.codex.json` を使うようにした。
|
|
43
|
+
- **編集 [src/tool-db/refresh.mjs](src/tool-db/refresh.mjs)**:
|
|
44
|
+
`refresh({hostAgent})` の local → global → investigate lookup が同じ host の
|
|
45
|
+
global cache だけを見るようにした。
|
|
46
|
+
- **編集 [src/cli/db-cmd.mjs](src/cli/db-cmd.mjs) / [src/cli/doctor.mjs](src/cli/doctor.mjs)**:
|
|
47
|
+
`spotter db refresh/rebuild --host-agent codex` と `spotter doctor` の表示・消去対象を
|
|
48
|
+
host-global DB に追従。
|
|
49
|
+
- **編集 [test/tool-db.test.mjs](test/tool-db.test.mjs)**:
|
|
50
|
+
Claude global cache の同名 entry が Codex refresh に write-through されず、
|
|
51
|
+
Codex 側では Codex global cache / investigate を使う回帰テストを追加。
|
|
52
|
+
- **編集 README / README.ja / CLAUDE.md / docs**:
|
|
53
|
+
local だけでなく global description cache も Claude / Codex で分離する設計に更新。
|
|
54
|
+
|
|
55
|
+
### ユーザー側で必要な手順
|
|
56
|
+
|
|
57
|
+
1. `npm install -g claude-spotter@1.4.5`
|
|
58
|
+
2. 既存の shared global cache を掃除するため、各プロジェクトで
|
|
59
|
+
`spotter db rebuild` と `spotter db rebuild --host-agent codex` を 1 回ずつ実行
|
|
60
|
+
3. 各プロジェクトで `spotter install`
|
|
61
|
+
|
|
3
62
|
## 1.4.4
|
|
4
63
|
|
|
5
64
|
**Codex CLI auditor の default model を明示固定**。Spotter の hook 判定は高頻度・低遅延・低コストの
|
package/README.ja.md
CHANGED
|
@@ -105,9 +105,9 @@ flowchart LR
|
|
|
105
105
|
DB --> H[Haiku 監査<br/>session-scoped, preamble-once]
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
監査対象のツール (name + description) は host-local に分離されます。Claude は `<project>/.spotter/tool-db.json`、Codex は `<project>/.spotter/tool-db.codex.json` を使います。**daemon が監査に使うのは Claude local DB のみ**で、Codex native hooks は Codex local DB を読みます。グローバル
|
|
108
|
+
監査対象のツール (name + description) は host-local に分離されます。Claude は `<project>/.spotter/tool-db.json`、Codex は `<project>/.spotter/tool-db.codex.json` を使います。**daemon が監査に使うのは Claude local DB のみ**で、Codex native hooks は Codex local DB を読みます。グローバル description cache も host ごとに分離され、Claude は `~/.spotter/tool-db.json`、Codex は `~/.spotter/tool-db.codex.json` を使います。これらは同じ host の他プロジェクト間でだけ再利用され、監査入力には混ぜません。各 host-local DB は **その host の現時点の discovery 結果と一致** (refresh 時に prune される) するため、別プロジェクトや別 host のツールリストで上書きされることはありません。
|
|
109
109
|
|
|
110
|
-
**`spotter install` が Claude catalog の初回 seed を自動実行し、Claude Code セッション起動ごとに SessionStart hook が bg で `spotter db refresh` を走らせる**ため、Claude 通常運用で手動コマンドを叩く必要はありません。Codex CLI が使える環境では、同じ `spotter install` が Codex native hooks
|
|
110
|
+
**`spotter install` が Claude catalog の初回 seed を自動実行し、Claude Code セッション起動ごとに SessionStart hook が bg で `spotter db refresh` を走らせる**ため、Claude 通常運用で手動コマンドを叩く必要はありません。Codex CLI が使える環境では、同じ `spotter install` が Codex native hooks も登録し、`.spotter/tool-db.codex.json` も同期 seed します。これにより初回 Codex セッションから catalog を読めます。以降の Codex `SessionStart` hook は `spotter db refresh --host-agent codex` を bg 起動して `.spotter/tool-db.codex.json` を更新します。Claude catalog には書き込みません。Claude discovery は `claude mcp list` と Claude skills / sub-agents、Codex discovery は `codex mcp list/get` と Codex skills を読むため、両 host の利用可能ツール差分を別 DB として保持できます。各 MCP サーバーの `tools/list` は JSON-RPC で取得 (HTTP / SSE / stdio transport 対応)、スキルとサブエージェントは frontmatter から直接抽出、claude.ai baseline (OAuth proxy 経由の Gmail / Calendar / Drive 25 件) は Claude 側でのみ `claude mcp list` に該当サーバーが存在する環境で注入されます。**手書きでツールリストを管理する必要はありません**。
|
|
111
111
|
|
|
112
112
|
## Throughline との関係
|
|
113
113
|
|
|
@@ -132,7 +132,7 @@ spotter db refresh --host-agent codex
|
|
|
132
132
|
# Codex MCP / スキルから description を収集して .spotter/tool-db.codex.json を更新
|
|
133
133
|
# (Claude は install + Claude SessionStart、Codex は spotter install 後の
|
|
134
134
|
# Codex SessionStart で自動実行されるので通常は不要)
|
|
135
|
-
spotter db rebuild # Claude local + global DB を両方消してから refresh (カタログ設計変更時のクリーン用)
|
|
135
|
+
spotter db rebuild # Claude local + Claude global DB を両方消してから refresh (カタログ設計変更時のクリーン用)
|
|
136
136
|
spotter status # 稼働中の daemon 一覧
|
|
137
137
|
spotter doctor # 環境診断 (Node / claude CLI / Codex readiness / tool-db 整合性)
|
|
138
138
|
spotter diagnostics logs # daemon log から pass=false / backend latency / anomaly signal を集計
|
package/README.md
CHANGED
|
@@ -105,9 +105,9 @@ flowchart LR
|
|
|
105
105
|
DB --> H[Haiku audit<br/>session-scoped, preamble-once]
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
The audited catalog is host-local: Claude uses `<project>/.spotter/tool-db.json`, while Codex uses `<project>/.spotter/tool-db.codex.json`. **The daemon audits against the Claude local DB only**, and Codex native hooks read the Codex local DB.
|
|
108
|
+
The audited catalog is host-local: Claude uses `<project>/.spotter/tool-db.json`, while Codex uses `<project>/.spotter/tool-db.codex.json`. **The daemon audits against the Claude local DB only**, and Codex native hooks read the Codex local DB. Global description caches are host-specific too: Claude uses `~/.spotter/tool-db.json`, while Codex uses `~/.spotter/tool-db.codex.json`. They are shared only across projects for the same host and are never audit sources. Each host-local DB matches that host's **current** discovery snapshot for the project (stale entries are pruned on refresh), so tools from another project or another host cannot overwrite this session's audit catalog.
|
|
109
109
|
|
|
110
|
-
**`spotter install` seeds the Claude catalog automatically, and the SessionStart hook runs a background `spotter db refresh` on every Claude Code session start** — so you don't need to invoke Claude catalog commands by hand. When Codex CLI is available, the same `spotter install` registers Codex native hooks
|
|
110
|
+
**`spotter install` seeds the Claude catalog automatically, and the SessionStart hook runs a background `spotter db refresh` on every Claude Code session start** — so you don't need to invoke Claude catalog commands by hand. When Codex CLI is available, the same `spotter install` registers Codex native hooks and seeds `.spotter/tool-db.codex.json` synchronously, so the first Codex session has a catalog too. Later Codex `SessionStart` hooks start `spotter db refresh --host-agent codex` in the background, updating `.spotter/tool-db.codex.json` without touching the Claude catalog. Claude discovery reads `claude mcp list` plus Claude skills / sub-agents; Codex discovery reads `codex mcp list/get` plus Codex skills. Each MCP server's `tools/list` is fetched via JSON-RPC (HTTP / SSE / stdio transports supported); skill and sub-agent metadata comes straight from frontmatter; the claude.ai baseline (25 hand-curated entries for Gmail / Calendar / Drive over OAuth proxy) is injected only for Claude when `claude mcp list` confirms the server is present. **You never have to maintain the tool list by hand.**
|
|
111
111
|
|
|
112
112
|
## Spotter and Throughline
|
|
113
113
|
|
|
@@ -132,7 +132,7 @@ spotter db refresh --host-agent codex
|
|
|
132
132
|
# rediscover Codex MCP / skills and update .spotter/tool-db.codex.json
|
|
133
133
|
# (Claude refresh is automatic on install + Claude SessionStart;
|
|
134
134
|
# Codex refresh is automatic on Codex SessionStart after spotter install)
|
|
135
|
-
spotter db rebuild # wipe Claude local + global DBs and refresh from scratch
|
|
135
|
+
spotter db rebuild # wipe Claude local + Claude global DBs and refresh from scratch
|
|
136
136
|
# (use after catalog-shape changes)
|
|
137
137
|
spotter status # list running daemons
|
|
138
138
|
spotter doctor # environment check (Node / claude CLI / Codex readiness / tool-db integrity)
|
|
@@ -189,7 +189,7 @@ those values for smoke tests or controlled experiments.
|
|
|
189
189
|
<summary><strong>📋 Recent highlights</strong></summary>
|
|
190
190
|
|
|
191
191
|
- **Plugin-scoped MCP servers** — names like `plugin:everything-claude-code:context7` (with internal colons) are now parsed correctly and their tools enter the catalog. Earlier versions silently collapsed all plugin MCP servers into a single literal `"plugin"`, dropping their tools from Bell's audit
|
|
192
|
-
- **Per-project audit isolation** — the daemon audits against the local DB only;
|
|
192
|
+
- **Per-project / per-host audit isolation** — the daemon audits against the local DB only; global DBs are host-specific description caches. Tools discovered in *other* projects or another host can never bleed into this project's audit set
|
|
193
193
|
- **Zero-touch catalog** — `spotter install` seeds the Claude DB automatically; Claude and Codex SessionStart hooks keep their host-local DBs fresh in the background. You never have to maintain the tool list by hand
|
|
194
194
|
- **Codex native hooks** — Codex host uses Codex CLI as the primary auditor backend, keeps a separate `.spotter/tool-db.codex.json`, and surfaces backend failures explicitly instead of falling back to Haiku
|
|
195
195
|
- **Audit scope** — only user-added surface (MCP servers / skills / sub-agents). Claude Code's built-in tools are intentionally out of scope; Bell already uses those reliably
|
package/bin/spotter.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/src/cli/db-cmd.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// spotter db list — print the LOCAL tool-db (what the daemon actually audits)
|
|
4
4
|
// spotter db refresh — discover available tools and update DB (3-tier resolve)
|
|
5
|
-
// spotter db rebuild — wipe local + global DBs then refresh
|
|
5
|
+
// spotter db rebuild — wipe host-local + host-global DBs then refresh
|
|
6
6
|
//
|
|
7
7
|
// Run inside a project that has been `spotter install`-ed.
|
|
8
8
|
|
|
@@ -52,20 +52,19 @@ export async function runDbRefresh({ argv = [] } = {}) {
|
|
|
52
52
|
process.stdout.write(
|
|
53
53
|
`${resolved.size} tool(s) resolved (local=${counts.local}, global=${counts.global}, investigated=${counts.investigated})\n`
|
|
54
54
|
+ `local DB: ${localDbPath(projectRoot, opts.hostAgent)}\n`
|
|
55
|
-
+ `global DB: ${globalDbPath()}\n`
|
|
55
|
+
+ `global DB: ${globalDbPath(opts.hostAgent)}\n`
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
export async function runDbRebuild({ argv = [] } = {}) {
|
|
60
60
|
const projectRoot = requireProjectRoot();
|
|
61
61
|
const opts = parseDbArgs(argv);
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// touches names currently produced by investigation. Users need a clean slate.
|
|
62
|
+
// Wipe BOTH host-local and host-global DB. Rationale: catalog scope changes and
|
|
63
|
+
// description drift must not leak between Claude and Codex; each host cache is a
|
|
64
|
+
// separate clean-slate unit.
|
|
66
65
|
await saveDb(localDbPath(projectRoot, opts.hostAgent), emptyDb());
|
|
67
|
-
await saveDb(globalDbPath(), emptyDb());
|
|
68
|
-
process.stderr.write(`spotter db rebuild: cleared ${opts.hostAgent} local + global DB, refreshing...\n`);
|
|
66
|
+
await saveDb(globalDbPath(opts.hostAgent), emptyDb());
|
|
67
|
+
process.stderr.write(`spotter db rebuild: cleared ${opts.hostAgent} local + ${opts.hostAgent} global DB, refreshing...\n`);
|
|
69
68
|
await runDbRefresh({ argv });
|
|
70
69
|
}
|
|
71
70
|
|
package/src/cli/doctor.mjs
CHANGED
|
@@ -78,15 +78,18 @@ export async function runDoctor() {
|
|
|
78
78
|
if (!sidecar.ok) warnings += 1;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// tool-db (global
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
// tool-db (host-specific global caches). Since v1.2.0 these are not part of
|
|
82
|
+
// audit input; each host audits its project-local DB only. Empty caches are fine.
|
|
83
|
+
for (const hostAgent of ['claude', 'codex']) {
|
|
84
|
+
try {
|
|
85
|
+
const path = globalDbPath(hostAgent);
|
|
86
|
+
const global = await loadDb(path);
|
|
87
|
+
const count = Object.keys(global.tools).length;
|
|
88
|
+
mark(true, `${hostAgent} global cache DB: ${count} tools at ${path}`);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
mark(false, `${hostAgent} global cache DB`, `${err.message} (cache only; audit uses local DB)`);
|
|
91
|
+
warnings += 1;
|
|
92
|
+
}
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
// tool-db (local) if cwd is inside a Spotter project. Claude and Codex use
|
package/src/cli/install.mjs
CHANGED
|
@@ -120,9 +120,11 @@ export async function runInstall({
|
|
|
120
120
|
console.log(`wrote ${settingsPath}`);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
let codexHooksRegistered = false;
|
|
123
124
|
if (target === 'project' && !skipCodexHooks) {
|
|
124
125
|
if (codexCliPresentFn()) {
|
|
125
126
|
const result = await installCodexHooksFn();
|
|
127
|
+
codexHooksRegistered = true;
|
|
126
128
|
console.log(' Codex hooks registered');
|
|
127
129
|
console.log(` Codex hooks: ${result.hooksPath}`);
|
|
128
130
|
} else {
|
|
@@ -140,23 +142,33 @@ export async function runInstall({
|
|
|
140
142
|
console.log('\ndiscovering MCP servers, skills, and sub-agents...');
|
|
141
143
|
const log = (msg) => process.stderr.write(` ${msg}\n`);
|
|
142
144
|
try {
|
|
143
|
-
const
|
|
144
|
-
console.log(` ${
|
|
145
|
-
console.log(` local DB: ${localDbPath(cwd, 'claude')}`);
|
|
146
|
-
console.log(` global DB: ${globalDbPath()}`);
|
|
145
|
+
const claudeResolved = await refreshFn({ projectRoot: cwd, hostAgent: 'claude', logFn: log });
|
|
146
|
+
console.log(` ${claudeResolved.size} Claude tool(s) resolved`);
|
|
147
|
+
console.log(` Claude local DB: ${localDbPath(cwd, 'claude')}`);
|
|
148
|
+
console.log(` Claude global DB: ${globalDbPath('claude')}`);
|
|
149
|
+
if (codexHooksRegistered) {
|
|
150
|
+
const codexResolved = await refreshFn({ projectRoot: cwd, hostAgent: 'codex', logFn: log });
|
|
151
|
+
console.log(` ${codexResolved.size} Codex tool(s) resolved`);
|
|
152
|
+
console.log(` Codex local DB: ${localDbPath(cwd, 'codex')}`);
|
|
153
|
+
console.log(` Codex global DB: ${globalDbPath('codex')}`);
|
|
154
|
+
}
|
|
147
155
|
} catch (err) {
|
|
148
156
|
// §0: throw (fallback 禁止). But surface the recovery path so the user isn't
|
|
149
157
|
// left with "hooks registered, tool-db missing" and no clue what to run.
|
|
150
158
|
process.stderr.write(`\nspotter install: tool-db seeding failed.\n`);
|
|
151
159
|
process.stderr.write(` hooks are registered but tool-db is not ready.\n`);
|
|
152
|
-
process.stderr.write(` recover with: spotter db refresh\n`);
|
|
160
|
+
process.stderr.write(` recover with: spotter db refresh and, for Codex, spotter db refresh --host-agent codex\n`);
|
|
153
161
|
throw err;
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
console.log('\nnext steps:');
|
|
158
166
|
console.log(' reload Claude Code (or open a new session) to activate Spotter');
|
|
159
|
-
|
|
167
|
+
if (codexHooksRegistered) {
|
|
168
|
+
console.log(' open a new Codex session to activate Codex hooks');
|
|
169
|
+
} else if (target === 'project' && !skipCodexHooks) {
|
|
170
|
+
console.log(' Codex hooks are not active: rerun `spotter install` where `codex --version` succeeds');
|
|
171
|
+
}
|
|
160
172
|
}
|
|
161
173
|
|
|
162
174
|
async function exists(path) {
|
package/src/tool-db/loader.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Two layers:
|
|
4
4
|
// - local: <project>/.spotter/tool-db.json (Claude host)
|
|
5
5
|
// <project>/.spotter/tool-db.codex.json (Codex host)
|
|
6
|
-
// - global: ~/.spotter/tool-db.json
|
|
6
|
+
// - global: ~/.spotter/tool-db.json (Claude host)
|
|
7
|
+
// ~/.spotter/tool-db.codex.json (Codex host)
|
|
7
8
|
//
|
|
8
9
|
// Both have the same shape:
|
|
9
10
|
// { "version": 1, "tools": { "<name>": "<description>", ... } }
|
|
@@ -22,8 +23,10 @@ export class ToolDbSchemaError extends Error {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
export function globalDbPath() {
|
|
26
|
-
|
|
26
|
+
export function globalDbPath(hostAgent = 'claude') {
|
|
27
|
+
const host = normalizeToolDbHostAgent(hostAgent);
|
|
28
|
+
const file = host === 'claude' ? 'tool-db.json' : `tool-db.${host}.json`;
|
|
29
|
+
return join(homedir(), '.spotter', file);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export function localDbPath(projectRoot, hostAgent = 'claude') {
|
package/src/tool-db/refresh.mjs
CHANGED
|
@@ -101,14 +101,14 @@ export async function refresh({
|
|
|
101
101
|
return resolveAll({
|
|
102
102
|
toolNames,
|
|
103
103
|
localPath: localDbPath(projectRoot, toolDbHostAgent),
|
|
104
|
-
globalPath: globalDbPath(),
|
|
104
|
+
globalPath: globalDbPath(toolDbHostAgent),
|
|
105
105
|
investigate,
|
|
106
106
|
logFn,
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
// Read-only: load the LOCAL tool-db only — the daemon's audit must reflect what this
|
|
111
|
-
// specific project can actually use. The global DB is a
|
|
111
|
+
// specific project can actually use. The host-specific global DB is a cache written by
|
|
112
112
|
// `refresh` (so other projects can pick up descriptions cheaply) but is NEVER mixed
|
|
113
113
|
// into the daemon's audit catalog. Mixing global in caused phantom-tool suggestions
|
|
114
114
|
// from previously-visited projects bleeding into unrelated ones.
|