Skip to content

Commit eb9c2d3

Browse files
authored
Merge pull request #182 from clay-good/fix/e2e-hardening-post-v2.1.2
fix: e2e hardening for v2.1.3 (first-run gitignore, no-throw contracts, panic concurrency, large-output + federation conformance)
2 parents 49ec719 + 9a2d8fa commit eb9c2d3

48 files changed

Lines changed: 1079 additions & 409 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Changelog
2+
3+
All notable changes to OpenLore are documented here. This project adheres to
4+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5+
6+
## [2.1.3] - 2026-06-22
7+
8+
Everything merged since v2.1.2: a batch of new agent-facing capabilities plus a
9+
deep end-to-end hardening and dogfooding pass. The version is read from
10+
`package.json`, so the CLI and the MCP server both report `2.1.3`.
11+
12+
### Added
13+
14+
- **Agent behavioral governance ("panic")** — opt-in, off by default (#175). A
15+
PreToolUse destabilization guard (`openlore panic-check`), an observe→memory
16+
feedback loop that feeds behavioral hotspots into `orient`, an optional Gryph
17+
runtime observer, and an accuracy-validation harness
18+
(`panic-validate` / `panic-calibrate` / `panic-replay`). Enable per project with
19+
`openlore setup --panic <mode>` and install the hooks with
20+
`openlore setup --hooks <format>` (remove them with `--hooks none`).
21+
- **External spec-store binding** — the `spec_store_status` MCP tool (federation
22+
preset) reports the read-only health of a `.openlore/config.json` `specStore`
23+
binding and its indexed targets (#178).
24+
- **Working-set context briefing** — the `working_set_context` MCP tool assembles
25+
one token-budgeted, per-target structural briefing for an active change across
26+
its spec-store targets (#180).
27+
- **Change-impact certificate** — the `change_impact_certificate` MCP tool and the
28+
`openlore impact-certificate` CLI certify what a diff touches: the paths it
29+
newly opens into declared covering surfaces (differential, no LLM), blast
30+
radius, drifted specs, and the tests to run (#181).
31+
- **Live dependency graph in watch mode**`watch` now reconciles file creates &
32+
deletes and keeps `dependency-graph.json` import edges (including inline
33+
`<script>` and HTML asset edges) fresh incrementally (#173).
34+
- **Pi extension** — marketplace gallery preview image (#174); Windows daemon
35+
hardening so no console window flashes (#177).
36+
37+
### Changed
38+
39+
- Removed the `get_decisions` MCP tool. ADRs are now surfaced through
40+
`search_specs` (domain `decisions`) and via `orient`'s ADR matches, which now
41+
work without an embedding server (#179).
42+
- `.mjs` / `.cjs` / `.mts` / `.cts` files are now recognized as JavaScript /
43+
TypeScript and included in the call graph and signature index (previously
44+
silently dropped).
45+
- Panic-state: the on-disk file is the single source of truth for the
46+
cross-process intervention counter; all writers (MCP server, hook, daemon)
47+
serialize through one lock.
48+
- Documentation: Windows setup steps in CONTRIBUTING (#176); corrected and guarded
49+
MCP tool-count references.
50+
51+
### Fixed
52+
53+
End-to-end hardening pass (PR #182), all with regression tests:
54+
55+
- **First run**`openlore init` and `openlore run` now create `.gitignore` on a
56+
fresh `git init` repo, so `.openlore/` analysis artifacts (multi-MB lance
57+
binaries) aren't accidentally committed and don't pollute diff-based tools.
58+
- **MCP no-throw / robustness**`get_spec` confines its `domain` argument
59+
(path-traversal fix); `get_file_dependencies` guards a partial dependency-graph
60+
artifact; `change_impact_certificate` drops non-object surface members and
61+
`buildLeaseAnchors` never escapes the handler; a malformed `callGraph` is
62+
normalized instead of crashing graph handlers; large tool results stay valid
63+
JSON when capped to the byte budget.
64+
- **LLM generation** — all providers tolerate malformed or `usage`-less responses
65+
(common with OpenAI-compatible gateways) instead of crashing or reporting `$NaN`
66+
cost.
67+
- **Panic** — fixed a cross-process lost-update on the intervention counter;
68+
untrusted `panic-state.json` fields are sanitized and a NaN timestamp is treated
69+
as expired; panic hooks gained an uninstall path and update in place on a format
70+
change.
71+
- **Multi-repo federation** — a registered repo that throws mid-query is skipped
72+
with a reason instead of aborting the whole fleet query; tool output no longer
73+
leaks absolute host paths.
74+
- **CLI**`verify --json` and `decisions --sync` now exit non-zero on failure
75+
(they previously reported failure but exited 0, defeating CI gates); `decisions`
76+
has a top-level error boundary; `openlore view` reports a friendly message on a
77+
port-in-use, sanitizes errors before logging, and serves a 404 (not 500) for a
78+
missing graph artifact.
79+
80+
**Full Changelog**: https://github.com/clay-good/OpenLore/compare/v2.1.2...v2.1.3

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<br>
1515
<img src="https://img.shields.io/badge/MCP-ready-7c3aed?logo=anthropic&logoColor=white" alt="MCP ready">
1616
<img src="https://img.shields.io/badge/languages-18-f97316" alt="18 languages">
17-
<img src="https://img.shields.io/badge/tests-3900%2B-success" alt="3900+ tests">
17+
<img src="https://img.shields.io/badge/tests-4400%2B-success" alt="4400+ tests">
1818
<img src="https://img.shields.io/badge/API_key-not_required-0ea5e9" alt="No API key required">
1919
<a href="https://github.com/clay-good/OpenLore/stargazers"><img src="https://img.shields.io/github/stars/clay-good/OpenLore?style=social" alt="GitHub stars"></a>
2020
</p>
@@ -312,7 +312,7 @@ If you only install one thing, install `openlore-orient`. The workflow skills ar
312312

313313
Continuously maintains a structural representation of your codebase using pure static analysis. Builds a full call graph persisted to SQLite, runs label-propagation community detection to cluster tightly coupled functions, computes McCabe cyclomatic complexity for every function, and extracts DB schemas, HTTP routes, UI components, middleware chains, and environment variables. Outputs `.openlore/analysis/CODEBASE.md` — a ~600-token structural digest that compresses the equivalent of tens of thousands of exploratory tokens into a small, queryable summary.
314314

315-
With `--watch-auto`, the call graph updates incrementally on every file save: changed file and its direct callers are re-parsed and the graph is atomically swapped. Orient and BFS queries remain live between full analyze runs.
315+
With `--watch-auto`, the call graph updates incrementally on every file save: the changed file and its direct callers are re-parsed and the graph is atomically swapped. File creates and deletes are reconciled too, and `dependency-graph.json` import edges (including HTML asset edges) stay live. Orient and BFS queries remain live between full analyze runs.
316316

317317
**Generate** (API key required)
318318

@@ -525,6 +525,8 @@ openlore federation list # ✓ indexed / ⚠
525525

526526
Once peers are registered, `analyze_impact`, `find_dead_code`, `select_tests`, and `find_path` take an opt-in `federation` flag and answer across the fleet — who consumes a published symbol, whether an export is dead *everywhere*, which consumer tests a change touches — always naming the repos consulted vs skipped, never guessing for an unindexed one. The capability is opt-in: `openlore mcp --preset federation`. See [docs/cli-reference.md](docs/cli-reference.md#federation-multi-repo).
527527

528+
The same `federation` preset also exposes a spec-store arc — binding OpenLore to an external spec store and reasoning about a change against it: `spec_store_status` (read-only health of a `.openlore/config.json` `specStore` binding and its indexed targets), `working_set_context` (one token-budgeted, per-target structural briefing for an active change across its targets), and `change_impact_certificate` (the paths a diff *newly opens* into declared covering surfaces, plus blast radius, drifted specs, and tests to run — also as `openlore impact-certificate`). All read-only, conclusion-shaped, and advisory.
529+
528530
---
529531

530532
## Documentation

docs/agent-setup.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Wire the generated digest into your agent's context:
100100

101101
**Claude Code — MCP config (token-efficient two-server setup)**
102102

103-
MCP clients load all tool schemas at session start. With 61 tools, this costs ~15k tokens of `tools/list` before any work begins (Spec 28 measured it; the lossless server-side trim is only ~2%, so the real lever is deferral). Claude Code supports `alwaysLoad: false` (deferred, default) — tools load only when the agent searches for them via Tool Search.
103+
MCP clients load all tool schemas at session start. With 62 tools, this costs ~16k tokens of `tools/list` before any work begins (Spec 28 measured it; the lossless server-side trim is only ~2%, so the real lever is deferral). Claude Code supports `alwaysLoad: false` (deferred, default) — tools load only when the agent searches for them via Tool Search.
104104

105105
The recommended setup uses two server entries: one always-visible core server and one deferred full server:
106106

@@ -124,7 +124,7 @@ The recommended setup uses two server entries: one always-visible core server an
124124
```
125125

126126
- **`openlore-core`** exposes 6 tools always visible in context (~600 tokens): `orient`, `search_code`, `record_decision`, `detect_changes`, `check_spec_drift`, `get_health_map`. These are the tools most likely to be called at session start.
127-
- **`openlore`** exposes all 61 tools deferred — loaded on demand when the agent uses Tool Search (e.g. "find tool for BFS graph traversal").
127+
- **`openlore`** exposes all 62 tools deferred — loaded on demand when the agent uses Tool Search (e.g. "find tool for BFS graph traversal").
128128

129129
If you only need one server entry, use `alwaysLoad: false` (the default) with the standard `openlore mcp` command — all tools are deferred and searchable via Tool Search.
130130

docs/mcp-tools.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,6 @@ Most tools run on **pure static analysis** — no LLM quota consumed. Exceptions
184184
|------|-------------|:---:|
185185
| `get_spec` | Read the full content of an OpenSpec domain spec by domain name. | Yes (generate) |
186186
| `get_mapping` | Requirement->function mapping produced by `openlore generate`. Shows which functions implement which spec requirements, confidence level, and orphan functions with no spec coverage. | Yes (generate) |
187-
| `get_decisions` | List or search Architecture Decision Records (ADRs) stored in `openspec/decisions/`. Optional keyword query. | Yes (generate) |
188187
| `check_spec_drift` | Detect code changes not reflected in OpenSpec specs. Compares git-changed files against spec coverage maps. Issues: gap / stale / uncovered / orphaned-spec / adr-gap. | Yes (generate) |
189188
| `search_specs` | Semantic search over OpenSpec specifications to find requirements, design notes, and architecture decisions by meaning. Also searches ADR files (`openspec/decisions/adr-*.md`) indexed under domain `decisions`. Returns linked source files for graph highlighting. Use this when asked "which spec covers X?" or "where should we implement Z?" or "what decisions were made about Y?". Requires a spec index built with `openlore analyze` or `--reindex-specs`. | Yes (generate) |
190189
| `list_spec_domains` | List all OpenSpec domains available in this project. Use this to discover what domains exist before doing a targeted `search_specs` call. | Yes (generate) |
@@ -426,12 +425,6 @@ maxDepth number Maximum path length in hops (default: 6)
426425
maxPaths number Maximum number of paths to return (default: 10, max: 50)
427426
```
428427

429-
**`get_decisions`**
430-
```
431-
directory string Absolute path to the project directory
432-
query string Optional keyword to filter ADRs by title or content
433-
```
434-
435428
**`get_spec`**
436429
```
437430
directory string Absolute path to the project directory

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openlore",
3-
"version": "2.1.2",
3+
"version": "2.1.3",
44
"description": "Persistent architectural memory and structural cognition for AI coding agents.",
55
"type": "module",
66
"main": "dist/api/index.js",

src/api/init.test.ts

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ vi.mock('../core/services/gitignore-manager.js', () => ({
2727
gitignoreExists: vi.fn(),
2828
isInGitignore: vi.fn(),
2929
addToGitignore: vi.fn(),
30+
ensureGitignored: vi.fn(),
3031
}));
3132

3233
import {
@@ -44,6 +45,7 @@ import {
4445
gitignoreExists,
4546
isInGitignore,
4647
addToGitignore,
48+
ensureGitignored,
4749
} from '../core/services/gitignore-manager.js';
4850

4951
const mockDetectProjectType = vi.mocked(detectProjectType);
@@ -56,6 +58,7 @@ const mockCreateOpenSpecStructure = vi.mocked(createOpenSpecStructure);
5658
const mockGitignoreExists = vi.mocked(gitignoreExists);
5759
const mockIsInGitignore = vi.mocked(isInGitignore);
5860
const mockAddToGitignore = vi.mocked(addToGitignore);
61+
const mockEnsureGitignored = vi.mocked(ensureGitignored);
5962

6063
// ============================================================================
6164
// SETUP
@@ -76,6 +79,7 @@ beforeEach(() => {
7679
mockGitignoreExists.mockResolvedValue(false);
7780
mockIsInGitignore.mockResolvedValue(false);
7881
mockAddToGitignore.mockResolvedValue(true);
82+
mockEnsureGitignored.mockResolvedValue('created');
7983
});
8084

8185
// ============================================================================
@@ -94,30 +98,12 @@ describe('openloreInit', () => {
9498
expect(mockCreateOpenSpecStructure).toHaveBeenCalledOnce();
9599
});
96100

97-
it('adds .openlore/ to .gitignore when gitignore exists', async () => {
98-
mockGitignoreExists.mockResolvedValue(true);
99-
mockIsInGitignore.mockResolvedValue(false);
100-
101-
await openloreInit({ rootPath: ROOT });
102-
103-
expect(mockAddToGitignore).toHaveBeenCalledWith(ROOT, '.openlore/', expect.any(String));
104-
});
105-
106-
it('skips addToGitignore when .openlore/ already in gitignore', async () => {
107-
mockGitignoreExists.mockResolvedValue(true);
108-
mockIsInGitignore.mockResolvedValue(true);
109-
110-
await openloreInit({ rootPath: ROOT });
111-
112-
expect(mockAddToGitignore).not.toHaveBeenCalled();
113-
});
114-
115-
it('skips addToGitignore when no .gitignore file', async () => {
116-
mockGitignoreExists.mockResolvedValue(false);
117-
101+
it('delegates .openlore/ gitignore handling to ensureGitignored (which creates the file when absent)', async () => {
118102
await openloreInit({ rootPath: ROOT });
119103

120-
expect(mockAddToGitignore).not.toHaveBeenCalled();
104+
// The create-or-append/skip decision lives in ensureGitignored (covered by its
105+
// own tests); init just delegates so a fresh `git init` repo always gets ignored.
106+
expect(mockEnsureGitignored).toHaveBeenCalledWith(ROOT, '.openlore/', expect.any(String));
121107
});
122108

123109
it('skips createOpenSpecStructure when openspec dir already exists', async () => {

src/api/init.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@ import {
1919
createOpenSpecStructure,
2020
detectExistingSpecDir,
2121
} from '../core/services/config-manager.js';
22-
import {
23-
gitignoreExists,
24-
isInGitignore,
25-
addToGitignore,
26-
} from '../core/services/gitignore-manager.js';
22+
import { ensureGitignored } from '../core/services/gitignore-manager.js';
2723
import type { InitApiOptions, InitResult, ProgressCallback } from './types.js';
2824

2925
function progress(onProgress: ProgressCallback | undefined, step: string, status: 'start' | 'progress' | 'complete' | 'skip', detail?: string): void {
@@ -92,16 +88,11 @@ export async function openloreInit(options: InitApiOptions = {}): Promise<InitRe
9288
progress(onProgress, 'OpenSpec directory exists', 'skip');
9389
}
9490

95-
// Update .gitignore
96-
const hasGitignore = await gitignoreExists(rootPath);
97-
if (hasGitignore) {
98-
const alreadyIgnored = await isInGitignore(rootPath, `${OPENLORE_DIR}/`);
99-
if (!alreadyIgnored) {
100-
progress(onProgress, 'Updating .gitignore', 'start');
101-
await addToGitignore(rootPath, `${OPENLORE_DIR}/`, 'openlore analysis artifacts');
102-
progress(onProgress, 'Updating .gitignore', 'complete');
103-
}
104-
}
91+
// Ensure .openlore/ analysis artifacts (multi-MB lance binaries) are ignored,
92+
// creating .gitignore when absent so a fresh `git init` repo doesn't leak them.
93+
progress(onProgress, 'Updating .gitignore', 'start');
94+
const gitignoreResult = await ensureGitignored(rootPath, `${OPENLORE_DIR}/`, 'openlore analysis artifacts');
95+
progress(onProgress, 'Updating .gitignore', gitignoreResult === 'present' ? 'skip' : 'complete');
10596

10697
return {
10798
configPath: OPENLORE_CONFIG_REL_PATH,

src/api/run.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ vi.mock('../core/services/gitignore-manager.js', () => ({
3939
gitignoreExists: vi.fn(),
4040
isInGitignore: vi.fn(),
4141
addToGitignore: vi.fn(),
42+
ensureGitignored: vi.fn(),
4243
}));
4344

4445
vi.mock('../core/services/llm-service.js', () => ({
@@ -110,7 +111,7 @@ import { access, stat, readFile } from 'node:fs/promises';
110111
import { isCacheFresh } from '../core/services/mcp-handlers/utils.js';
111112
import { detectProjectType, getProjectTypeName } from '../core/services/project-detector.js';
112113
import { getDefaultConfig, readOpenLoreConfig, writeOpenLoreConfig, openloreConfigExists, openspecDirExists, createOpenSpecStructure } from '../core/services/config-manager.js';
113-
import { gitignoreExists, isInGitignore, addToGitignore } from '../core/services/gitignore-manager.js';
114+
import { gitignoreExists, isInGitignore, addToGitignore, ensureGitignored } from '../core/services/gitignore-manager.js';
114115
import { createLLMService } from '../core/services/llm-service.js';
115116
import { RepositoryMapper } from '../core/analyzer/repository-mapper.js';
116117
import { DependencyGraphBuilder } from '../core/analyzer/dependency-graph.js';
@@ -133,6 +134,7 @@ const mockCreateOpenSpecStructure = vi.mocked(createOpenSpecStructure);
133134
const mockGitignoreExists = vi.mocked(gitignoreExists);
134135
const mockIsInGitignore = vi.mocked(isInGitignore);
135136
const mockAddToGitignore = vi.mocked(addToGitignore);
137+
const mockEnsureGitignored = vi.mocked(ensureGitignored);
136138
const mockCreateLLMService = vi.mocked(createLLMService);
137139
const mockIsCacheFresh = vi.mocked(isCacheFresh);
138140

@@ -188,6 +190,7 @@ function setupMocks({ configExists = false, analysisRecent = false } = {}) {
188190
mockGitignoreExists.mockResolvedValue(false);
189191
mockIsInGitignore.mockResolvedValue(false);
190192
mockAddToGitignore.mockResolvedValue(true);
193+
mockEnsureGitignored.mockResolvedValue('created');
191194

192195
// Analysis mocks
193196
const mtime = analysisRecent ? RECENT_MTIME : OLD_MTIME;
@@ -254,6 +257,13 @@ describe('openloreRun', () => {
254257
expect(mockWriteOpenLoreConfig).toHaveBeenCalled();
255258
});
256259

260+
it('delegates .openlore/ gitignore handling to ensureGitignored when creating config', async () => {
261+
setupMocks({ configExists: false, analysisRecent: true });
262+
await openloreRun({ rootPath: ROOT });
263+
264+
expect(mockEnsureGitignored).toHaveBeenCalledWith(ROOT, '.openlore/', expect.any(String));
265+
});
266+
257267
it('skips init when config exists and force=false', async () => {
258268
setupMocks({ configExists: true, analysisRecent: true });
259269
const result = await openloreRun({ rootPath: ROOT });

src/api/run.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@ import {
2323
openspecDirExists,
2424
createOpenSpecStructure,
2525
} from '../core/services/config-manager.js';
26-
import {
27-
gitignoreExists,
28-
isInGitignore,
29-
addToGitignore,
30-
} from '../core/services/gitignore-manager.js';
26+
import { ensureGitignored } from '../core/services/gitignore-manager.js';
3127
import { createLLMService } from '../core/services/llm-service.js';
3228
import type { LLMService } from '../core/services/llm-service.js';
3329
import { RepositoryMapper } from '../core/analyzer/repository-mapper.js';
@@ -112,13 +108,10 @@ export async function openloreRun(options: RunApiOptions = {}): Promise<RunResul
112108
await createOpenSpecStructure(fullOpenspecPath);
113109
}
114110

115-
const hasGitignore = await gitignoreExists(rootPath);
116-
if (hasGitignore) {
117-
const alreadyIgnored = await isInGitignore(rootPath, `${OPENLORE_DIR}/`);
118-
if (!alreadyIgnored) {
119-
await addToGitignore(rootPath, `${OPENLORE_DIR}/`, 'openlore analysis artifacts');
120-
}
121-
}
111+
// Ensure .openlore/ analysis artifacts (multi-MB lance binaries) are ignored,
112+
// creating .gitignore when absent so a fresh `git init` repo doesn't leak them
113+
// into git status and diff-based tools.
114+
await ensureGitignored(rootPath, `${OPENLORE_DIR}/`, 'openlore analysis artifacts');
122115

123116
initResult = {
124117
configPath: OPENLORE_CONFIG_REL_PATH,

0 commit comments

Comments
 (0)