peaks-cli 1.2.8 → 1.3.0
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/README.md +12 -0
- package/dist/src/cli/commands/project-commands.js +1 -1
- package/dist/src/cli/commands/scan-commands.js +22 -0
- package/dist/src/cli/commands/workspace-commands.js +59 -1
- package/dist/src/services/memory/project-memory-service.d.ts +1 -1
- package/dist/src/services/memory/project-memory-service.js +52 -23
- package/dist/src/services/sc/sc-service.d.ts +52 -1
- package/dist/src/services/sc/sc-service.js +266 -17
- package/dist/src/services/scan/libraries-service.d.ts +24 -0
- package/dist/src/services/scan/libraries-service.js +419 -0
- package/dist/src/services/scan/libraries-types.d.ts +59 -0
- package/dist/src/services/scan/libraries-types.js +9 -0
- package/dist/src/services/session/session-manager.d.ts +7 -5
- package/dist/src/services/session/session-manager.js +48 -14
- package/dist/src/services/skills/skill-presence-service.js +102 -68
- package/dist/src/services/skills/skill-runbook-service.js +36 -2
- package/dist/src/services/skills/skill-statusline-service.js +13 -7
- package/dist/src/services/workflow/autonomous-resume-writer.js +7 -7
- package/dist/src/services/workspace/reconcile-service.d.ts +119 -0
- package/dist/src/services/workspace/reconcile-service.js +464 -0
- package/dist/src/services/workspace/reconcile-types.d.ts +93 -0
- package/dist/src/services/workspace/reconcile-types.js +13 -0
- package/dist/src/shared/change-id.d.ts +30 -0
- package/dist/src/shared/change-id.js +40 -6
- package/dist/src/shared/paths.d.ts +1 -1
- package/dist/src/shared/paths.js +2 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +4 -1
- package/schemas/library-breaking-changes.data.json +141 -0
- package/schemas/library-breaking-changes.meta.json +6 -0
- package/schemas/library-breaking-changes.schema.json +50 -0
- package/skills/peaks-qa/SKILL.md +12 -0
- package/skills/peaks-rd/SKILL.md +145 -2
- package/skills/peaks-solo/SKILL.md +93 -319
- package/skills/peaks-solo/references/runbook.md +168 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +177 -0
- package/skills/peaks-solo-resume/SKILL.md +81 -0
- package/skills/peaks-solo-status/SKILL.md +120 -0
- package/skills/peaks-solo-test/SKILL.md +84 -0
- package/skills/peaks-txt/SKILL.md +8 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { findProjectRoot } from '../config/config-safety.js';
|
|
4
4
|
import { ensureMemoryBootstrap } from '../memory/project-memory-service.js';
|
|
5
5
|
import { getSessionMeta } from '../session/session-manager.js';
|
|
@@ -31,8 +31,16 @@ function getCurrentOuterSessionId() {
|
|
|
31
31
|
return claude;
|
|
32
32
|
return undefined;
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// As of slice 2026-06-05-peaks-runtime-layer the orchestrator's
|
|
35
|
+
// active-skill marker lives under `.peaks/_runtime/active-skill.json`.
|
|
36
|
+
// The legacy `.peaks/.active-skill.json` path is preserved as a
|
|
37
|
+
// read-only fallback for one minor release so older CLI versions (or
|
|
38
|
+
// trees that have not been migrated by `peaks workspace reconcile`)
|
|
39
|
+
// keep working without a forced re-init.
|
|
40
|
+
const PRESENCE_FILE = join('.peaks', '_runtime', 'active-skill.json');
|
|
41
|
+
const PRESENCE_FILE_LEGACY = '.peaks/.active-skill.json';
|
|
42
|
+
const SESSION_FILE = join('.peaks', '_runtime', 'session.json');
|
|
43
|
+
const SESSION_FILE_LEGACY = '.peaks/.session.json';
|
|
36
44
|
function resolveProjectRoot(override) {
|
|
37
45
|
if (override)
|
|
38
46
|
return resolve(override);
|
|
@@ -41,12 +49,44 @@ function resolveProjectRoot(override) {
|
|
|
41
49
|
function resolvePresencePath(projectRootOverride) {
|
|
42
50
|
return resolve(resolveProjectRoot(projectRootOverride), PRESENCE_FILE);
|
|
43
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Back-compat read for the active-skill marker. Prefers the new
|
|
54
|
+
* canonical `.peaks/_runtime/active-skill.json`; falls back to the
|
|
55
|
+
* legacy `.peaks/.active-skill.json` for one minor release.
|
|
56
|
+
*
|
|
57
|
+
* Returns the parsed SkillPresence object, or null when neither
|
|
58
|
+
* file is present / valid. The legacy file is never written by
|
|
59
|
+
* current code — only the new path receives writes.
|
|
60
|
+
*/
|
|
61
|
+
function readSkillPresenceBackCompat(projectRootOverride) {
|
|
62
|
+
const presencePath = resolvePresencePath(projectRootOverride);
|
|
63
|
+
const legacyPath = resolve(resolveProjectRoot(projectRootOverride), PRESENCE_FILE_LEGACY);
|
|
64
|
+
const pathToRead = existsSync(presencePath) ? presencePath : legacyPath;
|
|
65
|
+
if (!existsSync(pathToRead))
|
|
66
|
+
return null;
|
|
67
|
+
try {
|
|
68
|
+
const raw = readFileSync(pathToRead, 'utf8');
|
|
69
|
+
const parsed = JSON.parse(raw);
|
|
70
|
+
if (typeof parsed?.skill !== 'string' || parsed.skill.length === 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return { presence: parsed, path: pathToRead };
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
44
79
|
function getCurrentSessionId(projectRootOverride) {
|
|
45
|
-
const
|
|
46
|
-
|
|
80
|
+
const projectRoot = resolveProjectRoot(projectRootOverride);
|
|
81
|
+
const sessionPath = resolve(projectRoot, SESSION_FILE);
|
|
82
|
+
const legacyPath = resolve(projectRoot, SESSION_FILE_LEGACY);
|
|
83
|
+
// Back-compat window: prefer the new canonical path; fall back to the
|
|
84
|
+
// legacy `.peaks/.session.json` for one minor release.
|
|
85
|
+
const pathToRead = existsSync(sessionPath) ? sessionPath : legacyPath;
|
|
86
|
+
if (!existsSync(pathToRead))
|
|
47
87
|
return null;
|
|
48
88
|
try {
|
|
49
|
-
const data = JSON.parse(readFileSync(
|
|
89
|
+
const data = JSON.parse(readFileSync(pathToRead, 'utf8'));
|
|
50
90
|
return typeof data.sessionId === 'string' && data.sessionId.length > 0
|
|
51
91
|
? data.sessionId
|
|
52
92
|
: null;
|
|
@@ -80,7 +120,7 @@ function getBoundOuterSessionId(projectRootOverride) {
|
|
|
80
120
|
}
|
|
81
121
|
/**
|
|
82
122
|
* Snapshot of the previous peaks session's outer session id, read
|
|
83
|
-
* straight off
|
|
123
|
+
* straight off the active-skill marker *before* we overwrite it.
|
|
84
124
|
* Used to detect "the LLM just opened a fresh outer session" — if
|
|
85
125
|
* the previously-recorded outer session id differs from the one we
|
|
86
126
|
* are about to stamp, the user probably closed the previous outer
|
|
@@ -93,27 +133,24 @@ function getBoundOuterSessionId(projectRootOverride) {
|
|
|
93
133
|
* (most common when the swap is a no-op reconnect) or to roll a
|
|
94
134
|
* fresh session (when the new outer session is genuinely a new
|
|
95
135
|
* task).
|
|
136
|
+
*
|
|
137
|
+
* Reads from `.peaks/_runtime/active-skill.json` first; falls back to
|
|
138
|
+
* the legacy `.peaks/.active-skill.json` for one minor release.
|
|
96
139
|
*/
|
|
97
140
|
function getPreviousOuterSessionId(projectRootOverride) {
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
return undefined;
|
|
101
|
-
try {
|
|
102
|
-
const raw = readFileSync(presencePath, 'utf8');
|
|
103
|
-
const parsed = JSON.parse(raw);
|
|
104
|
-
if (typeof parsed.outerSessionId === 'string' && parsed.outerSessionId.length > 0) {
|
|
105
|
-
return parsed.outerSessionId;
|
|
106
|
-
}
|
|
107
|
-
// Legacy field name. Honour it on the read side so v1.2.x
|
|
108
|
-
// presence files do not show as a false mismatch.
|
|
109
|
-
if (typeof parsed.claudeSessionId === 'string' && parsed.claudeSessionId.length > 0) {
|
|
110
|
-
return parsed.claudeSessionId;
|
|
111
|
-
}
|
|
141
|
+
const result = readSkillPresenceBackCompat(projectRootOverride);
|
|
142
|
+
if (result === null)
|
|
112
143
|
return undefined;
|
|
144
|
+
const parsed = result.presence;
|
|
145
|
+
if (typeof parsed.outerSessionId === 'string' && parsed.outerSessionId.length > 0) {
|
|
146
|
+
return parsed.outerSessionId;
|
|
113
147
|
}
|
|
114
|
-
|
|
115
|
-
|
|
148
|
+
// Legacy field name. Honour it on the read side so v1.2.x
|
|
149
|
+
// presence files do not show as a false mismatch.
|
|
150
|
+
if (typeof parsed.claudeSessionId === 'string' && parsed.claudeSessionId.length > 0) {
|
|
151
|
+
return parsed.claudeSessionId;
|
|
116
152
|
}
|
|
153
|
+
return undefined;
|
|
117
154
|
}
|
|
118
155
|
export function exportSkillPresence(projectRootOverride) {
|
|
119
156
|
return resolvePresencePath(projectRootOverride);
|
|
@@ -184,65 +221,62 @@ export function setSkillPresence(skill, mode, gate, projectRootOverride) {
|
|
|
184
221
|
return presence;
|
|
185
222
|
}
|
|
186
223
|
export function getSkillPresence(projectRootOverride) {
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
224
|
+
const result = readSkillPresenceBackCompat(projectRootOverride);
|
|
225
|
+
if (result === null)
|
|
189
226
|
return null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
if (typeof parsed.sessionId === 'string' && parsed.sessionId.length > 0) {
|
|
198
|
-
const currentSessionId = getCurrentSessionId(projectRootOverride);
|
|
199
|
-
if (currentSessionId && parsed.sessionId !== currentSessionId) {
|
|
227
|
+
const { presence, path: presencePath } = result;
|
|
228
|
+
if (typeof presence.sessionId === 'string' && presence.sessionId.length > 0) {
|
|
229
|
+
const currentSessionId = getCurrentSessionId(projectRootOverride);
|
|
230
|
+
if (currentSessionId && presence.sessionId !== currentSessionId) {
|
|
231
|
+
try {
|
|
200
232
|
unlinkSync(presencePath);
|
|
201
|
-
return null;
|
|
202
233
|
}
|
|
234
|
+
catch {
|
|
235
|
+
// best effort
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
203
238
|
}
|
|
204
|
-
return parsed;
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
return null;
|
|
208
239
|
}
|
|
240
|
+
return presence;
|
|
209
241
|
}
|
|
210
242
|
export function touchSkillHeartbeat(projectRootOverride) {
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
243
|
+
const result = readSkillPresenceBackCompat(projectRootOverride);
|
|
244
|
+
if (result === null)
|
|
213
245
|
return null;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
if (typeof parsed.sessionId === 'string' && parsed.sessionId.length > 0) {
|
|
222
|
-
const currentSessionId = getCurrentSessionId(projectRootOverride);
|
|
223
|
-
if (currentSessionId && parsed.sessionId !== currentSessionId) {
|
|
246
|
+
const { presence, path: presencePath } = result;
|
|
247
|
+
if (typeof presence.sessionId === 'string' && presence.sessionId.length > 0) {
|
|
248
|
+
const currentSessionId = getCurrentSessionId(projectRootOverride);
|
|
249
|
+
if (currentSessionId && presence.sessionId !== currentSessionId) {
|
|
250
|
+
try {
|
|
224
251
|
unlinkSync(presencePath);
|
|
225
|
-
return null;
|
|
226
252
|
}
|
|
253
|
+
catch {
|
|
254
|
+
// best effort
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
227
257
|
}
|
|
228
|
-
parsed.lastHeartbeat = new Date().toISOString();
|
|
229
|
-
writeFileSync(presencePath, JSON.stringify(parsed, null, 2), 'utf8');
|
|
230
|
-
return parsed;
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
return null;
|
|
234
258
|
}
|
|
259
|
+
presence.lastHeartbeat = new Date().toISOString();
|
|
260
|
+
writeFileSync(presencePath, JSON.stringify(presence, null, 2), 'utf8');
|
|
261
|
+
return presence;
|
|
235
262
|
}
|
|
236
263
|
export function clearSkillPresence(projectRootOverride) {
|
|
264
|
+
// Clear both the new canonical path and the legacy path, so a stale
|
|
265
|
+
// presence marker from a prior CLI version cannot resurrect after
|
|
266
|
+
// a fresh `clear`.
|
|
237
267
|
const presencePath = resolvePresencePath(projectRootOverride);
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
268
|
+
const legacyPath = resolve(resolveProjectRoot(projectRootOverride), PRESENCE_FILE_LEGACY);
|
|
269
|
+
let cleared = false;
|
|
270
|
+
for (const p of [presencePath, legacyPath]) {
|
|
271
|
+
if (!existsSync(p))
|
|
272
|
+
continue;
|
|
273
|
+
try {
|
|
274
|
+
unlinkSync(p);
|
|
275
|
+
cleared = true;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// best effort
|
|
279
|
+
}
|
|
247
280
|
}
|
|
281
|
+
return cleared;
|
|
248
282
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
1
2
|
import { readText } from '../../shared/fs.js';
|
|
2
3
|
import { loadSkillRegistry } from './skill-registry.js';
|
|
3
4
|
const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
@@ -5,7 +6,8 @@ const DESTRUCTIVE_APPLY_PATTERNS = [
|
|
|
5
6
|
/peaks\s+memory\s+extract[^\n]*--apply/,
|
|
6
7
|
/peaks\s+artifacts\s+sync[^\n]*--apply/,
|
|
7
8
|
/peaks\s+openspec\s+archive[^\n]*--apply/,
|
|
8
|
-
/peaks\s+standards\s+(?:init|update)[^\n]*--apply
|
|
9
|
+
/peaks\s+standards\s+(?:init|update)[^\n]*--apply/,
|
|
10
|
+
/peaks\s+workspace\s+reconcile[^\n]*--apply/
|
|
9
11
|
];
|
|
10
12
|
const AUTHORIZATION_KEYWORDS_PATTERN = /authoriz|explicit|--dry-run|approv|only after|only when/i;
|
|
11
13
|
const PEAKS_COMMAND_LINE_PATTERN = /^\s*peaks\s+\w/;
|
|
@@ -13,6 +15,38 @@ function extractRunbookSection(body) {
|
|
|
13
15
|
const match = /## Default runbook\n+([\s\S]*?)(?=\n## |$)/.exec(body);
|
|
14
16
|
return match === null ? null : match[1];
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Load the runbook section, falling back to `references/runbook.md` if the
|
|
20
|
+
* SKILL.md only has a pointer section. This supports skills (notably
|
|
21
|
+
* `peaks-solo`) that extracted their 150-line bash runbook to a sibling
|
|
22
|
+
* reference to keep SKILL.md under the 800-line cap. The CLI
|
|
23
|
+
* `peaks skill runbook` command uses the same fallback so a human
|
|
24
|
+
* reviewer sees the full runbook regardless of where it lives.
|
|
25
|
+
*
|
|
26
|
+
* Strategy: prefer the LONGER of the two sections. A short pointer section
|
|
27
|
+
* in SKILL.md (~ 1-2 lines) is treated as a "this runbook is in the
|
|
28
|
+
* reference" marker; a long inline section (>= the reference length) is
|
|
29
|
+
* treated as the canonical runbook. This avoids the false positive where
|
|
30
|
+
* the pointer section's regex match returns a non-null but content-poor
|
|
31
|
+
* string.
|
|
32
|
+
*/
|
|
33
|
+
async function loadRunbookSection(skillPath, body) {
|
|
34
|
+
const inline = extractRunbookSection(body);
|
|
35
|
+
const refPath = join(dirname(skillPath), 'references', 'runbook.md');
|
|
36
|
+
let refSection = null;
|
|
37
|
+
try {
|
|
38
|
+
const refBody = await readText(refPath);
|
|
39
|
+
refSection = extractRunbookSection(refBody);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// reference file does not exist or is not readable
|
|
43
|
+
}
|
|
44
|
+
if (inline === null)
|
|
45
|
+
return refSection;
|
|
46
|
+
if (refSection === null)
|
|
47
|
+
return inline;
|
|
48
|
+
return inline.length >= refSection.length ? inline : refSection;
|
|
49
|
+
}
|
|
16
50
|
function findDestructiveApplyLines(section) {
|
|
17
51
|
const lines = section.split(/\r?\n/);
|
|
18
52
|
return lines.filter((line) => DESTRUCTIVE_APPLY_PATTERNS.some((pattern) => pattern.test(line)));
|
|
@@ -30,7 +64,7 @@ export async function inspectSkillRunbook(name, baseDir) {
|
|
|
30
64
|
throw new Error(`Skill "${name}" not found under skills directory`);
|
|
31
65
|
}
|
|
32
66
|
const body = await readText(skill.skillPath);
|
|
33
|
-
const section =
|
|
67
|
+
const section = await loadRunbookSection(skill.skillPath, body);
|
|
34
68
|
if (section === null) {
|
|
35
69
|
return {
|
|
36
70
|
name: skill.name,
|
|
@@ -6,16 +6,19 @@ import { findProjectRoot } from '../config/config-safety.js';
|
|
|
6
6
|
*
|
|
7
7
|
* Claude Code invokes the configured statusLine command on every turn and pipes
|
|
8
8
|
* a JSON session payload on stdin. This renderer reads the durable presence file
|
|
9
|
-
* (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* (`.peaks/_runtime/active-skill.json`, with a one-minor-release back-compat
|
|
10
|
+
* fallback to `.peaks/.active-skill.json`) and prints a single line that
|
|
11
|
+
* Claude Code paints at the bottom of the terminal. Because it is rendered
|
|
12
|
+
* by the harness — not emitted as LLM tokens — the signal cannot be forgotten
|
|
13
|
+
* by the model, cannot be confused with normal output, and survives context
|
|
14
|
+
* compaction.
|
|
13
15
|
*
|
|
14
16
|
* This module is intentionally READ-ONLY. Unlike getSkillPresence in
|
|
15
17
|
* skill-presence-service.ts, it never deletes or rewrites the presence file:
|
|
16
18
|
* the statusLine runs on every turn and must have zero side effects.
|
|
17
19
|
*/
|
|
18
|
-
const PRESENCE_FILE = '.peaks
|
|
20
|
+
const PRESENCE_FILE = '.peaks/_runtime/active-skill.json';
|
|
21
|
+
const PRESENCE_FILE_LEGACY = '.peaks/.active-skill.json';
|
|
19
22
|
const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
|
|
20
23
|
function resolveCwdFromStdin(stdin) {
|
|
21
24
|
const fromWorkspace = stdin?.workspace?.current_dir ?? stdin?.workspace?.project_dir;
|
|
@@ -48,11 +51,14 @@ export function parseStatusLineStdin(raw) {
|
|
|
48
51
|
*/
|
|
49
52
|
function readPresenceReadOnly(projectRoot) {
|
|
50
53
|
const presencePath = resolve(projectRoot, PRESENCE_FILE);
|
|
51
|
-
|
|
54
|
+
// Back-compat: prefer the new canonical path; fall back to the legacy
|
|
55
|
+
// `.peaks/.active-skill.json` for one minor release.
|
|
56
|
+
const pathToRead = existsSync(presencePath) ? presencePath : resolve(projectRoot, PRESENCE_FILE_LEGACY);
|
|
57
|
+
if (!existsSync(pathToRead)) {
|
|
52
58
|
return { presence: null, invalid: false };
|
|
53
59
|
}
|
|
54
60
|
try {
|
|
55
|
-
const parsed = JSON.parse(readFileSync(
|
|
61
|
+
const parsed = JSON.parse(readFileSync(pathToRead, 'utf8'));
|
|
56
62
|
if (!parsed || typeof parsed !== 'object') {
|
|
57
63
|
return { presence: null, invalid: true };
|
|
58
64
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { buildArtifactRelativePathInRoot, validateChangeIdOrThrow } from '../../shared/change-id.js';
|
|
4
4
|
import { pathExists } from '../../shared/fs.js';
|
|
5
5
|
function defaultClock() {
|
|
6
6
|
return new Date().toISOString();
|
|
@@ -109,27 +109,27 @@ Next actions:
|
|
|
109
109
|
function buildFiles(changeId, goal, createdAt, artifactWorkspacePath) {
|
|
110
110
|
return [
|
|
111
111
|
{
|
|
112
|
-
path: join(artifactWorkspacePath,
|
|
112
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'prd', 'autonomous-goal-package.json')),
|
|
113
113
|
content: renderGoalPackage(changeId, goal)
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
|
-
path: join(artifactWorkspacePath,
|
|
116
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'autonomous-rd-plan.json')),
|
|
117
117
|
content: renderRdPlan(changeId)
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
|
-
path: join(artifactWorkspacePath,
|
|
120
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'checkpoints', 'checkpoint-1.json')),
|
|
121
121
|
content: renderCheckpoint(changeId, createdAt)
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
|
-
path: join(artifactWorkspacePath,
|
|
124
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'evidence', 'unit-tests.md')),
|
|
125
125
|
content: renderUnitTestsEvidence(changeId)
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
|
-
path: join(artifactWorkspacePath,
|
|
128
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'evidence', 'validation-report.md')),
|
|
129
129
|
content: renderValidationReport(changeId)
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
|
-
path: join(artifactWorkspacePath,
|
|
132
|
+
path: join(artifactWorkspacePath, buildArtifactRelativePathInRoot(artifactWorkspacePath, changeId, 'rd', 'swarm', 'resume-instructions.md')),
|
|
133
133
|
content: renderResumeInstructions(changeId)
|
|
134
134
|
}
|
|
135
135
|
];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile service for `.peaks/2026-MM-DD-session-<6hex>/` directories.
|
|
3
|
+
*
|
|
4
|
+
* Reconcile scans the project root's `.peaks/` directory, identifies a
|
|
5
|
+
* canonical session via a 4-tier heuristic, re-points
|
|
6
|
+
* `.peaks/_runtime/session.json` (the canonical new home of the
|
|
7
|
+
* binding; legacy `.peaks/.session.json` is read-only back-compat),
|
|
8
|
+
* and (optionally, with apply === true) deletes empty / abandoned
|
|
9
|
+
* session dirs older than olderThanMs.
|
|
10
|
+
*
|
|
11
|
+
* As of slice 2026-06-05-peaks-runtime-layer the top-level orchestrator
|
|
12
|
+
* also runs `migrateOldRuntimeState` at the start so pre-migration
|
|
13
|
+
* trees have their `.peaks/.session.json` / `.peaks/.active-skill.json`
|
|
14
|
+
* / `.peaks/sop-state/` files moved into `.peaks/_runtime/`.
|
|
15
|
+
*
|
|
16
|
+
* Pure hand-rolled; uses only node:fs, node:path, and the existing
|
|
17
|
+
* session-manager helper for writing the binding. No new dependencies.
|
|
18
|
+
*/
|
|
19
|
+
import type { ReconcileOptions, ReconcileResult, SessionEntry } from './reconcile-types.js';
|
|
20
|
+
/**
|
|
21
|
+
* Walk the project root's `.peaks/` directory and return an entry per
|
|
22
|
+
* session dir matching the standard naming pattern, sorted by name
|
|
23
|
+
* ascending (the most recent is last by sort order, since the date
|
|
24
|
+
* prefix dominates the lexicographic order).
|
|
25
|
+
*
|
|
26
|
+
* Each entry's `lastActivity` is the mtime of the inner `session.json`
|
|
27
|
+
* file, or null if that file is missing. `artifactCount` is the count
|
|
28
|
+
* of files under the dir excluding `session.json` itself.
|
|
29
|
+
*/
|
|
30
|
+
export declare function discoverSessions(projectRoot: string): SessionEntry[];
|
|
31
|
+
/**
|
|
32
|
+
* 4-tier canonical selection. Tiers evaluated in order; first one that
|
|
33
|
+
* yields a session id wins.
|
|
34
|
+
*
|
|
35
|
+
* 1. active-skill sessionId, if it matches a real entry
|
|
36
|
+
* 2. entry with the most recent `session.json` mtime
|
|
37
|
+
* 3. entry with the most recent mtime of any file inside it
|
|
38
|
+
* 4. entry whose dir name sorts last lexicographically
|
|
39
|
+
*/
|
|
40
|
+
export declare function pickCanonicalSession(entries: SessionEntry[], activeSkillSessionId: string | null): {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
source: ReconcileResult['canonicalSource'];
|
|
43
|
+
} | null;
|
|
44
|
+
/**
|
|
45
|
+
* Write `.peaks/.session.json` to bind the project to `canonicalSessionId`.
|
|
46
|
+
* Preserves the projectRoot. The previous binding is returned so the CLI
|
|
47
|
+
* can surface the re-point delta.
|
|
48
|
+
*/
|
|
49
|
+
export declare function repointSessionJson(projectRoot: string, canonicalSessionId: string, repointedFrom: string | null): {
|
|
50
|
+
repointedFrom: string | null;
|
|
51
|
+
repointedTo: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Identify deletion candidates. A session is a candidate when:
|
|
55
|
+
* - the resolved `lastActivity` is older than `ageThresholdMs`, AND
|
|
56
|
+
* - the dir is "empty or auto-only" (artifactCount === 0, OR the
|
|
57
|
+
* only file is `session.json` which is auto-generated).
|
|
58
|
+
*
|
|
59
|
+
* If `lastActivity` is null (no `session.json` inside), the session's
|
|
60
|
+
* own dir mtime is used as a fallback so empty dirs without inner
|
|
61
|
+
* metadata are still fair-game.
|
|
62
|
+
*/
|
|
63
|
+
export declare function findDeletionCandidates(entries: SessionEntry[], ageThresholdMs: number): SessionEntry[];
|
|
64
|
+
/**
|
|
65
|
+
* Apply or report deletion of the given candidates. When `apply` is
|
|
66
|
+
* false, just return `wouldDelete` and do not touch disk. When `apply`
|
|
67
|
+
* is true, actually `rm -rf` each dir and accumulate any per-dir
|
|
68
|
+
* errors in the result.
|
|
69
|
+
*/
|
|
70
|
+
export declare function applyDeletions(candidates: SessionEntry[], apply: boolean): {
|
|
71
|
+
deleted: string[];
|
|
72
|
+
wouldDelete: string[];
|
|
73
|
+
errors: Array<{
|
|
74
|
+
sessionId: string;
|
|
75
|
+
message: string;
|
|
76
|
+
}>;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* One-time migration step (added in slice 2026-06-05-peaks-runtime-layer).
|
|
80
|
+
*
|
|
81
|
+
* Move the legacy runtime files at:
|
|
82
|
+
* - `.peaks/.session.json`
|
|
83
|
+
* - `.peaks/.active-skill.json`
|
|
84
|
+
* - `.peaks/sop-state/`
|
|
85
|
+
* into their new canonical home at:
|
|
86
|
+
* - `.peaks/_runtime/session.json`
|
|
87
|
+
* - `.peaks/_runtime/active-skill.json`
|
|
88
|
+
* - `.peaks/_runtime/sop-state/`
|
|
89
|
+
*
|
|
90
|
+
* Behavior:
|
|
91
|
+
* - Idempotent: re-running on a tree that is already on the new
|
|
92
|
+
* layout produces `migratedFiles: []`.
|
|
93
|
+
* - Best-effort: uses `fs.renameSync` (atomic on POSIX, best-effort
|
|
94
|
+
* on Windows) and falls back to `copyFileSync` + `unlinkSync` if
|
|
95
|
+
* rename throws (e.g. cross-device move on Windows). Errors are
|
|
96
|
+
* collected per file and returned in the `errors` array so the
|
|
97
|
+
* reconcile envelope can surface them without blocking the rest of
|
|
98
|
+
* the migration.
|
|
99
|
+
* - Creates `.peaks/_runtime/` on demand if any of the old paths
|
|
100
|
+
* are present.
|
|
101
|
+
*
|
|
102
|
+
* @returns `{ migratedFiles, errors }`. `migratedFiles` lists the
|
|
103
|
+
* *old* relative paths (e.g. `.peaks/.session.json`) that were
|
|
104
|
+
* successfully moved, in move order. `errors` lists per-file
|
|
105
|
+
* failures with the old path and a human-readable message.
|
|
106
|
+
*/
|
|
107
|
+
export declare function migrateOldRuntimeState(projectRoot: string): {
|
|
108
|
+
migratedFiles: string[];
|
|
109
|
+
errors: Array<{
|
|
110
|
+
path: string;
|
|
111
|
+
message: string;
|
|
112
|
+
}>;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Top-level orchestrator. Wires migration (added in slice
|
|
116
|
+
* 2026-06-05-peaks-runtime-layer), discovery, canonical pick, re-point,
|
|
117
|
+
* deletion-candidate selection, and deletion into a single result.
|
|
118
|
+
*/
|
|
119
|
+
export declare function reconcileWorkspace(options: ReconcileOptions): ReconcileResult;
|