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.
- package/CHANGELOG.md +18 -11
- package/dist/cli/cli-helpers.d.ts +0 -5
- package/dist/cli/cli-helpers.js +0 -24
- package/dist/cli/commands/audit-commands.js +1 -1
- package/dist/cli/commands/capability-worker-config-sc-commands.js +0 -2
- package/dist/cli/commands/code-commands.js +1 -1
- package/dist/cli/commands/code-review-commands.js +1 -1
- package/dist/cli/commands/config-commands.js +3 -74
- package/dist/cli/commands/core/skill-command.js +5 -2
- package/dist/cli/commands/dispatch-commands.js +14 -0
- package/dist/cli/commands/ecc-commands.d.ts +30 -0
- package/dist/cli/commands/ecc-commands.js +119 -0
- package/dist/cli/commands/perf-audit-commands.js +1 -1
- package/dist/cli/commands/prd-commands.js +3 -1
- package/dist/cli/commands/qa-commands.js +1 -1
- package/dist/cli/commands/sc-commands.js +3 -1
- package/dist/cli/commands/security-audit-commands.js +1 -1
- package/dist/cli/commands/skill-search-commands.js +3 -0
- package/dist/cli/commands/upgrade-commands.js +1 -1
- package/dist/cli/program.js +12 -4
- package/dist/services/agent/ecc-cache-service.d.ts +97 -0
- package/dist/services/agent/ecc-cache-service.js +491 -0
- package/dist/services/audit/static-service.d.ts +32 -25
- package/dist/services/audit/static-service.js +30 -109
- package/dist/services/config/config-service.d.ts +1 -12
- package/dist/services/config/config-service.js +3 -72
- package/dist/services/config/config-types.d.ts +2 -4
- package/dist/services/context/context-schema.d.ts +2 -2
- package/dist/services/job/job-types.d.ts +3 -3
- package/dist/services/log/retention.d.ts +16 -0
- package/dist/services/log/retention.js +17 -0
- package/dist/services/migrate-skill-name/schema.d.ts +2 -2
- package/dist/services/rd/rd-service.js +10 -11
- package/dist/services/rd/types.d.ts +2 -2
- package/dist/services/recommendations/capability-seed-items.js +0 -1
- package/dist/services/recommendations/capability-seed-mappings.js +0 -1
- package/dist/services/recommendations/capability-seed-sources.js +0 -1
- package/dist/services/skill/skill-search-service.d.ts +5 -0
- package/dist/services/skill/skill-search-service.js +6 -2
- package/dist/services/skills/skill-registry.d.ts +1 -0
- package/dist/services/skills/skill-registry.js +4 -2
- package/dist/services/workflow/workflow-router-service.js +7 -7
- package/dist/shared/version.d.ts +1 -1
- package/dist/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/bee/peaks-perf-audit/SKILL.md +1 -0
- package/skills/bee/peaks-rd/SKILL.md +1 -24
- package/skills/bee/peaks-reviewer/SKILL.md +1 -0
- package/skills/bee/peaks-security-audit/SKILL.md +1 -0
- package/skills/peaks-code/SKILL.md +1 -99
- package/skills/peaks-code/references/startup-sequence.md +110 -0
- package/skills/peaks-doctor/SKILL.md +1 -1
- package/skills/peaks-ide/SKILL.md +1 -1
- package/dist/cli/commands/agent-commands.d.ts +0 -20
- package/dist/cli/commands/agent-commands.js +0 -48
- package/dist/cli/commands/worker-commands.d.ts +0 -3
- package/dist/cli/commands/worker-commands.js +0 -52
- package/dist/services/agent/ecc-agent-service.d.ts +0 -47
- package/dist/services/agent/ecc-agent-service.js +0 -143
- package/dist/services/config/provider-service.d.ts +0 -48
- package/dist/services/config/provider-service.js +0 -143
- package/dist/services/providers/minimax-provider-service.d.ts +0 -24
- package/dist/services/providers/minimax-provider-service.js +0 -143
- package/dist/services/providers/minimax-worker-service.d.ts +0 -21
- 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
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
13
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
37
|
-
readonly
|
|
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 —
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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;
|