peaks-loop 4.0.20 → 4.0.21
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 +51 -0
- package/README-en.md +32 -0
- package/README.md +31 -0
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/codegraph-commands.js +156 -13
- package/dist/cli/commands/dispatch-commands.d.ts +23 -0
- package/dist/cli/commands/dispatch-commands.js +127 -3
- package/dist/cli/commands/sub-agent/detached.js +17 -1
- package/dist/cli/commands/sub-agent-shared.d.ts +39 -0
- package/dist/cli/commands/vendor-detect.d.ts +15 -0
- package/dist/cli/commands/vendor-detect.js +22 -5
- package/dist/cli/commands/workspace/init-command.js +34 -0
- package/dist/services/codegraph/codegraph-service.d.ts +159 -0
- package/dist/services/codegraph/codegraph-service.js +217 -3
- package/dist/services/doctor/doctor-service/checks/codegraph-capability.d.ts +7 -0
- package/dist/services/doctor/doctor-service/checks/codegraph-capability.js +139 -13
- package/dist/services/doctor/doctor-service/types.d.ts +25 -0
- package/package.json +3 -3
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* Reports which vendor CLIs are installed on PATH + recommends default.
|
|
4
|
-
* Spec: docs/superpowers/specs/2026-08-10-peaks-detached-sub-agent-design.md §3.3
|
|
5
|
-
*/
|
|
1
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
2
|
+
import { ok } from 'peaks-loop-shared/result';
|
|
6
3
|
import { defaultRegistry } from 'peaks-loop-internal-runtime';
|
|
7
4
|
export async function vendorDetect(opts) {
|
|
8
5
|
const reg = defaultRegistry();
|
|
@@ -14,3 +11,23 @@ export async function vendorDetect(opts) {
|
|
|
14
11
|
const recommended = installed[0] ?? null;
|
|
15
12
|
return { ok: true, command: 'vendor-detect', data: { installed, recommended } };
|
|
16
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* `peaks vendor-detect` CLI registration (rid-001 redo).
|
|
16
|
+
* Slice 2026-08-11 detached-sub-agent-design §3.3 + §5.3 — register the
|
|
17
|
+
* previously-dead-coded `vendorDetect()` handler at the top-level program
|
|
18
|
+
* surface so `peaks vendor-detect --json` actually reaches it (was
|
|
19
|
+
* dead-coded at the CLI seam even though the handler existed).
|
|
20
|
+
*/
|
|
21
|
+
export function registerVendorDetectCommand(program, io) {
|
|
22
|
+
addJsonOption(program
|
|
23
|
+
.command('vendor-detect')
|
|
24
|
+
.description('Detect which vendor CLIs (claude / codex / copilot) are installed ' +
|
|
25
|
+
'on PATH and recommend a default for --mode detached dispatch. ' +
|
|
26
|
+
'Returns `{ installed: string[], recommended: <id|null> }`.')).action(async (options) => {
|
|
27
|
+
const result = await vendorDetect({ json: options.json === true });
|
|
28
|
+
printResult(io, ok('vendor-detect', result.data, [], [
|
|
29
|
+
'Pass the recommended vendor id to `peaks sub-agent dispatch --mode detached --vendor <id>`.',
|
|
30
|
+
'Re-run with --json to machine-parse.'
|
|
31
|
+
]), options.json === true);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -26,6 +26,8 @@ import { resolveCanonicalProjectRoot } from '../../../services/config/config-ser
|
|
|
26
26
|
import { applyHookInstall, readHookStatus } from '../../../services/skills/hooks-settings-service.js';
|
|
27
27
|
import { clearStalePresenceOnRotation } from '../../../services/skills/skill-presence-service.js';
|
|
28
28
|
import { gcStalePresenceLeases } from '../../../services/skills/presence-lease-service.js';
|
|
29
|
+
import { defaultCodegraphInitGuard, writeCodegraphMarker } from '../../../services/codegraph/codegraph-service.js';
|
|
30
|
+
import { mkdirSync } from 'node:fs';
|
|
29
31
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
30
32
|
import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
|
|
31
33
|
import { hasStandardsCheckedMarker, markStandardsChecked } from '../../../services/standards/missing-standards-detector.js';
|
|
@@ -337,6 +339,36 @@ export function registerWorkspaceInitCommand(workspace, io) {
|
|
|
337
339
|
if (report.standardsMissing.missing) {
|
|
338
340
|
markStandardsChecked(projectRoot, sessionId);
|
|
339
341
|
}
|
|
342
|
+
// rid-CG-001: auto-stake a peaks-loop-managed `.codegraph/`
|
|
343
|
+
// directory after a successful workspace init. Calls the
|
|
344
|
+
// conflict guard (rid-CG-006) BEFORE writing so a foreign
|
|
345
|
+
// schema is never silently overwritten. We deliberately do
|
|
346
|
+
// NOT spawn the upstream codegraph binary here — `peaks
|
|
347
|
+
// workspace init` must stay fast and offline-safe; the user
|
|
348
|
+
// runs `peaks codegraph index` next if they want the actual
|
|
349
|
+
// SQLite-backed index.
|
|
350
|
+
let codegraphAutoOutcome = null;
|
|
351
|
+
try {
|
|
352
|
+
const guard = defaultCodegraphInitGuard(projectRoot);
|
|
353
|
+
if (guard.status === 'fresh') {
|
|
354
|
+
mkdirSync(guard.codegraphDir, { recursive: true });
|
|
355
|
+
writeCodegraphMarker(guard.codegraphDir);
|
|
356
|
+
codegraphAutoOutcome = { status: 'fresh' };
|
|
357
|
+
nextActions.push(`Auto-created empty ${guard.codegraphDir}/ and stamped the peaks-loop marker. ` +
|
|
358
|
+
'Run `peaks codegraph index --project <path>` next to build the SQLite-backed index.');
|
|
359
|
+
}
|
|
360
|
+
else if (guard.status === 'noop-already-peaks-loop') {
|
|
361
|
+
codegraphAutoOutcome = { status: 'noop' };
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
codegraphAutoOutcome = { status: 'conflict' };
|
|
365
|
+
warningsForEnvelope.push(`${guard.codegraphDir} already exists with a non-peaks-loop schema; ` +
|
|
366
|
+
'refusing to auto-init codegraph. Run `peaks codegraph init --project <path>` after moving the foreign directory.');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
warningsForEnvelope.push(`codegraph auto-stake failed: ${getErrorMessage(err)}`);
|
|
371
|
+
}
|
|
340
372
|
printResult(io, ok('workspace.init', {
|
|
341
373
|
...report,
|
|
342
374
|
// Slice 018: surface outer-session-mismatch rotation in the
|
|
@@ -351,6 +383,8 @@ export function registerWorkspaceInitCommand(workspace, io) {
|
|
|
351
383
|
scope: hooksOutcome.scope,
|
|
352
384
|
...(hooksOutcome.reason !== undefined ? { reason: hooksOutcome.reason } : {})
|
|
353
385
|
},
|
|
386
|
+
// rid-CG-001: surface codegraph auto-stake outcome.
|
|
387
|
+
...(codegraphAutoOutcome !== null ? { codegraphAutoStake: codegraphAutoOutcome } : {}),
|
|
354
388
|
// Slice 2026-07-15-project-scan-bootstrap (G2): envelope
|
|
355
389
|
// for the .peaks/project-scan/ bootstrap. Always present
|
|
356
390
|
// (even when skipped / errored) so downstream readers can
|
|
@@ -40,4 +40,163 @@ export type CodegraphExecutionResult = {
|
|
|
40
40
|
export type CodegraphProcessRunner = (invocation: CodegraphInvocation) => Promise<CodegraphExecutionResult>;
|
|
41
41
|
export declare function createCodegraphInvocation(options: CodegraphInvocationOptions): CodegraphInvocation;
|
|
42
42
|
export declare function executeCodegraphInvocation(invocation: CodegraphInvocation, runner?: CodegraphProcessRunner): Promise<CodegraphExecutionResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Slice rid-CG-006 — root `.codegraph/` (legacy). peaks-loop still
|
|
45
|
+
* reads from this location for back-compat with downstream consumers
|
|
46
|
+
* whose projects pre-date the slice rid-CG-003 preferred-path move.
|
|
47
|
+
*/
|
|
48
|
+
export declare const CODEGRAPH_DIR_NAME = ".codegraph";
|
|
49
|
+
/**
|
|
50
|
+
* Slice rid-CG-003 — preferred managed location. Going forward
|
|
51
|
+
* peaks-loop writes here; legacy root `.codegraph/` is only used
|
|
52
|
+
* when it pre-exists AND `.peaks/.codegraph/` does not.
|
|
53
|
+
*/
|
|
54
|
+
export declare const PREFERRED_CODEGRAPH_DIR = ".peaks/.codegraph";
|
|
55
|
+
/**
|
|
56
|
+
* Marker file peaks-loop writes inside the resolved codegraph dir
|
|
57
|
+
* after a successful upstream init. Its presence distinguishes
|
|
58
|
+
* peaks-loop-managed schemas from foreign ones (aider / cody /
|
|
59
|
+
* etc. all happily use the same directory name).
|
|
60
|
+
*/
|
|
61
|
+
export declare const CODEGRAPH_MARKER_NAME = ".peaks-loop-marker";
|
|
62
|
+
export declare const CODEGRAPH_INIT_CONFLICT_EXIT_CODE = 73;
|
|
63
|
+
export type ResolvedCodegraphLocation = {
|
|
64
|
+
readonly source: 'preferred';
|
|
65
|
+
readonly cwd: string;
|
|
66
|
+
readonly codegraphDir: string;
|
|
67
|
+
} | {
|
|
68
|
+
readonly source: 'legacy';
|
|
69
|
+
readonly cwd: string;
|
|
70
|
+
readonly codegraphDir: string;
|
|
71
|
+
} | {
|
|
72
|
+
readonly source: 'fresh-preferred';
|
|
73
|
+
readonly cwd: string;
|
|
74
|
+
readonly codegraphDir: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Slice rid-CG-003 — pure resolver that returns the cwd the
|
|
78
|
+
* upstream codegraph binary should be spawned with and the
|
|
79
|
+
* absolute path to the data directory it should read/write.
|
|
80
|
+
*
|
|
81
|
+
* Precedence:
|
|
82
|
+
* 1. `<projectRoot>/.peaks/.codegraph/` exists → use it
|
|
83
|
+
* (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path)
|
|
84
|
+
* 2. `<projectRoot>/.codegraph/` exists → fall back to legacy
|
|
85
|
+
* (cwd = `<projectRoot>`, codegraphDir = legacy path)
|
|
86
|
+
* 3. neither exists → default to the preferred path so the next
|
|
87
|
+
* `peaks codegraph init` lands in `.peaks/.codegraph/`
|
|
88
|
+
* (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path,
|
|
89
|
+
* `source: 'fresh-preferred'`)
|
|
90
|
+
*
|
|
91
|
+
* The cwd is the directory the upstream binary treats as "project
|
|
92
|
+
* root" — the binary's default codegraph discovery reads
|
|
93
|
+
* `<cwd>/.codegraph/`, so we always set cwd to the PARENT of the
|
|
94
|
+
* resolved codegraph dir. Pure fs check; no IO beyond `existsSync`.
|
|
95
|
+
*/
|
|
96
|
+
export declare function resolveCodegraphProjectRoot(projectRoot: string): ResolvedCodegraphLocation;
|
|
97
|
+
export type CodegraphInitGuardResult = {
|
|
98
|
+
status: 'fresh';
|
|
99
|
+
codegraphDir: string;
|
|
100
|
+
} | {
|
|
101
|
+
status: 'noop-already-peaks-loop';
|
|
102
|
+
codegraphDir: string;
|
|
103
|
+
} | {
|
|
104
|
+
status: 'conflict-foreign-schema';
|
|
105
|
+
codegraphDir: string;
|
|
106
|
+
};
|
|
107
|
+
export declare class CodegraphInitConflictError extends Error {
|
|
108
|
+
readonly codegraphDir: string;
|
|
109
|
+
readonly code = "CODEGRAPH_INIT_CONFLICT";
|
|
110
|
+
readonly exitCode = 73;
|
|
111
|
+
constructor(message: string, codegraphDir: string);
|
|
112
|
+
}
|
|
113
|
+
export type CodegraphInitGuard = (projectRoot: string) => CodegraphInitGuardResult;
|
|
114
|
+
/**
|
|
115
|
+
* Slice rid-CG-003 — preferred-path-first init guard.
|
|
116
|
+
*
|
|
117
|
+
* Looks at `.peaks/.codegraph/` first; falls back to `.codegraph/`
|
|
118
|
+
* when the preferred path is absent. The 'fresh' case returns the
|
|
119
|
+
* preferred path so the next `peaks codegraph init` lands inside
|
|
120
|
+
* `.peaks/` instead of polluting the project root.
|
|
121
|
+
*/
|
|
122
|
+
export declare function defaultCodegraphInitGuard(projectRoot: string): CodegraphInitGuardResult;
|
|
123
|
+
/**
|
|
124
|
+
* Pure-fs helper that stamps the peaks-loop marker AFTER a
|
|
125
|
+
* successful upstream init. The CLI action must call this after
|
|
126
|
+
* `executeCodegraphInvocation` returns exit 0.
|
|
127
|
+
*/
|
|
128
|
+
export declare function writeCodegraphMarker(codegraphDir: string): void;
|
|
129
|
+
/**
|
|
130
|
+
* Test seam — pure stub of the default guard. Lets callers inject a
|
|
131
|
+
* pre-computed outcome without touching the real filesystem.
|
|
132
|
+
*/
|
|
133
|
+
export declare function constantCodegraphInitGuard(outcome: CodegraphInitGuardResult): CodegraphInitGuard;
|
|
134
|
+
/**
|
|
135
|
+
* Per-file row rendered into the codegraph-context envelope's
|
|
136
|
+
* Markdown table. Symbol-count + cross-ref edges are intentionally
|
|
137
|
+
* optional: when the upstream `affected` JSON does not carry them
|
|
138
|
+
* (older codegraph versions, or a non-JSON stdout) we leave the
|
|
139
|
+
* cells blank instead of inventing numbers.
|
|
140
|
+
*/
|
|
141
|
+
export type CodegraphAffectedRow = {
|
|
142
|
+
readonly file: string;
|
|
143
|
+
readonly symbolCount?: number;
|
|
144
|
+
readonly crossRefEdges?: number;
|
|
145
|
+
};
|
|
146
|
+
export type WriteCodegraphAffectedContextInput = {
|
|
147
|
+
/** The peaks-loop project root (where `.peaks/_runtime/` lives). */
|
|
148
|
+
readonly projectRoot: string;
|
|
149
|
+
/** Request id, e.g. `rid-CG-002`. Used as the envelope's anchor. */
|
|
150
|
+
readonly rid: string;
|
|
151
|
+
/** Project-relative file paths passed to `peaks codegraph affected`. */
|
|
152
|
+
readonly files: readonly string[];
|
|
153
|
+
/** Raw upstream payload (string or already-parsed JSON). */
|
|
154
|
+
readonly affectedPayload: unknown;
|
|
155
|
+
/**
|
|
156
|
+
* Optional override for the active session id. When omitted,
|
|
157
|
+
* the writer calls `getSessionId(projectRoot)` and falls
|
|
158
|
+
* back to a graceful skip-with-warning when no binding is
|
|
159
|
+
* present (e.g. the consumer ran `peaks codegraph affected`
|
|
160
|
+
* outside of a peaks session).
|
|
161
|
+
*/
|
|
162
|
+
readonly sessionId?: string | null;
|
|
163
|
+
/**
|
|
164
|
+
* Optional clock seam for tests; defaults to `new Date()`.
|
|
165
|
+
*/
|
|
166
|
+
readonly now?: () => Date;
|
|
167
|
+
};
|
|
168
|
+
export type WriteCodegraphAffectedContextResult = {
|
|
169
|
+
readonly written: true;
|
|
170
|
+
readonly path: string;
|
|
171
|
+
readonly sessionId: string;
|
|
172
|
+
} | {
|
|
173
|
+
readonly written: false;
|
|
174
|
+
readonly path: '';
|
|
175
|
+
readonly warning: string;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Pure renderer — turns the affected payload + rid + file list
|
|
179
|
+
* into a Markdown body with a leading human-readable table and
|
|
180
|
+
* a trailing JSON fence for machine consumers.
|
|
181
|
+
*/
|
|
182
|
+
export declare function renderCodegraphAffectedContext(rid: string, files: readonly string[], affectedPayload: unknown, sessionId: string, now?: () => Date): string;
|
|
183
|
+
/**
|
|
184
|
+
* Write the `codegraph-context.md` envelope into the canonical
|
|
185
|
+
* session directory (`.peaks/_runtime/<sessionId>/rd/`). The
|
|
186
|
+
* envelope is what peaks-code's RD dispatch hook reads back to
|
|
187
|
+
* seed the QA / TXT handoff (see `peaks-code/SKILL.md`
|
|
188
|
+
* §"Codegraph orchestration context").
|
|
189
|
+
*
|
|
190
|
+
* The function is intentionally side-effect-safe:
|
|
191
|
+
* - No session binding → returns `{ written: false, warning }`,
|
|
192
|
+
* does NOT throw. Callers can either surface the warning or
|
|
193
|
+
* treat it as a no-op.
|
|
194
|
+
* - Directory creation is `mkdirSync({ recursive: true })`, so
|
|
195
|
+
* the first run in a fresh session creates the `rd/` subdir
|
|
196
|
+
* on demand.
|
|
197
|
+
* - The path is canonicalized through `getSessionDir` so
|
|
198
|
+
* downstream readers (QA / TXT) get a stable path even when
|
|
199
|
+
* the caller passed a relative `projectRoot`.
|
|
200
|
+
*/
|
|
201
|
+
export declare function writeCodegraphAffectedContext(input: WriteCodegraphAffectedContextInput): WriteCodegraphAffectedContextResult;
|
|
43
202
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { defaultCodegraphProcessRunner } from './codegraph-process-runner.js';
|
|
5
|
+
import { getSessionId, getSessionDir } from '../session/index.js';
|
|
5
6
|
const CODEGRAPH_PACKAGE_NAME = '@colbymchenry/codegraph';
|
|
6
7
|
const CODEGRAPH_PACKAGE_VERSION = '0.7.10';
|
|
7
8
|
const CODEGRAPH_EXECUTABLE = process.execPath;
|
|
@@ -142,6 +143,11 @@ function buildCommandArgs(options, projectRoot) {
|
|
|
142
143
|
export function createCodegraphInvocation(options) {
|
|
143
144
|
assertSupportedSubcommand(options.subcommand);
|
|
144
145
|
const projectRoot = resolveProjectRoot(options.project);
|
|
146
|
+
// Slice rid-CG-003 — spawn the upstream binary inside the
|
|
147
|
+
// resolved codegraph dir's PARENT so its default `.codegraph/`
|
|
148
|
+
// discovery lands on `.peaks/.codegraph/` (preferred) or
|
|
149
|
+
// `.codegraph/` (legacy fallback).
|
|
150
|
+
const location = resolveCodegraphProjectRoot(projectRoot);
|
|
145
151
|
assertSupportedOptions(options);
|
|
146
152
|
assertRequiredOptions(options);
|
|
147
153
|
assertPositiveInteger(options.limit, 'limit');
|
|
@@ -149,7 +155,7 @@ export function createCodegraphInvocation(options) {
|
|
|
149
155
|
return {
|
|
150
156
|
executable: CODEGRAPH_EXECUTABLE,
|
|
151
157
|
args: buildCommandArgs(options, projectRoot),
|
|
152
|
-
cwd:
|
|
158
|
+
cwd: location.cwd,
|
|
153
159
|
packageName: CODEGRAPH_PACKAGE_NAME,
|
|
154
160
|
packageVersion: CODEGRAPH_PACKAGE_VERSION,
|
|
155
161
|
subcommand: options.subcommand
|
|
@@ -158,3 +164,211 @@ export function createCodegraphInvocation(options) {
|
|
|
158
164
|
export async function executeCodegraphInvocation(invocation, runner = defaultCodegraphProcessRunner) {
|
|
159
165
|
return runner(invocation);
|
|
160
166
|
}
|
|
167
|
+
/* ──────────────────────────────────────────────────────────────────────
|
|
168
|
+
* Slice rid-CG-003 — preferred `.peaks/.codegraph/` lookup + legacy
|
|
169
|
+
* root `.codegraph/` fallback (spike follow-up #1).
|
|
170
|
+
* ────────────────────────────────────────────────────────────────────── */
|
|
171
|
+
/**
|
|
172
|
+
* Slice rid-CG-006 — root `.codegraph/` (legacy). peaks-loop still
|
|
173
|
+
* reads from this location for back-compat with downstream consumers
|
|
174
|
+
* whose projects pre-date the slice rid-CG-003 preferred-path move.
|
|
175
|
+
*/
|
|
176
|
+
export const CODEGRAPH_DIR_NAME = '.codegraph';
|
|
177
|
+
/**
|
|
178
|
+
* Slice rid-CG-003 — preferred managed location. Going forward
|
|
179
|
+
* peaks-loop writes here; legacy root `.codegraph/` is only used
|
|
180
|
+
* when it pre-exists AND `.peaks/.codegraph/` does not.
|
|
181
|
+
*/
|
|
182
|
+
export const PREFERRED_CODEGRAPH_DIR = '.peaks/.codegraph';
|
|
183
|
+
/**
|
|
184
|
+
* Marker file peaks-loop writes inside the resolved codegraph dir
|
|
185
|
+
* after a successful upstream init. Its presence distinguishes
|
|
186
|
+
* peaks-loop-managed schemas from foreign ones (aider / cody /
|
|
187
|
+
* etc. all happily use the same directory name).
|
|
188
|
+
*/
|
|
189
|
+
export const CODEGRAPH_MARKER_NAME = '.peaks-loop-marker';
|
|
190
|
+
export const CODEGRAPH_INIT_CONFLICT_EXIT_CODE = 73;
|
|
191
|
+
/**
|
|
192
|
+
* Slice rid-CG-003 — pure resolver that returns the cwd the
|
|
193
|
+
* upstream codegraph binary should be spawned with and the
|
|
194
|
+
* absolute path to the data directory it should read/write.
|
|
195
|
+
*
|
|
196
|
+
* Precedence:
|
|
197
|
+
* 1. `<projectRoot>/.peaks/.codegraph/` exists → use it
|
|
198
|
+
* (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path)
|
|
199
|
+
* 2. `<projectRoot>/.codegraph/` exists → fall back to legacy
|
|
200
|
+
* (cwd = `<projectRoot>`, codegraphDir = legacy path)
|
|
201
|
+
* 3. neither exists → default to the preferred path so the next
|
|
202
|
+
* `peaks codegraph init` lands in `.peaks/.codegraph/`
|
|
203
|
+
* (cwd = `<projectRoot>/.peaks`, codegraphDir = preferred path,
|
|
204
|
+
* `source: 'fresh-preferred'`)
|
|
205
|
+
*
|
|
206
|
+
* The cwd is the directory the upstream binary treats as "project
|
|
207
|
+
* root" — the binary's default codegraph discovery reads
|
|
208
|
+
* `<cwd>/.codegraph/`, so we always set cwd to the PARENT of the
|
|
209
|
+
* resolved codegraph dir. Pure fs check; no IO beyond `existsSync`.
|
|
210
|
+
*/
|
|
211
|
+
export function resolveCodegraphProjectRoot(projectRoot) {
|
|
212
|
+
const preferredDir = join(projectRoot, PREFERRED_CODEGRAPH_DIR);
|
|
213
|
+
const legacyDir = join(projectRoot, CODEGRAPH_DIR_NAME);
|
|
214
|
+
if (existsSync(preferredDir)) {
|
|
215
|
+
return {
|
|
216
|
+
source: 'preferred',
|
|
217
|
+
cwd: join(projectRoot, '.peaks'),
|
|
218
|
+
codegraphDir: preferredDir
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (existsSync(legacyDir)) {
|
|
222
|
+
return {
|
|
223
|
+
source: 'legacy',
|
|
224
|
+
cwd: projectRoot,
|
|
225
|
+
codegraphDir: legacyDir
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
source: 'fresh-preferred',
|
|
230
|
+
cwd: join(projectRoot, '.peaks'),
|
|
231
|
+
codegraphDir: preferredDir
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export class CodegraphInitConflictError extends Error {
|
|
235
|
+
codegraphDir;
|
|
236
|
+
code = 'CODEGRAPH_INIT_CONFLICT';
|
|
237
|
+
exitCode = CODEGRAPH_INIT_CONFLICT_EXIT_CODE;
|
|
238
|
+
constructor(message, codegraphDir) {
|
|
239
|
+
super(message);
|
|
240
|
+
this.codegraphDir = codegraphDir;
|
|
241
|
+
this.name = 'CodegraphInitConflictError';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Inspect a candidate codegraph directory and return its guard
|
|
246
|
+
* status. A file (or symlink-to-file) at the path counts as a
|
|
247
|
+
* foreign-schema conflict because it blocks directory creation.
|
|
248
|
+
*/
|
|
249
|
+
function inspectCandidateCodegraphDir(codegraphDir) {
|
|
250
|
+
let isDir = false;
|
|
251
|
+
try {
|
|
252
|
+
isDir = statSync(codegraphDir).isDirectory();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
isDir = false;
|
|
256
|
+
}
|
|
257
|
+
if (!isDir) {
|
|
258
|
+
return { status: 'conflict-foreign-schema', codegraphDir };
|
|
259
|
+
}
|
|
260
|
+
const markerPath = join(codegraphDir, CODEGRAPH_MARKER_NAME);
|
|
261
|
+
if (existsSync(markerPath)) {
|
|
262
|
+
return { status: 'noop-already-peaks-loop', codegraphDir };
|
|
263
|
+
}
|
|
264
|
+
return { status: 'conflict-foreign-schema', codegraphDir };
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Slice rid-CG-003 — preferred-path-first init guard.
|
|
268
|
+
*
|
|
269
|
+
* Looks at `.peaks/.codegraph/` first; falls back to `.codegraph/`
|
|
270
|
+
* when the preferred path is absent. The 'fresh' case returns the
|
|
271
|
+
* preferred path so the next `peaks codegraph init` lands inside
|
|
272
|
+
* `.peaks/` instead of polluting the project root.
|
|
273
|
+
*/
|
|
274
|
+
export function defaultCodegraphInitGuard(projectRoot) {
|
|
275
|
+
const preferredDir = join(projectRoot, PREFERRED_CODEGRAPH_DIR);
|
|
276
|
+
const legacyDir = join(projectRoot, CODEGRAPH_DIR_NAME);
|
|
277
|
+
if (existsSync(preferredDir)) {
|
|
278
|
+
return inspectCandidateCodegraphDir(preferredDir);
|
|
279
|
+
}
|
|
280
|
+
if (existsSync(legacyDir)) {
|
|
281
|
+
return inspectCandidateCodegraphDir(legacyDir);
|
|
282
|
+
}
|
|
283
|
+
return { status: 'fresh', codegraphDir: preferredDir };
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Pure-fs helper that stamps the peaks-loop marker AFTER a
|
|
287
|
+
* successful upstream init. The CLI action must call this after
|
|
288
|
+
* `executeCodegraphInvocation` returns exit 0.
|
|
289
|
+
*/
|
|
290
|
+
export function writeCodegraphMarker(codegraphDir) {
|
|
291
|
+
writeFileSync(join(codegraphDir, CODEGRAPH_MARKER_NAME), 'peaks-loop-managed\n', 'utf8');
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Test seam — pure stub of the default guard. Lets callers inject a
|
|
295
|
+
* pre-computed outcome without touching the real filesystem.
|
|
296
|
+
*/
|
|
297
|
+
export function constantCodegraphInitGuard(outcome) {
|
|
298
|
+
return () => outcome;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Pure renderer — turns the affected payload + rid + file list
|
|
302
|
+
* into a Markdown body with a leading human-readable table and
|
|
303
|
+
* a trailing JSON fence for machine consumers.
|
|
304
|
+
*/
|
|
305
|
+
export function renderCodegraphAffectedContext(rid, files, affectedPayload, sessionId, now = () => new Date()) {
|
|
306
|
+
const rows = files.map((file) => ({ file }));
|
|
307
|
+
const generatedAt = now().toISOString();
|
|
308
|
+
const lines = [
|
|
309
|
+
'# Codegraph orchestration context',
|
|
310
|
+
'',
|
|
311
|
+
`- sessionId: \`${sessionId}\``,
|
|
312
|
+
`- rid: \`${rid}\``,
|
|
313
|
+
`- generatedAt: \`${generatedAt}\``,
|
|
314
|
+
'',
|
|
315
|
+
'## Affected files',
|
|
316
|
+
'',
|
|
317
|
+
'| file | symbolCount | crossRefEdges |',
|
|
318
|
+
'| --- | --- | --- |',
|
|
319
|
+
...rows.map((row) => {
|
|
320
|
+
const sym = row.symbolCount === undefined ? '' : String(row.symbolCount);
|
|
321
|
+
const edges = row.crossRefEdges === undefined ? '' : String(row.crossRefEdges);
|
|
322
|
+
return `| \`${row.file}\` | ${sym} | ${edges} |`;
|
|
323
|
+
}),
|
|
324
|
+
'',
|
|
325
|
+
'## Raw upstream output',
|
|
326
|
+
'',
|
|
327
|
+
'```json',
|
|
328
|
+
typeof affectedPayload === 'string'
|
|
329
|
+
? affectedPayload
|
|
330
|
+
: JSON.stringify(affectedPayload, null, 2),
|
|
331
|
+
'```',
|
|
332
|
+
''
|
|
333
|
+
];
|
|
334
|
+
return lines.join('\n');
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Write the `codegraph-context.md` envelope into the canonical
|
|
338
|
+
* session directory (`.peaks/_runtime/<sessionId>/rd/`). The
|
|
339
|
+
* envelope is what peaks-code's RD dispatch hook reads back to
|
|
340
|
+
* seed the QA / TXT handoff (see `peaks-code/SKILL.md`
|
|
341
|
+
* §"Codegraph orchestration context").
|
|
342
|
+
*
|
|
343
|
+
* The function is intentionally side-effect-safe:
|
|
344
|
+
* - No session binding → returns `{ written: false, warning }`,
|
|
345
|
+
* does NOT throw. Callers can either surface the warning or
|
|
346
|
+
* treat it as a no-op.
|
|
347
|
+
* - Directory creation is `mkdirSync({ recursive: true })`, so
|
|
348
|
+
* the first run in a fresh session creates the `rd/` subdir
|
|
349
|
+
* on demand.
|
|
350
|
+
* - The path is canonicalized through `getSessionDir` so
|
|
351
|
+
* downstream readers (QA / TXT) get a stable path even when
|
|
352
|
+
* the caller passed a relative `projectRoot`.
|
|
353
|
+
*/
|
|
354
|
+
export function writeCodegraphAffectedContext(input) {
|
|
355
|
+
const sessionId = input.sessionId ?? getSessionId(input.projectRoot);
|
|
356
|
+
if (!sessionId || sessionId.length === 0) {
|
|
357
|
+
return {
|
|
358
|
+
written: false,
|
|
359
|
+
path: '',
|
|
360
|
+
warning: 'No active peaks-loop session binding; skipping codegraph-context envelope. ' +
|
|
361
|
+
'Run `peaks workspace init` (or `peaks session set <id>`) and re-invoke ' +
|
|
362
|
+
'`peaks codegraph affected`.'
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
const sessionDir = getSessionDir(input.projectRoot, sessionId);
|
|
366
|
+
const rdDir = join(sessionDir, 'rd');
|
|
367
|
+
if (!existsSync(rdDir)) {
|
|
368
|
+
mkdirSync(rdDir, { recursive: true });
|
|
369
|
+
}
|
|
370
|
+
const contextPath = join(rdDir, 'codegraph-context.md');
|
|
371
|
+
const body = renderCodegraphAffectedContext(input.rid, input.files, input.affectedPayload, sessionId, input.now);
|
|
372
|
+
writeFileSync(contextPath, body, 'utf8');
|
|
373
|
+
return { written: true, path: contextPath, sessionId };
|
|
374
|
+
}
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* Fails when the version drifts, when the binary is missing, or
|
|
7
7
|
* when the package is not resolvable at all.
|
|
8
8
|
*
|
|
9
|
+
* Slice rid-CG-003 (spike follow-up #3): also detects which managed
|
|
10
|
+
* codegraph directory is in use — `.peaks/.codegraph/` (preferred)
|
|
11
|
+
* or root `.codegraph/` (legacy) — via the injected `managedPathProbe`.
|
|
12
|
+
* The CG-007 yarn-pnp fallback is preserved as the package-resolution
|
|
13
|
+
* default; the managed-path probe defaults to the same fs-walk-style
|
|
14
|
+
* helper and is independently injectable for tests.
|
|
15
|
+
*
|
|
9
16
|
* The probe is injected so tests do not depend on the real
|
|
10
17
|
* `node_modules` resolution; the default probe uses
|
|
11
18
|
* `createRequire(import.meta.url)` to find the package.
|