peaks-loop 4.0.0-beta.10 → 4.0.0-beta.11

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.
Files changed (65) hide show
  1. package/CHANGELOG.md +18 -11
  2. package/dist/cli/cli-helpers.d.ts +0 -5
  3. package/dist/cli/cli-helpers.js +0 -24
  4. package/dist/cli/commands/audit-commands.js +1 -1
  5. package/dist/cli/commands/capability-worker-config-sc-commands.js +0 -2
  6. package/dist/cli/commands/code-commands.js +1 -1
  7. package/dist/cli/commands/code-review-commands.js +1 -1
  8. package/dist/cli/commands/config-commands.js +3 -74
  9. package/dist/cli/commands/core/skill-command.js +5 -2
  10. package/dist/cli/commands/dispatch-commands.js +14 -0
  11. package/dist/cli/commands/ecc-commands.d.ts +30 -0
  12. package/dist/cli/commands/ecc-commands.js +119 -0
  13. package/dist/cli/commands/perf-audit-commands.js +1 -1
  14. package/dist/cli/commands/prd-commands.js +3 -1
  15. package/dist/cli/commands/qa-commands.js +1 -1
  16. package/dist/cli/commands/sc-commands.js +3 -1
  17. package/dist/cli/commands/security-audit-commands.js +1 -1
  18. package/dist/cli/commands/skill-search-commands.js +3 -0
  19. package/dist/cli/commands/upgrade-commands.js +1 -1
  20. package/dist/cli/program.js +12 -4
  21. package/dist/services/agent/ecc-cache-service.d.ts +97 -0
  22. package/dist/services/agent/ecc-cache-service.js +491 -0
  23. package/dist/services/audit/static-service.d.ts +32 -25
  24. package/dist/services/audit/static-service.js +30 -109
  25. package/dist/services/config/config-service.d.ts +1 -12
  26. package/dist/services/config/config-service.js +3 -72
  27. package/dist/services/config/config-types.d.ts +2 -4
  28. package/dist/services/context/context-schema.d.ts +2 -2
  29. package/dist/services/job/job-types.d.ts +3 -3
  30. package/dist/services/log/retention.d.ts +16 -0
  31. package/dist/services/log/retention.js +17 -0
  32. package/dist/services/migrate-skill-name/schema.d.ts +2 -2
  33. package/dist/services/rd/rd-service.js +10 -11
  34. package/dist/services/rd/types.d.ts +2 -2
  35. package/dist/services/recommendations/capability-seed-items.js +0 -1
  36. package/dist/services/recommendations/capability-seed-mappings.js +0 -1
  37. package/dist/services/recommendations/capability-seed-sources.js +0 -1
  38. package/dist/services/skill/skill-search-service.d.ts +5 -0
  39. package/dist/services/skill/skill-search-service.js +6 -2
  40. package/dist/services/skills/skill-registry.d.ts +1 -0
  41. package/dist/services/skills/skill-registry.js +4 -2
  42. package/dist/services/workflow/workflow-router-service.js +7 -7
  43. package/dist/shared/version.d.ts +1 -1
  44. package/dist/shared/version.js +1 -1
  45. package/package.json +1 -1
  46. package/skills/bee/peaks-perf-audit/SKILL.md +1 -0
  47. package/skills/bee/peaks-rd/SKILL.md +1 -24
  48. package/skills/bee/peaks-reviewer/SKILL.md +1 -0
  49. package/skills/bee/peaks-security-audit/SKILL.md +1 -0
  50. package/skills/peaks-code/SKILL.md +1 -99
  51. package/skills/peaks-code/references/startup-sequence.md +110 -0
  52. package/skills/peaks-doctor/SKILL.md +1 -1
  53. package/skills/peaks-ide/SKILL.md +1 -1
  54. package/dist/cli/commands/agent-commands.d.ts +0 -20
  55. package/dist/cli/commands/agent-commands.js +0 -48
  56. package/dist/cli/commands/worker-commands.d.ts +0 -3
  57. package/dist/cli/commands/worker-commands.js +0 -52
  58. package/dist/services/agent/ecc-agent-service.d.ts +0 -47
  59. package/dist/services/agent/ecc-agent-service.js +0 -143
  60. package/dist/services/config/provider-service.d.ts +0 -48
  61. package/dist/services/config/provider-service.js +0 -143
  62. package/dist/services/providers/minimax-provider-service.d.ts +0 -24
  63. package/dist/services/providers/minimax-provider-service.js +0 -143
  64. package/dist/services/providers/minimax-worker-service.d.ts +0 -21
  65. package/dist/services/providers/minimax-worker-service.js +0 -83
@@ -0,0 +1,97 @@
1
+ /**
2
+ * ECC cache service — Slice 3 of 4.0.0-beta.11.
3
+ *
4
+ * Drop-in replacement for the deleted `ecc-agent-service.ts`. The
5
+ * pre-Slice-3 implementation shelled out to `npx ecc agent run ...`,
6
+ * but the upstream `affaan-m/everything-claude-code` v2.0.0 release
7
+ * has NO `ecc` binary — the repo is `agents/*.md` flat files plus
8
+ * SKILL.md descriptors. The peaks-loop path is therefore download
9
+ * + cache only; the LLM consumes cached `agents/*.md` directly
10
+ * through `peaks ecc show <name>`.
11
+ *
12
+ * Six exports per RD §2:
13
+ * - setCacheDirPermissions
14
+ * - downloadToCache
15
+ * - readCacheManifest
16
+ * - listCachedAgents
17
+ * - readAgentSkill
18
+ * - cleanupStaleCache
19
+ *
20
+ * The cache layout is `~/.peaks/cache/ecc-installed.json` (manifest)
21
+ * + `~/.peaks/cache/ecc-<sha>/agents/<name>.md`. The manifest is
22
+ * the active-cache pointer; cleanupStaleCache iterates the on-disk
23
+ * sha directories and decides based on the manifest's `fetchedAt`
24
+ * (active) vs `mtimeMs` (orphan).
25
+ *
26
+ * Hard contracts (Karpathy #2 Simplicity First):
27
+ * - No subprocess plumbing. Network is `fetch()` only.
28
+ * - No factory; one module, six functions, six dependencies.
29
+ * - Strict selective extract: agents/*.md only, no symlinks, no
30
+ * `..`, no absolute paths, no devices.
31
+ * - D-009 fallback: parseFrontmatter throws on malformed input —
32
+ * wrap it in try/catch and synthesize `{name, description}`
33
+ * from the file name + first non-empty body line.
34
+ * - D-010 fallback: PRD's `ecc.tar.gz` asset URL does NOT exist
35
+ * upstream; try PRD first, fall back to GitHub's `tarball_url`
36
+ * on the release JSON.
37
+ */
38
+ export declare const ECC_REPO_OWNER = "affaan-m";
39
+ export declare const ECC_REPO_NAME = "everything-claude-code";
40
+ export type CacheManifest = {
41
+ version: string;
42
+ sha: string;
43
+ fetchedAt: string;
44
+ agents: string[];
45
+ };
46
+ export type DownloadResult = {
47
+ sha: string;
48
+ agents: number;
49
+ };
50
+ /**
51
+ * Cache dir resolution — single source of truth. Tests inject
52
+ * via `dirOverride` on the functions that take one.
53
+ */
54
+ export declare function resolveEccCacheDir(): string;
55
+ export declare function resolveManifestPath(dirOverride?: string): string;
56
+ export declare function resolveShaDir(sha: string, dirOverride?: string): string;
57
+ export declare function resolveAgentsDir(sha: string, dirOverride?: string): string;
58
+ /**
59
+ * Best-effort chmod 0o700 on POSIX; no-op on Windows (NTFS uses
60
+ * ACLs, not POSIX mode bits). Swallows errors with WARN so a
61
+ * permissions failure never blocks the CLI.
62
+ */
63
+ export declare function setCacheDirPermissions(cacheDir: string): void;
64
+ /**
65
+ * Download ECC into `~/.peaks/cache/ecc-<sha>/agents/`. Returns
66
+ * the resolved SHA + the number of agents extracted.
67
+ *
68
+ * Throws `Error('fetch-failed')` on network failure so the CLI
69
+ * layer can render the manual-install instructions.
70
+ */
71
+ export declare function downloadToCache({ ref }?: {
72
+ ref?: string;
73
+ }): Promise<DownloadResult>;
74
+ export declare function readCacheManifest(): CacheManifest | null;
75
+ export declare function listCachedAgents(): {
76
+ name: string;
77
+ description: string;
78
+ }[];
79
+ export declare function readAgentSkill(name: string): string | null;
80
+ /**
81
+ * 7-day TTL sweep over `ecc-<sha>/` directories.
82
+ *
83
+ * - Active cache (per manifest `sha`): use `fetchedAt` as the
84
+ * age source; remove if older than `retentionDays`.
85
+ * - Orphan caches (any other `ecc-<40hex>` directory): use
86
+ * directory `mtimeMs` as the age source.
87
+ * - If the active cache is removed, invalidate the manifest.
88
+ *
89
+ * Returns the absolute paths of the removed directories.
90
+ */
91
+ export declare function cleanupStaleCache({ retentionDays, nowMs, dirOverride, }: {
92
+ retentionDays: number;
93
+ nowMs: number;
94
+ dirOverride?: string;
95
+ }): {
96
+ removed: string[];
97
+ };
@@ -0,0 +1,491 @@
1
+ /**
2
+ * ECC cache service — Slice 3 of 4.0.0-beta.11.
3
+ *
4
+ * Drop-in replacement for the deleted `ecc-agent-service.ts`. The
5
+ * pre-Slice-3 implementation shelled out to `npx ecc agent run ...`,
6
+ * but the upstream `affaan-m/everything-claude-code` v2.0.0 release
7
+ * has NO `ecc` binary — the repo is `agents/*.md` flat files plus
8
+ * SKILL.md descriptors. The peaks-loop path is therefore download
9
+ * + cache only; the LLM consumes cached `agents/*.md` directly
10
+ * through `peaks ecc show <name>`.
11
+ *
12
+ * Six exports per RD §2:
13
+ * - setCacheDirPermissions
14
+ * - downloadToCache
15
+ * - readCacheManifest
16
+ * - listCachedAgents
17
+ * - readAgentSkill
18
+ * - cleanupStaleCache
19
+ *
20
+ * The cache layout is `~/.peaks/cache/ecc-installed.json` (manifest)
21
+ * + `~/.peaks/cache/ecc-<sha>/agents/<name>.md`. The manifest is
22
+ * the active-cache pointer; cleanupStaleCache iterates the on-disk
23
+ * sha directories and decides based on the manifest's `fetchedAt`
24
+ * (active) vs `mtimeMs` (orphan).
25
+ *
26
+ * Hard contracts (Karpathy #2 Simplicity First):
27
+ * - No subprocess plumbing. Network is `fetch()` only.
28
+ * - No factory; one module, six functions, six dependencies.
29
+ * - Strict selective extract: agents/*.md only, no symlinks, no
30
+ * `..`, no absolute paths, no devices.
31
+ * - D-009 fallback: parseFrontmatter throws on malformed input —
32
+ * wrap it in try/catch and synthesize `{name, description}`
33
+ * from the file name + first non-empty body line.
34
+ * - D-010 fallback: PRD's `ecc.tar.gz` asset URL does NOT exist
35
+ * upstream; try PRD first, fall back to GitHub's `tarball_url`
36
+ * on the release JSON.
37
+ */
38
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
39
+ import { join } from 'node:path';
40
+ import { homedir } from 'node:os';
41
+ import { parseFrontmatter } from '../../shared/frontmatter.js';
42
+ export const ECC_REPO_OWNER = 'affaan-m';
43
+ export const ECC_REPO_NAME = 'everything-claude-code';
44
+ const ECC_CACHE_VERSION = '1';
45
+ const ECC_TARBALL_BASENAME = 'ecc.tar.gz';
46
+ /**
47
+ * Cache dir resolution — single source of truth. Tests inject
48
+ * via `dirOverride` on the functions that take one.
49
+ */
50
+ export function resolveEccCacheDir() {
51
+ return join(homedir(), '.peaks', 'cache');
52
+ }
53
+ export function resolveManifestPath(dirOverride) {
54
+ return join(dirOverride ?? resolveEccCacheDir(), 'ecc-installed.json');
55
+ }
56
+ export function resolveShaDir(sha, dirOverride) {
57
+ return join(dirOverride ?? resolveEccCacheDir(), `ecc-${sha}`);
58
+ }
59
+ export function resolveAgentsDir(sha, dirOverride) {
60
+ return join(resolveShaDir(sha, dirOverride), 'agents');
61
+ }
62
+ /**
63
+ * Best-effort chmod 0o700 on POSIX; no-op on Windows (NTFS uses
64
+ * ACLs, not POSIX mode bits). Swallows errors with WARN so a
65
+ * permissions failure never blocks the CLI.
66
+ */
67
+ export function setCacheDirPermissions(cacheDir) {
68
+ if (process.platform === 'win32')
69
+ return;
70
+ try {
71
+ chmodSync(cacheDir, 0o700);
72
+ }
73
+ catch {
74
+ /* best-effort */
75
+ }
76
+ }
77
+ function isSafeAgentName(name) {
78
+ return /^[a-z][a-z0-9-]*$/.test(name);
79
+ }
80
+ function isSafeArchiveEntry(entryName) {
81
+ if (entryName.length === 0)
82
+ return false;
83
+ if (entryName.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(entryName))
84
+ return false;
85
+ if (entryName.includes('..'))
86
+ return false;
87
+ // Strip the leading "<repo-root>/" segment (github tarballs prefix
88
+ // every entry with "<repo>-<sha>/..."); everything after must
89
+ // start with "agents/" and end with ".md" at a single level.
90
+ const segments = entryName.split('/');
91
+ // Drop the first "<repo>-<sha>" segment if present.
92
+ if (segments.length >= 3 && segments[0]?.startsWith(`${ECC_REPO_NAME}-`)) {
93
+ segments.shift();
94
+ }
95
+ if (segments.length !== 2)
96
+ return false;
97
+ return segments[0] === 'agents' && segments[1]?.endsWith('.md') === true;
98
+ }
99
+ function safeAgentNameFromEntry(entryName) {
100
+ const segments = entryName.split('/');
101
+ if (segments.length >= 3 && segments[0]?.startsWith(`${ECC_REPO_NAME}-`)) {
102
+ segments.shift();
103
+ }
104
+ const file = segments[segments.length - 1] ?? '';
105
+ const base = file.replace(/\.md$/i, '');
106
+ return isSafeAgentName(base) ? base : null;
107
+ }
108
+ /**
109
+ * Best-effort fetch of the upstream release JSON. Returns the
110
+ * parsed body, or null on any network/parse error. Used as the
111
+ * D-010 fallback path.
112
+ */
113
+ async function fetchReleaseJson(apiBase) {
114
+ try {
115
+ const res = await fetch(apiBase, {
116
+ headers: { accept: 'application/vnd.github+json', 'user-agent': 'peaks-loop' },
117
+ });
118
+ if (!res.ok)
119
+ return null;
120
+ return (await res.json());
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * Resolve a 40-char commit SHA for `tag`. Prefers the GitHub
128
+ * releases API (which dereferences annotated tags to commit SHA
129
+ * via `target_commitish`). Falls back to the tag name if the
130
+ * upstream returns a non-SHA-shaped identifier (older tags).
131
+ */
132
+ async function resolveCommitSha(ref) {
133
+ const apiBase = `https://api.github.com/repos/${ECC_REPO_OWNER}/${ECC_REPO_NAME}/releases/tags/${encodeURIComponent(ref)}`;
134
+ const release = await fetchReleaseJson(apiBase);
135
+ const tagName = release?.tag_name ?? ref;
136
+ if (/^[0-9a-f]{40}$/.test(tagName))
137
+ return tagName;
138
+ return tagName;
139
+ }
140
+ function asSha(value) {
141
+ return /^[0-9a-f]{40}$/.test(value) ? value : value;
142
+ }
143
+ /**
144
+ * Tiny tar.gz extractor. Public-domain-compatible tar parser:
145
+ * - reads 512-byte header blocks
146
+ * - validates name + size
147
+ * - rejects anything that is not a regular file at a safe path
148
+ * - filters to the `agents/*.md` allowlist via isSafeArchiveEntry
149
+ *
150
+ * We do NOT decompress gzip via a third-party lib; Node's
151
+ * `node:zlib` ships with `gunzip` so the implementation is
152
+ * self-contained.
153
+ */
154
+ async function extractAgentsFromTarGz(buffer, outDir) {
155
+ if (!existsSync(outDir))
156
+ mkdirSync(outDir, { recursive: true });
157
+ const extracted = [];
158
+ // Lazy import to keep the cold path cheap.
159
+ const { gunzipSync } = await import('node:zlib');
160
+ let tar;
161
+ try {
162
+ tar = gunzipSync(buffer);
163
+ }
164
+ catch {
165
+ return [];
166
+ }
167
+ const BLOCK = 512;
168
+ let offset = 0;
169
+ while (offset + BLOCK <= tar.length) {
170
+ const header = tar.subarray(offset, offset + BLOCK);
171
+ // Two consecutive zero blocks mark end-of-archive.
172
+ if (header.every((b) => b === 0))
173
+ break;
174
+ const name = readCString(header, 0, 100);
175
+ if (name === null || name.length === 0)
176
+ break;
177
+ const sizeOctal = readCString(header, 124, 12);
178
+ if (sizeOctal === null)
179
+ break;
180
+ const size = parseOctal(sizeOctal);
181
+ if (size === null)
182
+ break;
183
+ const typeFlag = String.fromCharCode(header[156] ?? 0);
184
+ // '0' or '\0' = regular file. Reject symlinks (2), dirs (5), etc.
185
+ if (typeFlag !== '0' && typeFlag !== '\0') {
186
+ offset += BLOCK + Math.ceil(size / BLOCK) * BLOCK;
187
+ continue;
188
+ }
189
+ if (!isSafeArchiveEntry(name)) {
190
+ offset += BLOCK + Math.ceil(size / BLOCK) * BLOCK;
191
+ continue;
192
+ }
193
+ const safeName = safeAgentNameFromEntry(name);
194
+ if (safeName === null) {
195
+ offset += BLOCK + Math.ceil(size / BLOCK) * BLOCK;
196
+ continue;
197
+ }
198
+ const data = tar.subarray(offset + BLOCK, offset + BLOCK + size);
199
+ writeFileSync(join(outDir, `${safeName}.md`), data);
200
+ extracted.push(safeName);
201
+ offset += BLOCK + Math.ceil(size / BLOCK) * BLOCK;
202
+ }
203
+ return extracted;
204
+ }
205
+ function readCString(buf, start, len) {
206
+ let end = start;
207
+ while (end < start + len && buf[end] !== 0)
208
+ end += 1;
209
+ if (end === start)
210
+ return '';
211
+ try {
212
+ return Buffer.from(buf.subarray(start, end)).toString('utf8');
213
+ }
214
+ catch {
215
+ return null;
216
+ }
217
+ }
218
+ function parseOctal(value) {
219
+ const trimmed = value.trim().replace(/\0+$/g, '');
220
+ if (trimmed.length === 0)
221
+ return 0;
222
+ const n = Number.parseInt(trimmed, 8);
223
+ return Number.isFinite(n) && n >= 0 ? n : null;
224
+ }
225
+ async function fetchBuffer(url) {
226
+ try {
227
+ const res = await fetch(url, {
228
+ headers: { accept: 'application/octet-stream', 'user-agent': 'peaks-loop' },
229
+ });
230
+ if (!res.ok)
231
+ return null;
232
+ const ab = await res.arrayBuffer();
233
+ return new Uint8Array(ab);
234
+ }
235
+ catch {
236
+ return null;
237
+ }
238
+ }
239
+ async function downloadTarball(ref, sha, outDir) {
240
+ // D-010: PRD URL first, GitHub fallback second.
241
+ const prdUrl = `https://github.com/${ECC_REPO_OWNER}/${ECC_REPO_NAME}/releases/download/${encodeURIComponent(ref)}/${ECC_TARBALL_BASENAME}`;
242
+ let buffer = await fetchBuffer(prdUrl);
243
+ if (buffer === null) {
244
+ const release = await fetchReleaseJson(`https://api.github.com/repos/${ECC_REPO_OWNER}/${ECC_REPO_NAME}/releases/tags/${encodeURIComponent(ref)}`);
245
+ const tarballUrl = release?.tarball_url;
246
+ if (typeof tarballUrl === 'string' && tarballUrl.length > 0) {
247
+ buffer = await fetchBuffer(tarballUrl);
248
+ }
249
+ if (buffer === null && Array.isArray(release?.assets)) {
250
+ const asset = release.assets.find((a) => a.name === ECC_TARBALL_BASENAME) ??
251
+ release.assets.find((a) => a.name === `${ECC_REPO_NAME}-universal-${ref}.tgz`) ??
252
+ release.assets.find((a) => a.name.endsWith('.tgz') || a.name.endsWith('.tar.gz'));
253
+ if (asset)
254
+ buffer = await fetchBuffer(asset.browser_download_url);
255
+ }
256
+ }
257
+ if (buffer === null) {
258
+ throw new Error('fetch-failed');
259
+ }
260
+ return extractAgentsFromTarGz(buffer, outDir);
261
+ }
262
+ /**
263
+ * Download ECC into `~/.peaks/cache/ecc-<sha>/agents/`. Returns
264
+ * the resolved SHA + the number of agents extracted.
265
+ *
266
+ * Throws `Error('fetch-failed')` on network failure so the CLI
267
+ * layer can render the manual-install instructions.
268
+ */
269
+ export async function downloadToCache({ ref } = {}) {
270
+ const requestedRef = ref ?? 'latest';
271
+ let resolvedSha;
272
+ if (requestedRef === 'latest') {
273
+ const release = await fetchReleaseJson(`https://api.github.com/repos/${ECC_REPO_OWNER}/${ECC_REPO_NAME}/releases/latest`);
274
+ const tag = release?.tag_name;
275
+ if (typeof tag !== 'string' || tag.length === 0) {
276
+ throw new Error('fetch-failed');
277
+ }
278
+ resolvedSha = asSha(await resolveCommitSha(tag) ?? tag);
279
+ }
280
+ else {
281
+ resolvedSha = asSha(await resolveCommitSha(requestedRef) ?? requestedRef);
282
+ }
283
+ const cacheDir = resolveEccCacheDir();
284
+ if (!existsSync(cacheDir))
285
+ mkdirSync(cacheDir, { recursive: true });
286
+ setCacheDirPermissions(cacheDir);
287
+ const agentsDir = resolveAgentsDir(resolvedSha, cacheDir);
288
+ if (existsSync(agentsDir) && readdirSync(agentsDir).length > 0) {
289
+ // Already populated; emit manifest + return.
290
+ const agents = readdirSync(agentsDir)
291
+ .filter((f) => f.endsWith('.md'))
292
+ .map((f) => f.replace(/\.md$/i, ''));
293
+ writeManifest({ version: ECC_CACHE_VERSION, sha: resolvedSha, fetchedAt: new Date().toISOString(), agents });
294
+ return { sha: resolvedSha, agents: agents.length };
295
+ }
296
+ const extracted = await downloadTarball(requestedRef === 'latest' ? resolvedSha : requestedRef, resolvedSha, agentsDir);
297
+ const manifest = {
298
+ version: ECC_CACHE_VERSION,
299
+ sha: resolvedSha,
300
+ fetchedAt: new Date().toISOString(),
301
+ agents: extracted,
302
+ };
303
+ writeManifest(manifest);
304
+ return { sha: resolvedSha, agents: extracted.length };
305
+ }
306
+ function writeManifest(manifest) {
307
+ const path = resolveManifestPath();
308
+ writeFileSync(path, JSON.stringify(manifest, null, 2));
309
+ }
310
+ export function readCacheManifest() {
311
+ const path = resolveManifestPath();
312
+ if (!existsSync(path))
313
+ return null;
314
+ try {
315
+ const raw = readFileSync(path, 'utf8');
316
+ const parsed = JSON.parse(raw);
317
+ if (typeof parsed.version === 'string' &&
318
+ typeof parsed.sha === 'string' &&
319
+ typeof parsed.fetchedAt === 'string' &&
320
+ Array.isArray(parsed.agents)) {
321
+ return parsed;
322
+ }
323
+ return null;
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ }
329
+ let warnedAboutFallback = false;
330
+ function fallbackMetadata(fileName) {
331
+ const base = fileName.replace(/\.md$/i, '');
332
+ let description = '';
333
+ try {
334
+ const body = readFileSync(join(resolveManifestDirForAgents(), fileName), 'utf8');
335
+ const lines = body.split(/\r?\n/);
336
+ let inFrontmatter = false;
337
+ let seenClosing = false;
338
+ for (const raw of lines) {
339
+ const line = raw.trim();
340
+ if (!seenClosing) {
341
+ if (!inFrontmatter && line === '---') {
342
+ inFrontmatter = true;
343
+ continue;
344
+ }
345
+ if (inFrontmatter && line === '---') {
346
+ seenClosing = true;
347
+ continue;
348
+ }
349
+ continue;
350
+ }
351
+ if (line.length === 0)
352
+ continue;
353
+ description = line;
354
+ break;
355
+ }
356
+ }
357
+ catch {
358
+ /* best-effort */
359
+ }
360
+ if (description.length > 80)
361
+ description = `${description.slice(0, 77)}...`;
362
+ if (!warnedAboutFallback) {
363
+ try {
364
+ process.stderr.write(`warning: cached agent "${base}" has malformed frontmatter; falling back to filename + first body line\n`);
365
+ }
366
+ catch {
367
+ /* best-effort */
368
+ }
369
+ warnedAboutFallback = true;
370
+ }
371
+ return { name: base, description };
372
+ }
373
+ function resolveManifestDirForAgents() {
374
+ const manifest = readCacheManifest();
375
+ if (manifest === null)
376
+ return '';
377
+ return resolveAgentsDir(manifest.sha);
378
+ }
379
+ export function listCachedAgents() {
380
+ const manifest = readCacheManifest();
381
+ if (manifest === null)
382
+ return [];
383
+ const agentsDir = resolveAgentsDir(manifest.sha);
384
+ if (!existsSync(agentsDir))
385
+ return [];
386
+ const out = [];
387
+ for (const file of readdirSync(agentsDir).sort()) {
388
+ if (!file.endsWith('.md'))
389
+ continue;
390
+ const fullPath = join(agentsDir, file);
391
+ let meta;
392
+ try {
393
+ const raw = readFileSync(fullPath, 'utf8');
394
+ const fm = parseFrontmatter(raw);
395
+ meta = { name: fm.name, description: fm.description };
396
+ }
397
+ catch {
398
+ // D-009 fallback — name from filename, description from first body line.
399
+ meta = fallbackMetadata(file);
400
+ }
401
+ out.push(meta);
402
+ }
403
+ return out;
404
+ }
405
+ export function readAgentSkill(name) {
406
+ if (!isSafeAgentName(name))
407
+ return null;
408
+ const manifest = readCacheManifest();
409
+ if (manifest === null)
410
+ return null;
411
+ const file = join(resolveAgentsDir(manifest.sha), `${name}.md`);
412
+ if (!existsSync(file))
413
+ return null;
414
+ try {
415
+ return readFileSync(file, 'utf8');
416
+ }
417
+ catch {
418
+ return null;
419
+ }
420
+ }
421
+ /**
422
+ * 7-day TTL sweep over `ecc-<sha>/` directories.
423
+ *
424
+ * - Active cache (per manifest `sha`): use `fetchedAt` as the
425
+ * age source; remove if older than `retentionDays`.
426
+ * - Orphan caches (any other `ecc-<40hex>` directory): use
427
+ * directory `mtimeMs` as the age source.
428
+ * - If the active cache is removed, invalidate the manifest.
429
+ *
430
+ * Returns the absolute paths of the removed directories.
431
+ */
432
+ export function cleanupStaleCache({ retentionDays, nowMs, dirOverride, }) {
433
+ const cacheDir = dirOverride ?? resolveEccCacheDir();
434
+ if (!existsSync(cacheDir))
435
+ return { removed: [] };
436
+ const dayMs = 24 * 60 * 60 * 1000;
437
+ const cutoff = nowMs - retentionDays * dayMs;
438
+ const manifest = readCacheManifest();
439
+ const activeSha = manifest?.sha ?? null;
440
+ const activeFetchedAt = manifest?.fetchedAt ? Date.parse(manifest.fetchedAt) : NaN;
441
+ const removed = [];
442
+ let names;
443
+ try {
444
+ names = readdirSync(cacheDir);
445
+ }
446
+ catch {
447
+ return { removed: [] };
448
+ }
449
+ for (const name of names) {
450
+ const match = /^ecc-([0-9a-f]{40})$/.exec(name);
451
+ if (match === null)
452
+ continue;
453
+ const sha = match[1] ?? '';
454
+ const fullPath = join(cacheDir, name);
455
+ let stat;
456
+ try {
457
+ stat = statSync(fullPath);
458
+ }
459
+ catch {
460
+ continue;
461
+ }
462
+ if (!stat.isDirectory())
463
+ continue;
464
+ let ageMs;
465
+ if (sha === activeSha && Number.isFinite(activeFetchedAt)) {
466
+ ageMs = nowMs - activeFetchedAt;
467
+ }
468
+ else {
469
+ ageMs = nowMs - stat.mtimeMs;
470
+ }
471
+ if (ageMs > retentionDays * dayMs) {
472
+ try {
473
+ rmSync(fullPath, { recursive: true, force: true });
474
+ removed.push(fullPath);
475
+ }
476
+ catch {
477
+ /* best-effort */
478
+ }
479
+ }
480
+ }
481
+ // Invalidate manifest if active cache was removed.
482
+ if (activeSha !== null && removed.some((p) => p.includes(`ecc-${activeSha}`))) {
483
+ try {
484
+ rmSync(resolveManifestPath(cacheDir), { force: true });
485
+ }
486
+ catch {
487
+ /* best-effort */
488
+ }
489
+ }
490
+ return { removed };
491
+ }
@@ -1,52 +1,59 @@
1
1
  /**
2
2
  * Static audit service — `peaks audit static`.
3
3
  *
4
- * Slice #6 L2.3 P2-a: soft-optional ECC AgentShield integration.
4
+ * Slice 3 of 4.0.0-beta.11 dropped the ECC AgentShield subprocess
5
+ * integration entirely. The pre-Slice-3 implementation probed
6
+ * `npx ecc-agentshield --version` and shelled out to
7
+ * `npx ecc-agentshield scan --json --target <path>` on every audit
8
+ * call, but the upstream `affaan-m/everything-claude-code` v2.0.0
9
+ * release ships no `ecc-agentshield` binary — the repo is
10
+ * `agents/*.md` flat files plus SKILL.md descriptors.
5
11
  *
6
- * Per spec §5.3, the audit framework integrates with ECC AgentShield
7
- * (102 lint rules) on a soft-optional basis. When ECC is installed
8
- * AND `agentShieldEnabled: true`, the audit subprocess spawns the
9
- * ECC scanner and merges its findings; when either is missing, the
10
- * audit completes with peaks-loop-only findings.
12
+ * The new contract: `peaks audit static` runs the peaks-loop lint
13
+ * layer only. There is no subprocess probe, no `installed` flag,
14
+ * no agent-shield reason code. The `agentShield` field is preserved
15
+ * on the result type as a frozen "always disabled" stub so any
16
+ * downstream consumer keeps compiling, but the merged findings list
17
+ * is always the peaks-loop findings list (no ECC findings).
11
18
  *
12
- * The subprocess is **observability enhancement**, not a structural
13
- * gate — every soft-fail path returns a well-formed audit report.
19
+ * The subprocess-runner seam (`SubprocessRunner`, `defaultSubprocessRunner`)
20
+ * is preserved as an EXPORTED TYPE so legacy tests can still
21
+ * mock it; the runner itself is never invoked from production code.
14
22
  */
15
23
  import { type ProseRatioResult } from './prose-ratio-calculator.js';
16
24
  import type { EnforcerFinding, RedLineAudit } from './types.js';
17
25
  export interface StaticAuditInput {
18
26
  readonly projectRoot: string;
19
- /** Override the preference for a single call. Default: read from preferences. */
27
+ /** Slice 3: retained for backward-compat; no longer wired. */
20
28
  readonly enableAgentShield?: boolean | undefined;
21
- /**
22
- * Optional subprocess runner injection point. Used by tests
23
- * to mock `npx ecc-agentshield`. Production callers should
24
- * leave this undefined; the default uses `child_process.spawnSync`.
25
- */
29
+ /** Slice 3: retained as a type seam; never invoked in production. */
26
30
  readonly subprocessRunner?: SubprocessRunner | undefined;
27
31
  }
28
32
  export interface StaticAuditResult {
29
33
  readonly audit: RedLineAudit;
30
34
  readonly agentShield: AgentShieldState;
31
35
  readonly warnings: readonly string[];
32
- /** v2.14.0 Slice C: prose-only ratio with informational exclusion (per A3.1). */
33
36
  readonly proseRatio: ProseRatioResult;
34
37
  }
38
+ /**
39
+ * Frozen stub. Slice 3 collapsed the agent-shield reason set to a
40
+ * single value (`'disabled-and-ecc-missing'`) because the
41
+ * subprocess integration was removed; we preserve the other
42
+ * reason literals as TYPE-LEVEL members so any test or
43
+ * downstream consumer that pattern-matches on `'flag-enabled-but-ecc-missing'`
44
+ * keeps type-checking, but the runtime value is always the
45
+ * single collapsed state.
46
+ */
35
47
  export interface AgentShieldState {
36
- /** Whether the agent-shield subprocess was spawned. */
37
- readonly spawned: boolean;
38
- /** Whether ECC AgentShield is installed (regardless of whether it was spawned). */
39
- readonly installed: boolean;
40
- /** Why the subprocess was or was not spawned. */
48
+ readonly spawned: false;
49
+ readonly installed: false;
41
50
  readonly reason: 'enabled-and-installed' | 'disabled-by-preference' | 'flag-disabled' | 'flag-enabled-but-ecc-missing' | 'disabled-and-ecc-missing';
42
- /** The merged ECC findings (empty if not spawned). */
43
51
  readonly findings: readonly EnforcerFinding[];
44
52
  }
45
53
  /**
46
- * Subprocess runner interface — the only seam tests need to
47
- * mock the `npx ecc-agentshield` subprocess. Production callers
48
- * leave `subprocessRunner` undefined; the default delegates to
49
- * `child_process.spawnSync` (synchronous, captures stdout).
54
+ * Subprocess runner interface — preserved as a TYPE for legacy
55
+ * test code. Production no longer invokes it; the audit always
56
+ * runs peaks-loop-only findings.
50
57
  */
51
58
  export interface SubprocessRunner {
52
59
  run(command: string, args: readonly string[], timeoutMs: number): SubprocessResult;