peaks-loop 4.0.35 → 4.0.37
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 +40 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/bin/peaks.js +71 -1
- package/dist/cli/cli-helpers.js +7 -0
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/best-practice-scan-command.d.ts +14 -1
- package/dist/cli/commands/best-practice-scan-command.js +67 -9
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +78 -7
- package/dist/cli/commands/core/doctor-command.d.ts +8 -0
- package/dist/cli/commands/core/doctor-command.js +44 -2
- package/dist/cli/commands/core/memory-command.js +5 -1
- package/dist/cli/commands/dispatch-commands.js +15 -3
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/hooks-commands.js +10 -1
- package/dist/cli/commands/job-commands.js +107 -25
- package/dist/cli/commands/memory-commands.d.ts +24 -0
- package/dist/cli/commands/memory-commands.js +77 -10
- package/dist/cli/commands/request-commands.d.ts +8 -0
- package/dist/cli/commands/request-commands.js +23 -2
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/sub-agent-commands.js +2 -0
- package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
- package/dist/cli/commands/wave-plan-commands.js +93 -0
- package/dist/cli/commands/web-commands.d.ts +28 -0
- package/dist/cli/commands/web-commands.js +327 -0
- package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
- package/dist/cli/commands/web-lifecycle-commands.js +321 -0
- package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
- package/dist/services/best-practice/scan-orchestrator.js +14 -5
- package/dist/services/code/orchestrator-can-do.js +27 -4
- package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
- package/dist/services/context/build-dispatch-system-prompt.js +132 -17
- package/dist/services/context/context-audit-hint.d.ts +79 -0
- package/dist/services/context/context-audit-hint.js +150 -0
- package/dist/services/context/context-audit.d.ts +100 -0
- package/dist/services/context/context-audit.js +322 -0
- package/dist/services/context/summary-view.d.ts +54 -0
- package/dist/services/context/summary-view.js +114 -0
- package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
- package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
- package/dist/services/dispatch/session-capsule.d.ts +23 -0
- package/dist/services/dispatch/session-capsule.js +56 -0
- package/dist/services/dispatch/slice-dag.d.ts +9 -0
- package/dist/services/dispatch/slice-dag.js +9 -1
- package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
- package/dist/services/dispatch/test-tool-detection.js +14 -13
- package/dist/services/hooks/auto-compact-hook-install.js +10 -1
- package/dist/services/hooks/write-gate.js +88 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/lint/detect-eslint.d.ts +2 -0
- package/dist/services/lint/detect-eslint.js +23 -9
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +38 -14
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
- package/dist/services/release/version-precheck-service.js +9 -2
- package/dist/services/scan/file-size-scan.d.ts +29 -0
- package/dist/services/scan/file-size-scan.js +63 -0
- package/dist/services/session/caller-binding-service.d.ts +24 -0
- package/dist/services/session/caller-binding-service.js +34 -0
- package/dist/services/session/getSessionDir.js +15 -10
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
- package/dist/services/skills/hooks-settings-service.d.ts +10 -0
- package/dist/services/skills/hooks-settings-service.js +152 -61
- package/dist/services/slice/slice-check-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +110 -50
- package/dist/services/slice/slice-check-types.d.ts +12 -7
- package/dist/services/slice/slice-check-types.js +8 -3
- package/dist/services/slice/slice-decompose-runners.js +24 -21
- package/dist/services/sop/sop-check-service.js +12 -1
- package/dist/services/web/bounded-output.d.ts +34 -0
- package/dist/services/web/bounded-output.js +68 -0
- package/dist/services/web/browser-acquire.d.ts +14 -0
- package/dist/services/web/browser-acquire.js +84 -0
- package/dist/services/web/browser-session-manager.d.ts +111 -0
- package/dist/services/web/browser-session-manager.js +413 -0
- package/dist/services/web/daemon-entry.d.ts +1 -0
- package/dist/services/web/daemon-entry.js +65 -0
- package/dist/services/web/daemon-registry.d.ts +42 -0
- package/dist/services/web/daemon-registry.js +164 -0
- package/dist/services/web/daemon-supervisor.d.ts +144 -0
- package/dist/services/web/daemon-supervisor.js +455 -0
- package/dist/services/web/playwright-loader.d.ts +89 -0
- package/dist/services/web/playwright-loader.js +253 -0
- package/dist/services/web/snapshot-pruner.d.ts +48 -0
- package/dist/services/web/snapshot-pruner.js +241 -0
- package/dist/services/web/untrusted-envelope.d.ts +27 -0
- package/dist/services/web/untrusted-envelope.js +44 -0
- package/dist/services/web/web-artifact-paths.d.ts +79 -0
- package/dist/services/web/web-artifact-paths.js +163 -0
- package/dist/services/web/web-client.d.ts +19 -0
- package/dist/services/web/web-client.js +55 -0
- package/dist/services/web/web-daemon-service.d.ts +38 -0
- package/dist/services/web/web-daemon-service.js +416 -0
- package/dist/services/web/web-fallback.d.ts +70 -0
- package/dist/services/web/web-fallback.js +121 -0
- package/dist/services/web/web-install-service.d.ts +91 -0
- package/dist/services/web/web-install-service.js +346 -0
- package/dist/services/web/web-login-profile.d.ts +89 -0
- package/dist/services/web/web-login-profile.js +612 -0
- package/dist/services/web/web-login-staging.d.ts +27 -0
- package/dist/services/web/web-login-staging.js +173 -0
- package/dist/services/web/web-protocol.d.ts +58 -0
- package/dist/services/web/web-protocol.js +58 -0
- package/dist/services/web/web-status-report.d.ts +33 -0
- package/dist/services/web/web-status-report.js +47 -0
- package/dist/services/workspace/claude-settings-template.d.ts +41 -5
- package/dist/services/workspace/claude-settings-template.js +116 -64
- package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
- package/dist/services/workspace/workspace-service.js +33 -0
- package/package.json +5 -5
- package/scripts/copy-templates.mjs +12 -0
- package/scripts/sync-version.mjs +20 -0
- package/skills/bee/peaks-qa/SKILL.md +2 -0
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
- package/skills/bee/peaks-rd/SKILL.md +2 -0
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
- package/skills/bee/peaks-txt/SKILL.md +2 -0
- package/skills/bee/peaks-ui/SKILL.md +2 -0
- package/skills/peaks-code/SKILL.md +18 -0
- package/skills/peaks-code/references/browser-workflow.md +10 -1
- package/skills/peaks-code/references/context-governance.md +29 -0
- package/skills/peaks-doctor/SKILL.md +2 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Reserved batch id for the session capsule (matches the channel path pattern). */
|
|
2
|
+
export declare const SESSION_CAPSULE_BATCH_ID = "session-capsule";
|
|
3
|
+
/** Reserved key inside that channel. */
|
|
4
|
+
export declare const SESSION_CAPSULE_KEY = "orchestrator.capsule";
|
|
5
|
+
export interface SessionCapsuleRef {
|
|
6
|
+
readonly batchId: string;
|
|
7
|
+
readonly key: string;
|
|
8
|
+
/** Byte size of the published value (for the pointer line). */
|
|
9
|
+
readonly bytes: number;
|
|
10
|
+
/** ISO8601 timestamp of the last write. */
|
|
11
|
+
readonly updatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Read the session capsule, if one was published. Returns `null` when the
|
|
15
|
+
* channel is absent, empty, or unreadable — the dispatch prompt then
|
|
16
|
+
* renders neither the pointer nor the precedence line (byte-identical to
|
|
17
|
+
* the pre-slice shape).
|
|
18
|
+
*/
|
|
19
|
+
export declare function readSessionCapsule(opts: {
|
|
20
|
+
projectRoot: string;
|
|
21
|
+
sid: string;
|
|
22
|
+
rid: string;
|
|
23
|
+
}): SessionCapsuleRef | null;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2026-09-10-dispatch-token-and-swarm §4 — session capsule reader.
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator publishes already-known background ONCE per session
|
|
5
|
+
* through the existing G8.4 channel:
|
|
6
|
+
*
|
|
7
|
+
* peaks sub-agent share \
|
|
8
|
+
* --batch session-capsule \
|
|
9
|
+
* --key orchestrator.capsule \
|
|
10
|
+
* --value '{"rootCauses":[...],"decisions":[...],"fileMap":{...}}'
|
|
11
|
+
*
|
|
12
|
+
* Dispatch prompts then carry a one-line `shared-read` pointer instead of
|
|
13
|
+
* re-explaining that background in every task spec.
|
|
14
|
+
*
|
|
15
|
+
* QUALITY GUARD: the capsule is ADVISORY BACKGROUND ONLY. Nothing a
|
|
16
|
+
* sub-agent must ACT on may live only in the capsule — the task spec is
|
|
17
|
+
* authoritative and wins on conflict. The precedence sentence is rendered
|
|
18
|
+
* by `renderCapsulePointer` in build-dispatch-system-prompt.ts and is not
|
|
19
|
+
* optional when the pointer is emitted.
|
|
20
|
+
*
|
|
21
|
+
* This module only READS. Publishing is the orchestrator's job through the
|
|
22
|
+
* already-shipped `peaks sub-agent share` primitive — no new write path.
|
|
23
|
+
*/
|
|
24
|
+
import { readSharedChannel } from 'peaks-loop-shared-channel';
|
|
25
|
+
/** Reserved batch id for the session capsule (matches the channel path pattern). */
|
|
26
|
+
export const SESSION_CAPSULE_BATCH_ID = 'session-capsule';
|
|
27
|
+
/** Reserved key inside that channel. */
|
|
28
|
+
export const SESSION_CAPSULE_KEY = 'orchestrator.capsule';
|
|
29
|
+
/**
|
|
30
|
+
* Read the session capsule, if one was published. Returns `null` when the
|
|
31
|
+
* channel is absent, empty, or unreadable — the dispatch prompt then
|
|
32
|
+
* renders neither the pointer nor the precedence line (byte-identical to
|
|
33
|
+
* the pre-slice shape).
|
|
34
|
+
*/
|
|
35
|
+
export function readSessionCapsule(opts) {
|
|
36
|
+
try {
|
|
37
|
+
const channel = readSharedChannel({
|
|
38
|
+
projectRoot: opts.projectRoot,
|
|
39
|
+
sid: opts.sid,
|
|
40
|
+
rid: opts.rid,
|
|
41
|
+
batchId: SESSION_CAPSULE_BATCH_ID
|
|
42
|
+
});
|
|
43
|
+
const entry = channel.entries[SESSION_CAPSULE_KEY];
|
|
44
|
+
if (entry === undefined)
|
|
45
|
+
return null;
|
|
46
|
+
return {
|
|
47
|
+
batchId: SESSION_CAPSULE_BATCH_ID,
|
|
48
|
+
key: SESSION_CAPSULE_KEY,
|
|
49
|
+
bytes: entry.valueSize,
|
|
50
|
+
updatedAt: entry.at
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null; // fail-soft: a missing capsule never blocks a dispatch
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -31,6 +31,15 @@ export interface SliceNode {
|
|
|
31
31
|
* (complex = user-attended, simple/trivial = overnight). Optional.
|
|
32
32
|
*/
|
|
33
33
|
readonly complexity?: SliceComplexity;
|
|
34
|
+
/**
|
|
35
|
+
* Slice 2026-09-10-dispatch-token-and-swarm §3: files this slice is
|
|
36
|
+
* expected to touch. When EVERY node of a topological level declares
|
|
37
|
+
* `files`, `--from-dag` emits a file-overlap wave plan (`firstLevelWaves`)
|
|
38
|
+
* so the LLM can fan the level out without serializing on a shared file.
|
|
39
|
+
* Optional and additive: absent → DAG hash and dispatch behavior are
|
|
40
|
+
* byte-identical to before this slice.
|
|
41
|
+
*/
|
|
42
|
+
readonly files?: readonly string[];
|
|
34
43
|
}
|
|
35
44
|
export interface DependsOn {
|
|
36
45
|
readonly from: string;
|
|
@@ -103,6 +103,11 @@ export function validateDag(dag) {
|
|
|
103
103
|
if (n.complexity !== undefined && !isSliceComplexity(n.complexity)) {
|
|
104
104
|
throw new InvalidSliceDagError(`node ${n.id} complexity must be one of ${SLICE_COMPLEXITIES.join('|')} when present`);
|
|
105
105
|
}
|
|
106
|
+
// Slice 2026-09-10 §3: optional file list. Only shape-checked when
|
|
107
|
+
// present so pre-existing DAGs stay valid.
|
|
108
|
+
if (n.files !== undefined && (!Array.isArray(n.files) || n.files.some((f) => typeof f !== 'string' || f.length === 0))) {
|
|
109
|
+
throw new InvalidSliceDagError(`node ${n.id} files must be an array of non-empty strings when present`);
|
|
110
|
+
}
|
|
106
111
|
}
|
|
107
112
|
// v2.15.0 follow-up — G12 defensive rule: foundation slice can only
|
|
108
113
|
// depend on another foundation slice. Business depending on foundation
|
|
@@ -219,7 +224,10 @@ export function serializeDag(dag) {
|
|
|
219
224
|
// (only when present, preserving hash stability for old DAGs).
|
|
220
225
|
...(n.foundation !== undefined ? { foundation: n.foundation } : {}),
|
|
221
226
|
...(n.upstreamSync !== undefined ? { upstreamSync: n.upstreamSync } : {}),
|
|
222
|
-
...(n.complexity !== undefined ? { complexity: n.complexity } : {})
|
|
227
|
+
...(n.complexity !== undefined ? { complexity: n.complexity } : {}),
|
|
228
|
+
// Slice 2026-09-10 §3: only present when declared, so the hash of a
|
|
229
|
+
// file-less DAG is unchanged.
|
|
230
|
+
...(n.files !== undefined ? { files: [...n.files] } : {})
|
|
223
231
|
}));
|
|
224
232
|
const edges = [...dag.edges]
|
|
225
233
|
.sort((a, b) => {
|
|
@@ -26,8 +26,19 @@
|
|
|
26
26
|
* "remove the redundancy" of a test-tool-detection instruction because
|
|
27
27
|
* the dispatch CLI always prepends it. This is a guarantee, not a
|
|
28
28
|
* suggestion.
|
|
29
|
+
*
|
|
30
|
+
* 2026-09-10-dispatch-block-d (Option D): ONE block for EVERY role. The
|
|
31
|
+
* runner-table EXAMPLES were dropped — they were never rules, and
|
|
32
|
+
* `package.json#scripts.test` is the source of truth at run time — while
|
|
33
|
+
* the one missing real rule is stated inline: PB-5, i.e. repo-defined
|
|
34
|
+
* `test` / `test:*` scripts are the human/LLM direct path and are NOT
|
|
35
|
+
* gated by the scope rule. The soft fallback ("only as a last resort, ask
|
|
36
|
+
* the user before assuming a runner") and the Windows-aware note on
|
|
37
|
+
* `peaks test <file>` are RETAINED — they are quality guidance, not
|
|
38
|
+
* examples. No role split remains, so every role receives a byte-identical
|
|
39
|
+
* block.
|
|
29
40
|
*/
|
|
30
|
-
export declare const TEST_TOOL_DETECTION_BLOCK = "## Test Tool Detection (mandatory)\n\
|
|
41
|
+
export declare const TEST_TOOL_DETECTION_BLOCK = "## Test Tool Detection (mandatory)\n\nRead `package.json#scripts.test` for the project's framework and use the project-local runner \u2014 do NOT invoke `npx <runner>`. If unsure, run `peaks test --json` first. Only as a last resort, ask the user before assuming a runner. `peaks test <file>` already resolves the local binary for you (Windows-aware).\n\n## Test Scope (mandatory)\n\nAny test command MUST be **scoped** to a single file or pattern; a bare `./node_modules/.bin/vitest run` is **refused** unless you prefix the explicit opt-in token `PEAKS_FULL_TEST=1` (CI / release verification only, never routine slice verification).\n\nRepo-defined `test` / `test:*` scripts are the human/LLM direct path and are NOT gated by this scope rule (PB-5).";
|
|
31
42
|
/**
|
|
32
43
|
* Pure helper that returns the block. Exists as a function (not just an
|
|
33
44
|
* exported constant) so future variants can take a runtime parameter
|
|
@@ -26,26 +26,27 @@
|
|
|
26
26
|
* "remove the redundancy" of a test-tool-detection instruction because
|
|
27
27
|
* the dispatch CLI always prepends it. This is a guarantee, not a
|
|
28
28
|
* suggestion.
|
|
29
|
+
*
|
|
30
|
+
* 2026-09-10-dispatch-block-d (Option D): ONE block for EVERY role. The
|
|
31
|
+
* runner-table EXAMPLES were dropped — they were never rules, and
|
|
32
|
+
* `package.json#scripts.test` is the source of truth at run time — while
|
|
33
|
+
* the one missing real rule is stated inline: PB-5, i.e. repo-defined
|
|
34
|
+
* `test` / `test:*` scripts are the human/LLM direct path and are NOT
|
|
35
|
+
* gated by the scope rule. The soft fallback ("only as a last resort, ask
|
|
36
|
+
* the user before assuming a runner") and the Windows-aware note on
|
|
37
|
+
* `peaks test <file>` are RETAINED — they are quality guidance, not
|
|
38
|
+
* examples. No role split remains, so every role receives a byte-identical
|
|
39
|
+
* block.
|
|
29
40
|
*/
|
|
30
41
|
export const TEST_TOOL_DETECTION_BLOCK = `## Test Tool Detection (mandatory)
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- **vitest** → \`./node_modules/.bin/vitest run <file>\` (or \`pnpm test -- <file>\`)
|
|
35
|
-
- **jest** → \`./node_modules/.bin/jest <file>\` (or \`pnpm test -- <file>\`)
|
|
36
|
-
- **mocha** → \`./node_modules/.bin/mocha <file>\` (or \`pnpm test -- <file>\`)
|
|
43
|
+
Read \`package.json#scripts.test\` for the project's framework and use the project-local runner — do NOT invoke \`npx <runner>\`. If unsure, run \`peaks test --json\` first. Only as a last resort, ask the user before assuming a runner. \`peaks test <file>\` already resolves the local binary for you (Windows-aware).
|
|
37
44
|
|
|
38
45
|
## Test Scope (mandatory)
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- **scoped** → \`./node_modules/.bin/vitest run tests/unit/foo.test.ts\` (or any explicit file/pattern)
|
|
43
|
-
- **intentional full run** → prefix with the explicit opt-in token \`PEAKS_FULL_TEST=1\` to override the scope gate. Use only for CI / release verification, never for routine verification during a slice.
|
|
44
|
-
- **refused** → bare \`./node_modules/.bin/vitest run\` (no argument after \`run\`) without the opt-in token.
|
|
45
|
-
|
|
46
|
-
\`pnpm test\` / \`pnpm test:unit\` / \`pnpm test:cli\` / \`pnpm test:integration\` and any repo-defined \`test*\` script remain the **human / LLM direct path** and are not gated by this rule (PB-5).
|
|
47
|
+
Any test command MUST be **scoped** to a single file or pattern; a bare \`./node_modules/.bin/vitest run\` is **refused** unless you prefix the explicit opt-in token \`PEAKS_FULL_TEST=1\` (CI / release verification only, never routine slice verification).
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
Repo-defined \`test\` / \`test:*\` scripts are the human/LLM direct path and are NOT gated by this scope rule (PB-5).`;
|
|
49
50
|
/**
|
|
50
51
|
* Pure helper that returns the block. Exists as a function (not just an
|
|
51
52
|
* exported constant) so future variants can take a runtime parameter
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
34
34
|
import { dirname, join } from 'node:path';
|
|
35
|
+
import { resolveHookShell } from '../skills/hooks-codegate-superpowers.js';
|
|
35
36
|
/**
|
|
36
37
|
* Stable matcher for the auto-compact hook. Single source of truth
|
|
37
38
|
* — `install` and `remove` both key off this constant so the
|
|
@@ -105,6 +106,13 @@ export function installAutoCompactHook(input) {
|
|
|
105
106
|
if (alreadyInstalled) {
|
|
106
107
|
return { action: 'already-installed', settingsPath };
|
|
107
108
|
}
|
|
109
|
+
// The matcher is `Bash|Task`, so on Windows this runs on the same
|
|
110
|
+
// Git-Bash / MSYS2 shell-form path as the other peaks Bash hooks, which
|
|
111
|
+
// force-allocates a console window on every matching tool call. The
|
|
112
|
+
// entry is written only into the machine-local, gitignored
|
|
113
|
+
// `.claude/settings.local.json`, so a machine-specific `shell` cannot
|
|
114
|
+
// leak into a shared file. `undefined` on POSIX omits the key entirely.
|
|
115
|
+
const shell = resolveHookShell();
|
|
108
116
|
const nextPreToolUse = [
|
|
109
117
|
...preToolUse,
|
|
110
118
|
{
|
|
@@ -112,7 +120,8 @@ export function installAutoCompactHook(input) {
|
|
|
112
120
|
hooks: [
|
|
113
121
|
{
|
|
114
122
|
type: 'command',
|
|
115
|
-
command: AUTO_COMPACT_HOOK_COMMAND
|
|
123
|
+
command: AUTO_COMPACT_HOOK_COMMAND,
|
|
124
|
+
...(shell !== undefined ? { shell } : {})
|
|
116
125
|
}
|
|
117
126
|
]
|
|
118
127
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* write-gate.js — peaks Write|Edit|MultiEdit PreToolUse path gate.
|
|
4
|
+
*
|
|
5
|
+
* Slice c5-write-hook-exec-form (session 2026-09-10-session-528a63).
|
|
6
|
+
* VERBATIM RELOCATION: the predicate chain below was character-for-character
|
|
7
|
+
* the chain that used to be inlined into `claude-settings-template.ts` as a
|
|
8
|
+
* `node -e "<js>"` one-liner.
|
|
9
|
+
*
|
|
10
|
+
* Slice c5b-write-gate-polarity (same session) NARROWED the decision to match
|
|
11
|
+
* the contract `.claude/HOOKS.md` documents for this handler. The relocated
|
|
12
|
+
* chain read its eight directory names as an EXCLUSION list, so `_runtime`
|
|
13
|
+
* AND every other `.peaks/<slug>/` were allowed; it now allows only paths
|
|
14
|
+
* under `.peaks/_runtime/` and falls through on everything else. That earlier
|
|
15
|
+
* polarity also permitted a top-level `.peaks/<change-id>/` write, which
|
|
16
|
+
* `CLAUDE.md`'s hard ban forbids outright.
|
|
17
|
+
*
|
|
18
|
+
* Why it moved into a file: the inlined form was shell-dialect-coupled. Its
|
|
19
|
+
* escaping contract was defined in terms of bash reducing `\\` to `\` inside a
|
|
20
|
+
* `"..."` wrapper, which PowerShell does NOT do — so the handler could not take
|
|
21
|
+
* the platform `shell` pin its Bash siblings carry. `node <path>` has no inline
|
|
22
|
+
* payload, so there is nothing left to escape and no dialect to couple to.
|
|
23
|
+
*
|
|
24
|
+
* Why `.js` and not `.sh` (the convention for the other hook scripts here):
|
|
25
|
+
* - `.sh` needs `bash`; on Windows that means Git Bash, so it is itself a
|
|
26
|
+
* shell dependency and the escaping problem merely moves to the
|
|
27
|
+
* `bash <script>` boundary.
|
|
28
|
+
* - `node` is already a hard dependency — the previous form invoked it.
|
|
29
|
+
* - A plain `.js` needs no compile step, so the SAME relative filename
|
|
30
|
+
* exists in `src/` (used by `tsx` + vitest) and in `dist/` (copied by
|
|
31
|
+
* `scripts/copy-templates.mjs`), which lets one emitted path string be
|
|
32
|
+
* valid for both the repo and an installed consumer.
|
|
33
|
+
*
|
|
34
|
+
* Contract (`.claude/HOOKS.md`): exit 0 = allow, exit 1 = fall through to the
|
|
35
|
+
* gate — NOT a deny. Only exit 2 blocks a tool call, and this handler never
|
|
36
|
+
* returns it. This handler's job is to stay silent on the paths the gate is
|
|
37
|
+
* meant to skip.
|
|
38
|
+
*
|
|
39
|
+
* Path source: the hook payload arrives as JSON on STDIN (Claude Code's
|
|
40
|
+
* documented channel; it appends no argv). `process.argv[2]` is honoured as a
|
|
41
|
+
* fallback so a positional-arg invocation keeps working.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** First string found at any of the candidate keys of `root`. */
|
|
45
|
+
function pathFrom(root) {
|
|
46
|
+
if (!root || typeof root !== 'object') return '';
|
|
47
|
+
for (const key of ['file_path', 'path', 'notebook_path']) {
|
|
48
|
+
if (typeof root[key] === 'string') return root[key];
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Pull the candidate file path out of a parsed hook payload. */
|
|
54
|
+
function candidatePath(payload) {
|
|
55
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
56
|
+
return pathFrom(payload.tool_input) || pathFrom(payload);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The gate decision: allow (0) only for paths under `.peaks/_runtime/`;
|
|
61
|
+
* everything else falls through to the gate (1).
|
|
62
|
+
*/
|
|
63
|
+
function decide(p) {
|
|
64
|
+
return p.includes('.peaks/_runtime/') ? 0 : 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ARGV_PATH = typeof process.argv[2] === 'string' ? process.argv[2] : '';
|
|
68
|
+
|
|
69
|
+
// No stdin to read (a human running this by hand): decide on argv alone
|
|
70
|
+
// instead of blocking forever waiting for an 'end' that never comes.
|
|
71
|
+
if (process.stdin.isTTY) {
|
|
72
|
+
process.exit(decide(ARGV_PATH));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let raw = '';
|
|
76
|
+
process.stdin.setEncoding('utf8');
|
|
77
|
+
process.stdin.on('data', (chunk) => { raw += chunk; });
|
|
78
|
+
process.stdin.on('end', () => {
|
|
79
|
+
let payload;
|
|
80
|
+
try {
|
|
81
|
+
payload = JSON.parse(raw);
|
|
82
|
+
} catch {
|
|
83
|
+
// Malformed or empty payload → no path → deny, which is the same
|
|
84
|
+
// outcome the old form produced for an absent `process.argv[1]`.
|
|
85
|
+
payload = undefined;
|
|
86
|
+
}
|
|
87
|
+
process.exit(decide(candidatePath(payload) || ARGV_PATH));
|
|
88
|
+
});
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { IdeAdapter } from '../ide-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the absolute path of a Claude Code transcript jsonl by OUTER
|
|
4
|
+
* session id, searching `~/.claude/projects/**` recursively.
|
|
5
|
+
*
|
|
6
|
+
* Exported (slice 2026-09-10-context-audit-and-discipline, Slice A) so
|
|
7
|
+
* `peaks code context-audit` reuses THIS locator instead of re-implementing
|
|
8
|
+
* the recursive find. Returns `null` when the transcript does not exist —
|
|
9
|
+
* callers MUST treat that as "unavailable", never as an error.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveClaudeTranscriptPath(outerSessionId: string, projectsDir?: string): string | null;
|
|
2
12
|
/**
|
|
3
13
|
* Resolve the currently-active Claude Code model id from a runtime env map.
|
|
4
14
|
* Returns the first non-empty `CLAUDE_CODE_MODEL_ENV_VARS` value (trimmed), or
|
|
@@ -90,6 +90,20 @@ function findTranscriptJsonl(projectsDir, outerSessionId) {
|
|
|
90
90
|
}
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the absolute path of a Claude Code transcript jsonl by OUTER
|
|
95
|
+
* session id, searching `~/.claude/projects/**` recursively.
|
|
96
|
+
*
|
|
97
|
+
* Exported (slice 2026-09-10-context-audit-and-discipline, Slice A) so
|
|
98
|
+
* `peaks code context-audit` reuses THIS locator instead of re-implementing
|
|
99
|
+
* the recursive find. Returns `null` when the transcript does not exist —
|
|
100
|
+
* callers MUST treat that as "unavailable", never as an error.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveClaudeTranscriptPath(outerSessionId, projectsDir = join(homedir(), '.claude', 'projects')) {
|
|
103
|
+
if (typeof outerSessionId !== 'string' || outerSessionId.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
return findTranscriptJsonl(projectsDir, outerSessionId);
|
|
106
|
+
}
|
|
93
107
|
/** 1M-context window size in tokens (documented single choice: 1,000,000). */
|
|
94
108
|
const ONE_MILLION_CONTEXT_TOKENS = 1_000_000;
|
|
95
109
|
/** Safe-default (non-1M) context window size in tokens. */
|
|
@@ -483,7 +497,12 @@ export const CLAUDE_CODE_ADAPTER = {
|
|
|
483
497
|
compactCommand: 'claude --compact',
|
|
484
498
|
compactPathway: 'ide-native',
|
|
485
499
|
postCompactDetectCommand: 'peaks code auto-compact --json',
|
|
486
|
-
readContextPercentFallback
|
|
500
|
+
readContextPercentFallback,
|
|
501
|
+
// Slice 2026-09-10-context-audit-and-discipline (Slice A): the vendor
|
|
502
|
+
// layout knowledge (`~/.claude/projects/**/<outerSessionId>.jsonl`)
|
|
503
|
+
// stays here; `peaks code context-audit` resolves it through the
|
|
504
|
+
// adapter registry, never by naming this adapter directly.
|
|
505
|
+
resolveTranscriptPath: (outerSessionId) => resolveClaudeTranscriptPath(outerSessionId),
|
|
487
506
|
},
|
|
488
507
|
// Slice #011: standards profile. Claude Code reads its constitution at
|
|
489
508
|
// CLAUDE.md + module-level rules under .claude/rules/**. The values mirror
|
|
@@ -217,6 +217,21 @@ export interface IdeCompactProfile {
|
|
|
217
217
|
* Added in slice 2026-09-02-vendor-neutral-context-probe.
|
|
218
218
|
*/
|
|
219
219
|
readonly readContextPercentFallback?: (input: ContextPercentFallbackInput) => ContextPercentProbe | null;
|
|
220
|
+
/**
|
|
221
|
+
* Optional locator for the IDE's per-session transcript file (jsonl),
|
|
222
|
+
* keyed by the OUTER (harness) session id. `peaks code context-audit`
|
|
223
|
+
* (slice 2026-09-10-context-audit-and-discipline, Slice A) uses it to
|
|
224
|
+
* group the session's tool results by tool + short input key, so the
|
|
225
|
+
* generic audit service never learns any vendor's on-disk layout.
|
|
226
|
+
*
|
|
227
|
+
* Returns the absolute path, or `null` when the transcript does not
|
|
228
|
+
* exist. Adapters MUST NOT throw on a missing file — the audit treats
|
|
229
|
+
* `null` as `available: false` and continues.
|
|
230
|
+
*
|
|
231
|
+
* Adapters that do not opt in simply omit the field; the audit then
|
|
232
|
+
* reports `transcript-locator-unavailable`.
|
|
233
|
+
*/
|
|
234
|
+
readonly resolveTranscriptPath?: (outerSessionId: string) => string | null;
|
|
220
235
|
}
|
|
221
236
|
/**
|
|
222
237
|
* Input the generic `readContextPercent` reader passes to an adapter's
|
|
@@ -7,4 +7,6 @@ export type EslintDetectResult = {
|
|
|
7
7
|
readonly warnings: readonly string[];
|
|
8
8
|
readonly nextActions: readonly string[];
|
|
9
9
|
};
|
|
10
|
+
/** Named code for "npm itself could not be launched" — distinct from a registry miss. */
|
|
11
|
+
export declare const NPM_PROBE_UNRESOLVED_CODE = "NPM_PROBE_UNRESOLVED";
|
|
10
12
|
export declare function detectEslint(): EslintDetectResult;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* unified Gate B5 verdict.
|
|
5
5
|
*/
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { resolveNpxInvocation } from './npx-resolver.js';
|
|
7
|
+
import { resolveNpmInvocation, resolveNpxInvocation } from './npx-resolver.js';
|
|
8
8
|
import { ESLINT_PACKAGE_PINS } from './eslint-runner.js';
|
|
9
9
|
const PACKAGES_TO_PROBE = [
|
|
10
10
|
'eslint',
|
|
@@ -23,14 +23,23 @@ function probeNpx() {
|
|
|
23
23
|
const probe = spawnSync(command, args, { encoding: 'utf8', env: baseEnv });
|
|
24
24
|
return probe.status === 0;
|
|
25
25
|
}
|
|
26
|
+
/** Named code for "npm itself could not be launched" — distinct from a registry miss. */
|
|
27
|
+
export const NPM_PROBE_UNRESOLVED_CODE = 'NPM_PROBE_UNRESOLVED';
|
|
26
28
|
function probePackage(key) {
|
|
27
29
|
const pkg = packageNameFor(key);
|
|
28
30
|
const pin = ESLINT_PACKAGE_PINS[key];
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// 2026-09-10: `npm` on Windows is an `npm.cmd` shim, which Node >= 20 refuses
|
|
32
|
+
// to spawn at all without `shell: true` — and `shell: true` concatenates the
|
|
33
|
+
// argv unescaped (DEP0190 on every call, and any argument containing a space
|
|
34
|
+
// is split). So the shim is bypassed: `resolveNpmInvocation` resolves npm's
|
|
35
|
+
// own JS entry and this runs it through `process.execPath`. Same shape as the
|
|
36
|
+
// npx probe above and as `eslint-runner.ts`.
|
|
37
|
+
const { command, args, baseEnv } = resolveNpmInvocation(['view', `${pkg}@${pin}`, 'version']);
|
|
38
|
+
const result = spawnSync(command, args, { encoding: 'utf8', env: baseEnv });
|
|
39
|
+
return {
|
|
40
|
+
ok: result.status === 0,
|
|
41
|
+
error: result.error === undefined || result.error === null ? null : result.error.message
|
|
42
|
+
};
|
|
34
43
|
}
|
|
35
44
|
export function detectEslint() {
|
|
36
45
|
const nextActions = [];
|
|
@@ -45,9 +54,14 @@ export function detectEslint() {
|
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
for (const key of PACKAGES_TO_PROBE) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
const probe = probePackage(key);
|
|
58
|
+
if (probe.ok)
|
|
59
|
+
continue;
|
|
60
|
+
const target = `${packageNameFor(key)}@${ESLINT_PACKAGE_PINS[key]}`;
|
|
61
|
+
warnings.push(probe.error === null
|
|
62
|
+
? `npm registry cannot resolve ${target}`
|
|
63
|
+
: `${NPM_PROBE_UNRESOLVED_CODE}: could not launch npm to probe ${target} (${probe.error}). ` +
|
|
64
|
+
'Ensure Node.js >= 20 with its bundled npm is installed.');
|
|
51
65
|
}
|
|
52
66
|
if (warnings.length > 0) {
|
|
53
67
|
nextActions.push('Re-run `peaks code lint --json` after npm connectivity is restored.');
|
|
@@ -4,3 +4,9 @@ export type NpxInvocation = {
|
|
|
4
4
|
readonly baseEnv: NodeJS.ProcessEnv;
|
|
5
5
|
};
|
|
6
6
|
export declare function resolveNpxInvocation(npxArgs: readonly string[]): NpxInvocation;
|
|
7
|
+
/**
|
|
8
|
+
* `npm` sibling of `resolveNpxInvocation`, same shim bypass: `node
|
|
9
|
+
* <npm-cli.js> <args>`. A caller must NOT reach for bare `npm` + `shell: true`
|
|
10
|
+
* instead — the shim is the defect, not the justification.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveNpmInvocation(npmArgs: readonly string[]): NpxInvocation;
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cross-platform `npx` resolver.
|
|
2
|
+
* Cross-platform `npm` / `npx` resolver.
|
|
3
3
|
*
|
|
4
|
-
* On Windows, `npm` installs `npx` as
|
|
5
|
-
* `child_process.spawnSync` refuses to invoke
|
|
6
|
-
* is set, and `shell: true`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* On Windows, `npm` installs `npx` and `npm` as `.cmd` shims; Node's
|
|
5
|
+
* `child_process.spawnSync` refuses to invoke a `.cmd` unless `shell: true`
|
|
6
|
+
* is set, and `shell: true` concatenates (does not escape) the argv — it
|
|
7
|
+
* corrupts quoted `--package` arguments, splits any argument containing a
|
|
8
|
+
* space, and emits DEP0190 on every call. Rather than depend on shell
|
|
9
|
+
* quoting, these helpers resolve the CLI script bundled with the user's
|
|
10
|
+
* `npm` install and invoke it via `node <npm|npx>-cli.js` with the same
|
|
11
|
+
* argv. macOS / Linux continue to use the regular binaries.
|
|
11
12
|
*/
|
|
12
13
|
import { existsSync } from 'node:fs';
|
|
13
14
|
import { join } from 'node:path';
|
|
14
15
|
import process from 'node:process';
|
|
15
|
-
|
|
16
|
+
/** Locate `<npm install>/bin/<scriptFile>`, or `null` when not on disk. */
|
|
17
|
+
function locateNpmCliScript(scriptFile) {
|
|
16
18
|
const candidates = process.platform === 'win32'
|
|
17
19
|
? [
|
|
18
|
-
join(process.execPath, '..', '..', 'node_modules', 'npm', 'bin',
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
join(process.execPath, '..', '..', 'node_modules', 'npm', 'bin', scriptFile),
|
|
21
|
+
`C:/nvm4w/nodejs/node_modules/npm/bin/${scriptFile}`,
|
|
22
|
+
`C:/Program Files/nodejs/node_modules/npm/bin/${scriptFile}`
|
|
21
23
|
]
|
|
22
24
|
: [
|
|
23
|
-
join(process.execPath, '..', '..', 'lib', 'node_modules', 'npm', 'bin',
|
|
25
|
+
join(process.execPath, '..', '..', 'lib', 'node_modules', 'npm', 'bin', scriptFile)
|
|
24
26
|
];
|
|
25
27
|
for (const candidate of candidates) {
|
|
26
28
|
if (existsSync(candidate))
|
|
@@ -30,7 +32,7 @@ function locateNpxCliScript() {
|
|
|
30
32
|
}
|
|
31
33
|
export function resolveNpxInvocation(npxArgs) {
|
|
32
34
|
if (process.platform === 'win32') {
|
|
33
|
-
const cliScript =
|
|
35
|
+
const cliScript = locateNpmCliScript('npx-cli.js');
|
|
34
36
|
if (cliScript !== null) {
|
|
35
37
|
return {
|
|
36
38
|
command: process.execPath,
|
|
@@ -45,3 +47,25 @@ export function resolveNpxInvocation(npxArgs) {
|
|
|
45
47
|
baseEnv: process.env
|
|
46
48
|
};
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* `npm` sibling of `resolveNpxInvocation`, same shim bypass: `node
|
|
52
|
+
* <npm-cli.js> <args>`. A caller must NOT reach for bare `npm` + `shell: true`
|
|
53
|
+
* instead — the shim is the defect, not the justification.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveNpmInvocation(npmArgs) {
|
|
56
|
+
if (process.platform === 'win32') {
|
|
57
|
+
const cliScript = locateNpmCliScript('npm-cli.js');
|
|
58
|
+
if (cliScript !== null) {
|
|
59
|
+
return {
|
|
60
|
+
command: process.execPath,
|
|
61
|
+
args: [cliScript, ...npmArgs],
|
|
62
|
+
baseEnv: process.env
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
command: 'npm',
|
|
68
|
+
args: npmArgs,
|
|
69
|
+
baseEnv: process.env
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -55,6 +55,11 @@ export declare function resolveMemoryKind(content: string): MemoryKindResolution
|
|
|
55
55
|
* `parseStoredMemoryFile` (read path) and the reindex / ingest / doctor
|
|
56
56
|
* classifiers consume this so there is exactly one kind-resolution rule
|
|
57
57
|
* in the codebase.
|
|
58
|
+
*
|
|
59
|
+
* Tolerates a leading run of HTML-comment lines (e.g. the
|
|
60
|
+
* `<!-- peaks-memory:start -->` sediment marker) before the opening `---`
|
|
61
|
+
* fence. The closing fence is still required and body extraction is
|
|
62
|
+
* unchanged: the body is the text after the closing fence.
|
|
58
63
|
*/
|
|
59
64
|
export declare function parseMemoryFrontmatter(content: string): ParsedMemoryFrontmatter;
|
|
60
65
|
/** Which frontmatter field (or the filename) supplied a memory's name. */
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
//
|
|
11
11
|
// 2. `parseStoredMemoryFile` — read-path side. Files in `.peaks/memory/`
|
|
12
12
|
// are stored as standard YAML frontmatter (name / description /
|
|
13
|
-
// metadata.type / metadata.sourceArtifact) followed by the body.
|
|
13
|
+
// metadata.type / metadata.sourceArtifact) followed by the body. A file
|
|
14
|
+
// may also open with HTML-comment lines (the `<!-- peaks-memory:start -->`
|
|
15
|
+
// sediment marker) before the frontmatter; `parseMemoryFrontmatter`
|
|
16
|
+
// skips that leading comment run before looking for the `---` fence.
|
|
14
17
|
//
|
|
15
18
|
// Both parsers share the `VALID_MEMORY_KINDS` allow-list (derived from the
|
|
16
19
|
// canonical `PROJECT_MEMORY_KINDS` tuple in `../types.ts`) and the
|
|
@@ -83,23 +86,70 @@ export function resolveMemoryKind(content) {
|
|
|
83
86
|
const parsed = parseMemoryFrontmatter(content);
|
|
84
87
|
return parsed.kind;
|
|
85
88
|
}
|
|
89
|
+
/** Start of an HTML comment line, allowing leading horizontal whitespace. */
|
|
90
|
+
const LEADING_COMMENT_OPEN = /^[ \t]*<!--/;
|
|
91
|
+
/**
|
|
92
|
+
* Length of the leading run of blank lines and standalone HTML-comment lines.
|
|
93
|
+
*
|
|
94
|
+
* A stored memory may be written with the documented sediment marker
|
|
95
|
+
* (`<!-- peaks-memory:start -->`) — or any HTML comment — BEFORE its YAML
|
|
96
|
+
* frontmatter. This helper reports how much of the file to skip so the fence
|
|
97
|
+
* can still be found. At least one comment line must be present: a file that
|
|
98
|
+
* merely starts with blank lines is not treated as marker-prefixed, so the
|
|
99
|
+
* pre-existing (fence-at-byte-0) behaviour is preserved exactly.
|
|
100
|
+
*/
|
|
101
|
+
function leadingCommentPrefixLength(normalized) {
|
|
102
|
+
let offset = 0;
|
|
103
|
+
let sawComment = false;
|
|
104
|
+
for (;;) {
|
|
105
|
+
const rest = normalized.slice(offset);
|
|
106
|
+
const blank = /^[ \t]*\n/.exec(rest);
|
|
107
|
+
if (blank !== null) {
|
|
108
|
+
offset += blank[0].length;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const open = LEADING_COMMENT_OPEN.exec(rest);
|
|
112
|
+
if (open === null)
|
|
113
|
+
break;
|
|
114
|
+
const closeIndex = rest.indexOf('-->', open[0].length);
|
|
115
|
+
if (closeIndex < 0)
|
|
116
|
+
break;
|
|
117
|
+
const afterClose = closeIndex + '-->'.length;
|
|
118
|
+
const lineEnd = rest.indexOf('\n', afterClose);
|
|
119
|
+
if (lineEnd < 0)
|
|
120
|
+
break;
|
|
121
|
+
// Only a whole comment line counts; trailing prose after `-->` means the
|
|
122
|
+
// file does not open with a comment block.
|
|
123
|
+
if (rest.slice(afterClose, lineEnd).trim() !== '')
|
|
124
|
+
break;
|
|
125
|
+
offset += lineEnd + 1;
|
|
126
|
+
sawComment = true;
|
|
127
|
+
}
|
|
128
|
+
return sawComment ? offset : 0;
|
|
129
|
+
}
|
|
86
130
|
/**
|
|
87
131
|
* Single parse surface for stored memory frontmatter. Both
|
|
88
132
|
* `parseStoredMemoryFile` (read path) and the reindex / ingest / doctor
|
|
89
133
|
* classifiers consume this so there is exactly one kind-resolution rule
|
|
90
134
|
* in the codebase.
|
|
135
|
+
*
|
|
136
|
+
* Tolerates a leading run of HTML-comment lines (e.g. the
|
|
137
|
+
* `<!-- peaks-memory:start -->` sediment marker) before the opening `---`
|
|
138
|
+
* fence. The closing fence is still required and body extraction is
|
|
139
|
+
* unchanged: the body is the text after the closing fence.
|
|
91
140
|
*/
|
|
92
141
|
export function parseMemoryFrontmatter(content) {
|
|
93
142
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
94
|
-
|
|
143
|
+
const head = normalized.slice(leadingCommentPrefixLength(normalized));
|
|
144
|
+
if (!head.startsWith('---\n')) {
|
|
95
145
|
return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
|
|
96
146
|
}
|
|
97
|
-
const endIndex =
|
|
147
|
+
const endIndex = head.indexOf('\n---\n', 4);
|
|
98
148
|
if (endIndex < 0) {
|
|
99
149
|
return { hasFrontmatter: false, kind: { kind: null, source: 'none', rawKind: null }, frontmatter: '', body: normalized.trim() };
|
|
100
150
|
}
|
|
101
|
-
const frontmatter =
|
|
102
|
-
const body =
|
|
151
|
+
const frontmatter = head.slice(4, endIndex);
|
|
152
|
+
const body = head.slice(endIndex + '\n---\n'.length).trim();
|
|
103
153
|
let name;
|
|
104
154
|
let titleField;
|
|
105
155
|
let description;
|