Skip to content

Commit 32aa264

Browse files
authored
Merge pull request #235 from ardelperal/feat/issue-234-release-tracker
docs(design): track project-scoped daemon release
2 parents 27f780c + 18e3b3e commit 32aa264

16 files changed

Lines changed: 1641 additions & 110 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1111

1212
### Changed
1313

14+
- Project-scoped CodeGraph resources can now be released non-interactively through the CLI or the opt-in MCP tool before deleting a worktree, without stopping unrelated projects. (#234)
1415
- VBA extractor comments now describe the behavior they guard, and the rule-table invariant also validates the procedure pre-walk so future drift fails loudly. (#215)
1516
- VBA classifiers now share one rule dispatcher with consistent scan modes, structural gates, counting, and declarative terminal behavior, preventing future rule tables from silently bypassing their documented preconditions. (#216)
1617
- VBA extraction now exposes only the classifier factories used by the shared source walker, removing obsolete compatibility entry points and other unused public helpers. (#217)

‎__tests__/daemon-release.test.ts‎

Lines changed: 649 additions & 0 deletions
Large diffs are not rendered by default.

‎__tests__/mcp-tool-annotations.test.ts‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ const ALL_TOOLS = tools.map((t) => t.name).join(',');
2727
function expectReadOnly(tool: ToolDefinition): void {
2828
expect(tool.annotations, `${tool.name} is missing annotations`).toBeDefined();
2929
// The hint Cursor Ask mode (and other clients) gate on.
30-
expect(tool.annotations!.readOnlyHint).toBe(!['codegraph_index', 'codegraph_sync'].includes(tool.name));
30+
expect(tool.annotations!.readOnlyHint).toBe(
31+
!['codegraph_index', 'codegraph_sync', 'codegraph_release'].includes(tool.name),
32+
);
3133
// The exact triplet the issue asks for, plus the honest closed-world hint.
3234
expect(tool.annotations!.destructiveHint).toBe(false);
3335
expect(tool.annotations!.idempotentHint).toBe(true);
@@ -36,6 +38,15 @@ function expectReadOnly(tool: ToolDefinition): void {
3638

3739
/** Assert each tool's exact read-only or mutating contract. */
3840
function expectToolAnnotations(tool: ToolDefinition): void {
41+
if (tool.name === 'codegraph_release') {
42+
expect(tool.annotations).toEqual({
43+
readOnlyHint: false,
44+
destructiveHint: true,
45+
idempotentHint: true,
46+
openWorldHint: false,
47+
});
48+
return;
49+
}
3950
if (tool.name === 'codegraph_uninit') {
4051
expect(tool.annotations).toEqual({
4152
readOnlyHint: false,
@@ -86,7 +97,7 @@ describe('Read-only annotations on the codegraph MCP tools (#1018)', () => {
8697
for (const tool of got) {
8798
expectToolAnnotations(tool);
8899
// Sanity: this IS the clone path (projectPath got marked required).
89-
if (tool.name !== 'codegraph_init') {
100+
if (!['codegraph_init', 'codegraph_release'].includes(tool.name)) {
90101
expect(tool.inputSchema.required ?? []).toContain('projectPath');
91102
}
92103
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Project-Scoped Daemon Release
2+
3+
## Decision
4+
5+
Provide one explicit release lifecycle per canonical project root across daemon control, watchdog supervision, MCP coordination, and the CLI.
6+
7+
## Scope
8+
9+
The chain adds one explicit project-scoped release operation across daemon lifecycle control, watchdog supervision, MCP proxy coordination, and the non-interactive CLI.
10+
11+
## Non-Negotiable Invariants
12+
13+
- **Generation-safe ownership**: release targets a canonical root and one authenticated daemon generation. It never signals an unverified PID.
14+
- **Startup exclusion**: startup and release arbitrate ownership before publishing or deleting daemon artifacts.
15+
- **Intentional release is stable**: watchdog supervision does not immediately respawn a deliberately released root. A later normal CodeGraph launch or query may establish a new lifecycle.
16+
- **Ordered requests**: a release waits for earlier requests and rejects later project calls on the owning proxy.
17+
- **Explicit operator access**: MCP release remains opt-in and destructive; CLI release requires an explicit path.
18+
- **Project isolation**: releasing one root leaves unrelated projects available. Process-wide cleanup is not part of this design.
19+
- **Idempotent outcomes**: repeated release returns a typed outcome: `released`, `not-running`, `no-daemon`, `identity-mismatch`, `unreachable`, or `termination-failed`.
20+
- **Resource release only**: release does not remove a worktree or project directory.
21+
- **Tests travel with behavior**: each child carries focused behavior-first tests and builds independently on Node 22.
22+
23+
## Chain Units
24+
25+
1. Lifecycle arbitration, authenticated generation-safe release, leases, heartbeat recovery, startup exclusion, and core tests.
26+
2. Watchdog release tombstones, canonical aliases, explicit resume, and focused tests.
27+
3. Proxy request barriers, per-root coordination, opt-in MCP surface, annotations, and focused tests.
28+
4. Non-interactive CLI command, daemon manager presentation, parser and integration tests, and final cross-surface coverage.
29+
30+
## Operator Surfaces
31+
32+
The non-interactive CLI surface is `codegraph daemon stop --path <project-root>`. The destructive MCP surface is `codegraph_release`, disabled by default and requiring an explicit `path`.
33+
34+
## Consequences
35+
36+
Each child targets its immediate parent. Reviewers can validate and roll back one behavior boundary at a time while the tracker remains a durable integration map.
37+
38+
## Contributor Checklist
39+
40+
- [ ] Keep each child diff limited to its declared unit.
41+
- [ ] Run the focused tests and build under Node 22.
42+
- [ ] Preserve authenticated ownership, startup exclusion, and request ordering.
43+
- [ ] Do not merge the tracker before all children are integrated.

‎site/src/content/docs/reference/cli.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ codegraph callers <symbol> # Find what calls a function/method (--limit,
2121
codegraph callees <symbol> # Find what a function/method calls (--limit, --json)
2222
codegraph impact <symbol> # Analyze what code is affected by changing a symbol (--depth, --json)
2323
codegraph affected [files...] # Find test files affected by changes (see below)
24-
codegraph daemon # Manage background daemons — pick one to stop (alias: daemons)
24+
codegraph daemon # Manage background daemons (alias: daemons)
25+
codegraph daemon stop --path <project-root> # Release one project's daemon resources
2526
codegraph telemetry [on|off] # Show or change anonymous usage telemetry
2627
codegraph upgrade [version] # Update to the latest release (--check, --force)
2728
codegraph version # Print the installed version (also -v, --version)
@@ -49,3 +50,13 @@ codegraph impact AuthMiddleware --depth 3
4950
## affected
5051

5152
Traces import dependencies transitively to find which test files are affected by changed source files. See [Affected Tests in CI](/codegraph/guides/affected-tests/) for options and a CI example.
53+
54+
## daemon stop
55+
56+
Release CodeGraph resources for one project before deleting its worktree:
57+
58+
```bash
59+
codegraph daemon stop --path <project-root>
60+
```
61+
62+
The command is non-interactive and releases only the canonical project root selected by `--path`. It does not remove the worktree or affect CodeGraph daemons for unrelated projects.

‎site/src/content/docs/reference/mcp-server.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ CODEGRAPH_MCP_TOOLS=explore,node,search,callers
3939

4040
Each also has a CLI equivalent (`codegraph node` / `query` / `callers` / `callees` / `impact` / `files` / `status`) for scripts and non-MCP harnesses.
4141

42+
## Project release
43+
44+
`codegraph_release` is a destructive lifecycle tool and is disabled by default. Enable it explicitly:
45+
46+
```bash
47+
CODEGRAPH_MCP_TOOLS=explore,release
48+
```
49+
50+
Call it with the selected worktree or project root:
51+
52+
```json
53+
{ "path": "/absolute/path/to/project-root" }
54+
```
55+
56+
Use it immediately before deleting that worktree. It releases CodeGraph resources for the canonicalized root but does not remove the worktree. Never kill all Node or CodeGraph processes as cleanup: unrelated projects may still be active.
57+
4258
## How agents should use it
4359

4460
CodeGraph *is* the pre-built search index. For "how does X work?", architecture, a flow ("how does X reach Y"), or where-is-X questions — and while editing code — an agent should answer with `codegraph_explore` and stop, typically with **zero file reads**, rather than re-deriving the answer with `grep` + `Read`. A direct CodeGraph answer is one to a few calls; a grep/read exploration is dozens.

‎src/bin/codegraph.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import '../mcp/early-ppid';
3030

3131
import { Command } from 'commander';
32+
import { registerDaemonStopCommand } from './daemon-release';
3233
import * as path from 'path';
3334
import * as fs from 'fs';
3435
import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, hasStructuralKeyword, extractCodeTokens } from '../directory';
@@ -1640,7 +1641,7 @@ function printFileTree(
16401641
* to pick one (the current project's daemon floats to the top, auto-selected),
16411642
* enter to stop it. Falls back to a plain list when output isn't a TTY.
16421643
*/
1643-
program
1644+
const daemonCommand = program
16441645
.command('daemon')
16451646
.aliases(['daemons'])
16461647
.description('Manage running CodeGraph background daemons — pick one and press enter to stop it')
@@ -1683,6 +1684,8 @@ program
16831684
});
16841685
});
16851686

1687+
registerDaemonStopCommand(daemonCommand);
1688+
16861689
/**
16871690
* codegraph serve
16881691
*/

‎src/bin/daemon-release.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Command } from 'commander';
2+
import { releaseDaemonAt, type StopResult } from '../mcp/daemon-registry';
3+
4+
/** Testable command handler behind `daemon stop --path`. */
5+
export async function runDaemonStop(
6+
root: string,
7+
release: (path: string) => Promise<StopResult> = releaseDaemonAt,
8+
): Promise<string> {
9+
return JSON.stringify(await release(root));
10+
}
11+
12+
/** Register the non-interactive parser path without importing the full CLI entrypoint. */
13+
export function registerDaemonStopCommand(
14+
parent: Command,
15+
run: (path: string) => Promise<string> = runDaemonStop,
16+
output: (value: string) => void = console.log,
17+
): Command {
18+
return parent
19+
.command('stop')
20+
.description('Release the CodeGraph daemon and index locks for one explicit project root')
21+
.requiredOption('-p, --path <path>', 'Canonical project root to release')
22+
.action(async (options: { path: string }) => output(await run(options.path)));
23+
}

‎src/mcp/daemon-manager.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,18 @@ export async function runDaemonPicker(deps: PickerDeps): Promise<void> {
102102

103103
if (choice === STOP_ALL) {
104104
const results = await deps.stopAll();
105-
const n = results.filter((r) => r.outcome === 'term' || r.outcome === 'kill').length;
105+
const n = results.filter((r) => r.outcome === 'released' || r.outcome === 'not-running').length;
106106
deps.note(`Stopped ${n} daemon${n === 1 ? '' : 's'}.`);
107107
deps.done('Done.');
108108
return;
109109
}
110110

111111
const result = await deps.stop(String(choice));
112-
const forced = result.outcome === 'kill' ? ', forced' : '';
113-
deps.note(`Stopped daemon (pid ${result.pid}${forced}) — ${choice}`);
112+
if (result.outcome === 'released' || result.outcome === 'not-running') {
113+
deps.note(`Stopped daemon (pid ${result.pid}) — ${choice}`);
114+
} else {
115+
deps.note(`Daemon not stopped (${result.outcome}) — ${choice}`);
116+
}
114117
// Loop: the next iteration re-lists; if more remain it re-prompts, otherwise
115118
// the top-of-loop empty check prints "All daemons stopped."
116119
}

‎src/mcp/daemon-paths.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ export function getDaemonPidPath(projectRoot: string): string {
9393
return path.join(getCodeGraphDir(projectRoot), 'daemon.pid');
9494
}
9595

96+
/** Root-scoped lease held while intentional release owns lifecycle cleanup. */
97+
export function getDaemonReleaseLeasePath(projectRoot: string): string {
98+
return path.join(getCodeGraphDir(projectRoot), 'daemon.release');
99+
}
100+
101+
/** Exclusive marker for release-lease recovery/heartbeat publication. */
102+
export function getDaemonReleaseRecoveryPath(projectRoot: string): string {
103+
return path.join(getCodeGraphDir(projectRoot), 'daemon.release.recovery');
104+
}
105+
106+
/** Short root-scoped arbitration lock shared by startup and release publication. */
107+
export function getDaemonLifecyclePath(projectRoot: string): string {
108+
return path.join(getCodeGraphDir(projectRoot), 'daemon.lifecycle');
109+
}
110+
96111
/** Structured contents of the pid lockfile. */
97112
export interface DaemonLockInfo {
98113
pid: number;

0 commit comments

Comments
 (0)