peaks-cli 1.4.2 → 2.0.1
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/.claude-plugin/marketplace.json +51 -0
- package/CHANGELOG.md +279 -0
- package/README-en.md +226 -0
- package/README.md +152 -122
- package/dist/src/cli/commands/agent-commands.d.ts +20 -0
- package/dist/src/cli/commands/agent-commands.js +48 -0
- package/dist/src/cli/commands/audit-commands.d.ts +18 -0
- package/dist/src/cli/commands/audit-commands.js +138 -0
- package/dist/src/cli/commands/capability-commands.js +2 -1
- package/dist/src/cli/commands/classify-classify-commands.d.ts +19 -0
- package/dist/src/cli/commands/classify-classify-commands.js +151 -0
- package/dist/src/cli/commands/code-review-commands.d.ts +34 -0
- package/dist/src/cli/commands/code-review-commands.js +83 -0
- package/dist/src/cli/commands/config-commands.js +90 -0
- package/dist/src/cli/commands/context-commands.d.ts +21 -0
- package/dist/src/cli/commands/context-commands.js +167 -0
- package/dist/src/cli/commands/core-artifact-commands.js +60 -2
- package/dist/src/cli/commands/hook-handle.js +50 -0
- package/dist/src/cli/commands/loop-commands.d.ts +21 -0
- package/dist/src/cli/commands/loop-commands.js +128 -0
- package/dist/src/cli/commands/openspec-commands.js +37 -0
- package/dist/src/cli/commands/preferences-commands.d.ts +2 -0
- package/dist/src/cli/commands/preferences-commands.js +147 -0
- package/dist/src/cli/commands/skill-conformance-commands.d.ts +9 -0
- package/dist/src/cli/commands/skill-conformance-commands.js +39 -0
- package/dist/src/cli/commands/understand-commands.js +34 -0
- package/dist/src/cli/commands/upgrade-commands.d.ts +23 -0
- package/dist/src/cli/commands/upgrade-commands.js +57 -0
- package/dist/src/cli/commands/workflow-commands.js +70 -0
- package/dist/src/cli/commands/workspace-commands.js +117 -2
- package/dist/src/cli/program.js +30 -0
- package/dist/src/lib/render/message-renderer.d.ts +20 -0
- package/dist/src/lib/render/message-renderer.js +80 -0
- package/dist/src/services/agent/ecc-agent-service.d.ts +47 -0
- package/dist/src/services/agent/ecc-agent-service.js +143 -0
- package/dist/src/services/artifacts/request-artifact-service.js +14 -0
- package/dist/src/services/audit/backing-detector.d.ts +24 -0
- package/dist/src/services/audit/backing-detector.js +59 -0
- package/dist/src/services/audit/classifier.d.ts +38 -0
- package/dist/src/services/audit/classifier.js +127 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +29 -0
- package/dist/src/services/audit/enforcers/active-skill-resolver.js +71 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.d.ts +25 -0
- package/dist/src/services/audit/enforcers/design-draft-confirm.js +54 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.d.ts +21 -0
- package/dist/src/services/audit/enforcers/lint-audit-regression.js +86 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.d.ts +27 -0
- package/dist/src/services/audit/enforcers/lint-catalog-governance.js +38 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.d.ts +16 -0
- package/dist/src/services/audit/enforcers/lint-cli-back.js +35 -0
- package/dist/src/services/audit/enforcers/lint-output-style.d.ts +11 -0
- package/dist/src/services/audit/enforcers/lint-output-style.js +94 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.d.ts +6 -0
- package/dist/src/services/audit/enforcers/lint-reference-integrity.js +83 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.d.ts +30 -0
- package/dist/src/services/audit/enforcers/lint-reference-shape.js +272 -0
- package/dist/src/services/audit/enforcers/lint-style.d.ts +49 -0
- package/dist/src/services/audit/enforcers/lint-style.js +173 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.d.ts +5 -0
- package/dist/src/services/audit/enforcers/lint-workflow-shape.js +141 -0
- package/dist/src/services/audit/enforcers/login-gate.d.ts +23 -0
- package/dist/src/services/audit/enforcers/login-gate.js +40 -0
- package/dist/src/services/audit/enforcers/mock-placement.d.ts +25 -0
- package/dist/src/services/audit/enforcers/mock-placement.js +48 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.d.ts +21 -0
- package/dist/src/services/audit/enforcers/no-root-pollution.js +56 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.d.ts +22 -0
- package/dist/src/services/audit/enforcers/pre-rd-scan.js +23 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.d.ts +25 -0
- package/dist/src/services/audit/enforcers/prototype-fidelity.js +75 -0
- package/dist/src/services/audit/enforcers/resume-detection.d.ts +21 -0
- package/dist/src/services/audit/enforcers/resume-detection.js +52 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.d.ts +23 -0
- package/dist/src/services/audit/enforcers/solo-code-ban.js +27 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.d.ts +25 -0
- package/dist/src/services/audit/enforcers/sub-agent-sid.js +63 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.d.ts +28 -0
- package/dist/src/services/audit/enforcers/tech-doc-presence.js +35 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.d.ts +21 -0
- package/dist/src/services/audit/red-line-catalog-p2-a.js +233 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.d.ts +19 -0
- package/dist/src/services/audit/red-line-catalog-p2-b.js +225 -0
- package/dist/src/services/audit/red-line-catalog.d.ts +51 -0
- package/dist/src/services/audit/red-line-catalog.js +210 -0
- package/dist/src/services/audit/red-lines-service.d.ts +23 -0
- package/dist/src/services/audit/red-lines-service.js +486 -0
- package/dist/src/services/audit/scanners/openspec-scanner.d.ts +15 -0
- package/dist/src/services/audit/scanners/openspec-scanner.js +55 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.d.ts +16 -0
- package/dist/src/services/audit/scanners/rules-tree-scanner.js +56 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.d.ts +17 -0
- package/dist/src/services/audit/scanners/skills-tree-scanner.js +46 -0
- package/dist/src/services/audit/static-service.d.ts +57 -0
- package/dist/src/services/audit/static-service.js +125 -0
- package/dist/src/services/audit/types.d.ts +69 -0
- package/dist/src/services/audit/types.js +13 -0
- package/dist/src/services/classify/classify-service.d.ts +42 -0
- package/dist/src/services/classify/classify-service.js +122 -0
- package/dist/src/services/classify/classify-types.d.ts +79 -0
- package/dist/src/services/classify/classify-types.js +90 -0
- package/dist/src/services/code-review/ocr-service.d.ts +129 -0
- package/dist/src/services/code-review/ocr-service.js +362 -0
- package/dist/src/services/config/config-migration.d.ts +32 -0
- package/dist/src/services/config/config-migration.js +111 -0
- package/dist/src/services/config/config-restore.d.ts +10 -0
- package/dist/src/services/config/config-restore.js +47 -0
- package/dist/src/services/config/config-rollback.d.ts +13 -0
- package/dist/src/services/config/config-rollback.js +26 -0
- package/dist/src/services/config/config-service.d.ts +36 -2
- package/dist/src/services/config/config-service.js +105 -0
- package/dist/src/services/config/config-types.d.ts +73 -0
- package/dist/src/services/config/config-types.js +28 -13
- package/dist/src/services/config/model-routing.js +5 -3
- package/dist/src/services/doctor/doctor-service.js +96 -0
- package/dist/src/services/ide/adapters/hermes-adapter.d.ts +21 -0
- package/dist/src/services/ide/adapters/hermes-adapter.js +51 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.d.ts +14 -0
- package/dist/src/services/ide/adapters/openclaw-adapter.js +42 -0
- package/dist/src/services/ide/ide-registry.js +7 -0
- package/dist/src/services/ide/ide-types.d.ts +1 -1
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.d.ts +31 -0
- package/dist/src/services/openspec/openspec-propose-from-doctor-service.js +95 -0
- package/dist/src/services/preferences/preferences-service.d.ts +6 -0
- package/dist/src/services/preferences/preferences-service.js +43 -0
- package/dist/src/services/preferences/preferences-types.d.ts +90 -0
- package/dist/src/services/preferences/preferences-types.js +38 -0
- package/dist/src/services/rd/rd-service.js +29 -1
- package/dist/src/services/skills/skill-conformance-service.d.ts +40 -0
- package/dist/src/services/skills/skill-conformance-service.js +136 -0
- package/dist/src/services/skills/skill-runbook-service.js +44 -10
- package/dist/src/services/skills/sync-service.d.ts +86 -0
- package/dist/src/services/skills/sync-service.js +271 -0
- package/dist/src/services/slice/slice-check-service.js +166 -13
- package/dist/src/services/slice/slice-check-types.d.ts +1 -1
- package/dist/src/services/standards/migrate-claude-rules-service.d.ts +19 -0
- package/dist/src/services/standards/migrate-claude-rules-service.js +193 -0
- package/dist/src/services/understand/understand-scan-service.js +15 -2
- package/dist/src/services/understand/understand-types.d.ts +26 -0
- package/dist/src/services/upgrade/1x-detector-service.d.ts +7 -0
- package/dist/src/services/upgrade/1x-detector-service.js +94 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.d.ts +56 -0
- package/dist/src/services/upgrade/gitignore-migrate-service.js +170 -0
- package/dist/src/services/upgrade/upgrade-service.d.ts +47 -0
- package/dist/src/services/upgrade/upgrade-service.js +381 -0
- package/dist/src/services/workflow/workflow-router-service.js +15 -4
- package/dist/src/services/workspace/claude-settings-template.d.ts +53 -0
- package/dist/src/services/workspace/claude-settings-template.js +133 -0
- package/dist/src/services/workspace/sid-naming-guard.d.ts +14 -0
- package/dist/src/services/workspace/sid-naming-guard.js +31 -0
- package/dist/src/services/workspace/workspace-archive-service.d.ts +19 -0
- package/dist/src/services/workspace/workspace-archive-service.js +32 -0
- package/dist/src/services/workspace/workspace-clean-service.d.ts +41 -0
- package/dist/src/services/workspace/workspace-clean-service.js +86 -0
- package/dist/src/services/workspace/workspace-service.d.ts +24 -0
- package/dist/src/services/workspace/workspace-service.js +124 -2
- package/dist/src/services/workspace/workspace-state-service.d.ts +7 -0
- package/dist/src/services/workspace/workspace-state-service.js +43 -0
- package/dist/src/shared/change-id.js +4 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +8 -2
- package/schemas/doctor-report.schema.json +1 -1
- package/scripts/install-skills.mjs +296 -12
- package/skills/peaks-doctor/SKILL.md +59 -0
- package/skills/peaks-doctor/references/doctor-check-catalog.md +31 -0
- package/skills/peaks-doctor/references/from-doctor-flow.md +64 -0
- package/skills/peaks-doctor/test_prompts.json +17 -0
- package/skills/peaks-ide/SKILL.md +2 -0
- package/skills/peaks-qa/SKILL.md +9 -7
- package/skills/peaks-qa/references/artifact-per-request.md +19 -5
- package/skills/peaks-qa/references/qa-perf-test-plan.md +6 -6
- package/skills/peaks-qa/references/qa-runbook.md +1 -1
- package/skills/peaks-rd/SKILL.md +25 -10
- package/skills/peaks-rd/references/ocr-integration.md +214 -0
- package/skills/peaks-rd/references/rd-fanout-contracts.md +70 -0
- package/skills/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/peaks-solo/SKILL.md +16 -4
- package/skills/peaks-solo/references/anchoring-and-session-info.md +9 -0
- package/skills/peaks-solo/references/step-0-55-1x-detection.md +82 -0
- package/skills/peaks-solo/references/workflow-gates-and-types.md +9 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks upgrade --to 2.0 — umbrella service for the 1.x → 2.0
|
|
3
|
+
* migration.
|
|
4
|
+
*
|
|
5
|
+
* Per the "one-key completion" + "minimal-user-operation" tenets
|
|
6
|
+
* (2026-06-11), the typical upgrade path is:
|
|
7
|
+
*
|
|
8
|
+
* $ npm i -g peaks-cli@2.0 # postinstall does everything
|
|
9
|
+
*
|
|
10
|
+
* OR (postinstall skipped / manual fallback):
|
|
11
|
+
*
|
|
12
|
+
* $ peaks upgrade --to 2.0
|
|
13
|
+
*
|
|
14
|
+
* The umbrella orchestrates 7 sub-commands:
|
|
15
|
+
* 1. config migrate (already ships as `peaks config migrate`)
|
|
16
|
+
* 2. standards migrate (`peaks standards migrate --from-claude-rules`)
|
|
17
|
+
* 3. memory extract (already ships as `peaks memory extract`)
|
|
18
|
+
* 4. hooks install (already ships as `peaks hooks install`)
|
|
19
|
+
* 5. skill sync (this session, `peaks skill sync --all`)
|
|
20
|
+
* 6. audit verify (already ships as `peaks audit red-lines`)
|
|
21
|
+
* 7. write upgrade record (in-process, .peaks/memory/upgrade-2.0-*.md)
|
|
22
|
+
*
|
|
23
|
+
* Each sub-step is a thin shell-out to the existing CLI; the
|
|
24
|
+
* umbrella's only in-process work is the audit and the upgrade
|
|
25
|
+
* record write. Sub-step failures are SOFT (logged + nextActions
|
|
26
|
+
* populated) so the umbrella never blocks a successful partial
|
|
27
|
+
* upgrade.
|
|
28
|
+
*/
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { join, dirname, relative, resolve } from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { runRedLinesAudit } from '../audit/red-lines-service.js';
|
|
34
|
+
import { savePreferences } from '../preferences/preferences-service.js';
|
|
35
|
+
import { migrateGitignoreFile } from './gitignore-migrate-service.js';
|
|
36
|
+
const STEPS = [
|
|
37
|
+
{ name: 'config-migrate', args: (p) => ['config', 'migrate', '--project', p, '--apply', '--json'] },
|
|
38
|
+
{ name: 'standards-migrate', args: (p) => ['standards', 'migrate', '--from-claude-rules', '--project', p, '--apply', '--json'] },
|
|
39
|
+
// memory extract is special: its --artifact takes literal file
|
|
40
|
+
// paths (memory-service rejects glob patterns via realpathSync).
|
|
41
|
+
// The umbrella expands the three documented patterns
|
|
42
|
+
// (skills/**/SKILL.md, CLAUDE.md, .claude/rules/**/*.md) on disk
|
|
43
|
+
// and passes the resulting literal list. See runUpgrade's special
|
|
44
|
+
// case below for the args resolution; the args function here is
|
|
45
|
+
// a placeholder so the STEPS table stays uniform.
|
|
46
|
+
{ name: 'memory-extract', args: (p) => ['memory', 'extract', '--project', p, '--json'] },
|
|
47
|
+
{ name: 'hooks-install', args: (p) => ['hooks', 'install', '--project', p, '--json'] },
|
|
48
|
+
{ name: 'skill-sync', args: (p) => ['skill', 'sync', '--all', '--project', p, '--json'] },
|
|
49
|
+
{ name: 'audit-verify', args: (p) => ['audit', 'red-lines', '--project', p, '--json'] },
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Walk `<root>` recursively and collect every file whose basename
|
|
53
|
+
* matches `predicate`. Returns absolute paths.
|
|
54
|
+
*
|
|
55
|
+
* Mirrors `readMarkdownFilesRecursive` in
|
|
56
|
+
* src/services/standards/migrate-claude-rules-service.ts so the
|
|
57
|
+
* umbrella does not pull a new glob dependency (Node 20+ engine
|
|
58
|
+
* constraint — `fs.globSync` requires Node 22+).
|
|
59
|
+
*/
|
|
60
|
+
function collectFilesRecursive(root, predicate) {
|
|
61
|
+
if (!existsSync(root))
|
|
62
|
+
return [];
|
|
63
|
+
const stat = statSync(root);
|
|
64
|
+
if (stat.isFile()) {
|
|
65
|
+
return predicate(root.split(/[\\/]/).pop() ?? '') ? [root] : [];
|
|
66
|
+
}
|
|
67
|
+
if (!stat.isDirectory())
|
|
68
|
+
return [];
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const entry of readdirSync(root)) {
|
|
71
|
+
const child = join(root, entry);
|
|
72
|
+
let childStat;
|
|
73
|
+
try {
|
|
74
|
+
childStat = statSync(child);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (childStat.isFile()) {
|
|
80
|
+
if (predicate(entry))
|
|
81
|
+
out.push(child);
|
|
82
|
+
}
|
|
83
|
+
else if (childStat.isDirectory()) {
|
|
84
|
+
out.push(...collectFilesRecursive(child, predicate));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the three documented memory-extract artifact patterns
|
|
91
|
+
* against a real project tree. Returns project-relative paths
|
|
92
|
+
* (memory-service joins them with --project root) so the
|
|
93
|
+
* realpathSync inside memory-service's assertInsideProject
|
|
94
|
+
* succeeds.
|
|
95
|
+
*
|
|
96
|
+
* Patterns:
|
|
97
|
+
* - skills/[asterisk][asterisk]/SKILL.md (project-root convention)
|
|
98
|
+
* - .claude/skills/[asterisk][asterisk]/SKILL.md (Claude-Code consumer convention; ice-cola)
|
|
99
|
+
* - CLAUDE.md
|
|
100
|
+
* - .claude/rules/[asterisk][asterisk]/[asterisk].md
|
|
101
|
+
*
|
|
102
|
+
* Returns an empty list when none of the roots exist. The
|
|
103
|
+
* caller marks the step skipped in that case.
|
|
104
|
+
*/
|
|
105
|
+
function expandMemoryArtifacts(projectRoot) {
|
|
106
|
+
const out = [];
|
|
107
|
+
// skills/**/SKILL.md (peaks-cli repo convention)
|
|
108
|
+
const skillFiles = collectFilesRecursive(join(projectRoot, 'skills'), (name) => name === 'SKILL.md');
|
|
109
|
+
for (const abs of skillFiles) {
|
|
110
|
+
out.push(relative(projectRoot, abs));
|
|
111
|
+
}
|
|
112
|
+
// .claude/skills/**/SKILL.md (Claude-Code consumer convention;
|
|
113
|
+
// surfaced by ice-cola dogfood 2026-06-12 — the 1.x install
|
|
114
|
+
// landed skills under .claude/skills/, not <root>/skills/)
|
|
115
|
+
const claudeSkillFiles = collectFilesRecursive(join(projectRoot, '.claude', 'skills'), (name) => name === 'SKILL.md');
|
|
116
|
+
for (const abs of claudeSkillFiles) {
|
|
117
|
+
out.push(relative(projectRoot, abs));
|
|
118
|
+
}
|
|
119
|
+
// CLAUDE.md (literal)
|
|
120
|
+
const claudeMd = join(projectRoot, 'CLAUDE.md');
|
|
121
|
+
if (existsSync(claudeMd) && statSync(claudeMd).isFile()) {
|
|
122
|
+
out.push('CLAUDE.md');
|
|
123
|
+
}
|
|
124
|
+
// .claude/rules/**/*.md
|
|
125
|
+
const claudeRules = collectFilesRecursive(join(projectRoot, '.claude', 'rules'), (name) => name.endsWith('.md'));
|
|
126
|
+
for (const abs of claudeRules) {
|
|
127
|
+
out.push(relative(projectRoot, abs));
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function read1xVersion(cwd) {
|
|
132
|
+
const home = process.env['HOME'] ?? process.env['USERPROFILE'] ?? '';
|
|
133
|
+
if (home.length === 0)
|
|
134
|
+
return null;
|
|
135
|
+
const global = join(home, '.peaks', 'config.json');
|
|
136
|
+
if (!existsSync(global))
|
|
137
|
+
return null;
|
|
138
|
+
try {
|
|
139
|
+
const raw = JSON.parse(readFileSync(global, 'utf8'));
|
|
140
|
+
if (typeof raw.version === 'string')
|
|
141
|
+
return raw.version;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// ignore
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function runStep(peaksBin, name, args, timeoutMs = 60_000) {
|
|
149
|
+
const start = Date.now();
|
|
150
|
+
// The global `peaks` shim is a `/bin/sh` symlink script (the
|
|
151
|
+
// npm install postinstall creates `peaks` → `peaks.sh` on
|
|
152
|
+
// Windows). cmd.exe (the default Windows shell) cannot run
|
|
153
|
+
// `.sh` scripts directly, so the shim fails with "unknown
|
|
154
|
+
// command 'migrate'" etc. The fix: prefer the local node
|
|
155
|
+
// binary + the peaks.js script path. The umbrella resolves
|
|
156
|
+
// the script path at startup; only falls back to `peaks` if
|
|
157
|
+
// no script path is available (Unix-only).
|
|
158
|
+
let command;
|
|
159
|
+
let spawnArgs;
|
|
160
|
+
if (peaksBin.includes('\\') || peaksBin.includes('/')) {
|
|
161
|
+
// peaksBin is a real path (e.g. /c/.../bin/peaks.js);
|
|
162
|
+
// invoke directly via node.
|
|
163
|
+
command = process.execPath;
|
|
164
|
+
spawnArgs = [peaksBin, ...args];
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// peaksBin is just "peaks" — best-effort shell exec.
|
|
168
|
+
command = peaksBin;
|
|
169
|
+
spawnArgs = args;
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const result = spawnSync(command, spawnArgs, {
|
|
173
|
+
encoding: 'utf8',
|
|
174
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
175
|
+
timeout: timeoutMs,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
name,
|
|
179
|
+
status: result.status === 0 ? 'pass' : 'fail',
|
|
180
|
+
exitCode: result.status,
|
|
181
|
+
stdout: result.stdout ?? '',
|
|
182
|
+
stderr: result.stderr ?? '',
|
|
183
|
+
durationMs: Date.now() - start,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
188
|
+
return {
|
|
189
|
+
name,
|
|
190
|
+
status: 'fail',
|
|
191
|
+
exitCode: null,
|
|
192
|
+
stdout: '',
|
|
193
|
+
stderr: message,
|
|
194
|
+
durationMs: Date.now() - start,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function writeUpgradeRecord(projectRoot, result) {
|
|
199
|
+
try {
|
|
200
|
+
const memoryDir = join(projectRoot, '.peaks', 'memory');
|
|
201
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
202
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
203
|
+
const file = join(memoryDir, `upgrade-2.0-${date}.md`);
|
|
204
|
+
const lines = [];
|
|
205
|
+
lines.push(`# Upgrade to peaks-cli 2.0 — ${date}`);
|
|
206
|
+
lines.push('');
|
|
207
|
+
lines.push(`> Auto-generated by \`peaks upgrade --to 2.0${result.applied ? ' --auto' : ''}\`.`);
|
|
208
|
+
lines.push(`> Per the "one-key completion" + "minimal-user-operation" tenets.`);
|
|
209
|
+
lines.push('');
|
|
210
|
+
if (result.fromVersion !== null) {
|
|
211
|
+
lines.push(`**From version**: ${result.fromVersion}`);
|
|
212
|
+
}
|
|
213
|
+
lines.push(`**To version**: 2.0.0`);
|
|
214
|
+
lines.push(`**Project root**: \`${result.projectRoot}\``);
|
|
215
|
+
lines.push('');
|
|
216
|
+
lines.push('## Sub-step results');
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('| step | status | exitCode | durationMs |');
|
|
219
|
+
lines.push('|------|--------|----------|------------|');
|
|
220
|
+
for (const step of result.steps) {
|
|
221
|
+
lines.push(`| ${step.name} | ${step.status} | ${step.exitCode ?? 'n/a'} | ${step.durationMs} |`);
|
|
222
|
+
}
|
|
223
|
+
lines.push('');
|
|
224
|
+
if (result.auditBefore !== null || result.auditAfter !== null) {
|
|
225
|
+
lines.push('## Audit snapshot');
|
|
226
|
+
lines.push('');
|
|
227
|
+
if (result.auditBefore !== null) {
|
|
228
|
+
lines.push(`- Before: totalRedLines=${result.auditBefore.totalRedLines}, cliBacked=${result.auditBefore.cliBacked}`);
|
|
229
|
+
}
|
|
230
|
+
if (result.auditAfter !== null) {
|
|
231
|
+
lines.push(`- After: totalRedLines=${result.auditAfter.totalRedLines}, cliBacked=${result.auditAfter.cliBacked}`);
|
|
232
|
+
}
|
|
233
|
+
lines.push('');
|
|
234
|
+
}
|
|
235
|
+
lines.push('## Next actions');
|
|
236
|
+
lines.push('');
|
|
237
|
+
for (const a of result.nextActions) {
|
|
238
|
+
lines.push(`- ${a}`);
|
|
239
|
+
}
|
|
240
|
+
writeFileSync(file, lines.join('\n') + '\n', 'utf8');
|
|
241
|
+
return file;
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
process.stderr.write(`peaks upgrade: failed to write upgrade record: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export function runUpgrade(input) {
|
|
249
|
+
// Resolve the peaks binary. Default: the peaks.js script
|
|
250
|
+
// co-located with this compiled module (the user just installed
|
|
251
|
+
// peaks-cli globally, but the global `peaks` shim is a .sh
|
|
252
|
+
// script that cmd.exe can't run on Windows). Falling back
|
|
253
|
+
// to just "peaks" lets the umbrella work when invoked from
|
|
254
|
+
// a Unix-style environment that can run the shim directly.
|
|
255
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
256
|
+
// Walk up from the compiled location to find bin/peaks.js.
|
|
257
|
+
// The compiled service lives at dist/src/services/upgrade/upgrade-service.js;
|
|
258
|
+
// bin/peaks.js is at the peaks-cli root.
|
|
259
|
+
const peaksBin = input.peaksBin ??
|
|
260
|
+
resolve(here, '..', '..', '..', '..', 'bin', 'peaks.js');
|
|
261
|
+
const fallbackPeaks = 'peaks';
|
|
262
|
+
const resolvedPeaksBin = existsSync(peaksBin) ? peaksBin : fallbackPeaks;
|
|
263
|
+
const fromVersion = read1xVersion(input.projectRoot);
|
|
264
|
+
const steps = [];
|
|
265
|
+
const warnings = [];
|
|
266
|
+
const nextActions = [];
|
|
267
|
+
// Ensure .peaks/preferences.json exists. This is the file the
|
|
268
|
+
// 1.x detector keys off — without it, `peaks upgrade --detect-1x`
|
|
269
|
+
// keeps returning isOneX=true after a successful upgrade and the
|
|
270
|
+
// user gets stuck in a re-prompt loop. savePreferences with an
|
|
271
|
+
// empty override merges with DEFAULT_PREFERENCES and writes; if
|
|
272
|
+
// the file already exists the user's values are preserved.
|
|
273
|
+
// Real bug surfaced by ice-cola dogfood 2026-06-12.
|
|
274
|
+
try {
|
|
275
|
+
savePreferences(input.projectRoot, {});
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
warnings.push(`ensure-preferences failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
279
|
+
}
|
|
280
|
+
// Migrate .gitignore so 2.0 tracked artifacts
|
|
281
|
+
// (.peaks/standards/, .peaks/memory/*.md durable memories,
|
|
282
|
+
// .peaks/PROJECT.md) aren't silently hidden by a 1.x wholesale
|
|
283
|
+
// `/.peaks/` ignore rule. Real bug surfaced by ice-cola dogfood
|
|
284
|
+
// 2026-06-12: every consumer artifact was being dropped from git
|
|
285
|
+
// status. Service is idempotent + creates a timestamped backup
|
|
286
|
+
// before any write.
|
|
287
|
+
try {
|
|
288
|
+
const giResult = migrateGitignoreFile({ projectRoot: input.projectRoot, apply: true });
|
|
289
|
+
if (giResult.changed && giResult.appliedWrite && giResult.backupPath !== null) {
|
|
290
|
+
nextActions.push(`Updated .gitignore — removed stale wholesale .peaks rule(s): ${giResult.removedRules.join(', ')}. Backup at ${giResult.backupPath}.`);
|
|
291
|
+
}
|
|
292
|
+
else if (giResult.missing) {
|
|
293
|
+
warnings.push('gitignore-migrate skipped: project has no .gitignore');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
warnings.push(`gitignore-migrate failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
298
|
+
}
|
|
299
|
+
// Audit BEFORE the upgrade (baseline)
|
|
300
|
+
let auditBefore = null;
|
|
301
|
+
try {
|
|
302
|
+
const r = runRedLinesAudit({ projectRoot: input.projectRoot });
|
|
303
|
+
auditBefore = { totalRedLines: r.audit.totalRedLines, cliBacked: r.audit.cliBacked };
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
warnings.push(`audit-before failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
307
|
+
}
|
|
308
|
+
// Run the 6 sub-steps
|
|
309
|
+
for (const step of STEPS) {
|
|
310
|
+
if (step.name === 'memory-extract') {
|
|
311
|
+
// Special case: expand the three glob patterns to literal
|
|
312
|
+
// paths before spawning. memory-service rejects literal
|
|
313
|
+
// '**' in artifact paths (assertInsideProject's realpathSync
|
|
314
|
+
// throws ENOENT) and refuses to run without --artifact.
|
|
315
|
+
const artifacts = expandMemoryArtifacts(input.projectRoot);
|
|
316
|
+
if (artifacts.length === 0) {
|
|
317
|
+
steps.push({
|
|
318
|
+
name: 'memory-extract',
|
|
319
|
+
status: 'skipped',
|
|
320
|
+
exitCode: null,
|
|
321
|
+
stdout: '',
|
|
322
|
+
stderr: 'no skills/, CLAUDE.md, or .claude/rules/ artifacts found in the project',
|
|
323
|
+
durationMs: 0,
|
|
324
|
+
});
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const args = ['memory', 'extract', '--project', input.projectRoot, '--artifact', ...artifacts, '--apply', '--json'];
|
|
328
|
+
const r = runStep(resolvedPeaksBin, 'memory-extract', args);
|
|
329
|
+
steps.push(r);
|
|
330
|
+
if (r.status === 'fail') {
|
|
331
|
+
warnings.push(`memory-extract failed: ${r.stderr.slice(0, 200)}`);
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const args = step.args(input.projectRoot);
|
|
336
|
+
const r = runStep(resolvedPeaksBin, step.name, args);
|
|
337
|
+
steps.push(r);
|
|
338
|
+
if (r.status === 'fail') {
|
|
339
|
+
warnings.push(`${step.name} failed: ${r.stderr.slice(0, 200)}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Audit AFTER the upgrade (verify)
|
|
343
|
+
let auditAfter = null;
|
|
344
|
+
try {
|
|
345
|
+
const r = runRedLinesAudit({ projectRoot: input.projectRoot });
|
|
346
|
+
auditAfter = { totalRedLines: r.audit.totalRedLines, cliBacked: r.audit.cliBacked };
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
warnings.push(`audit-after failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
|
+
}
|
|
351
|
+
const passedCount = steps.filter((s) => s.status === 'pass').length;
|
|
352
|
+
const failedCount = steps.filter((s) => s.status === 'fail').length;
|
|
353
|
+
const skippedCount = steps.filter((s) => s.status === 'skipped').length;
|
|
354
|
+
const applied = failedCount === 0;
|
|
355
|
+
if (failedCount > 0) {
|
|
356
|
+
nextActions.push(`${failedCount} sub-step(s) failed. Run \`peaks upgrade --to 2.0\` again to retry the failed steps.`);
|
|
357
|
+
}
|
|
358
|
+
if (input.auto !== true) {
|
|
359
|
+
nextActions.push('Run `peaks audit red-lines --project .` to verify the L2 catalog is healthy.');
|
|
360
|
+
}
|
|
361
|
+
nextActions.push('See `docs/UPGRADING-2.0.md` for the manual fallback if this auto-upgrade fails.');
|
|
362
|
+
// Write the upgrade record (always, even on partial failure —
|
|
363
|
+
// the user gets a forensic artifact either way)
|
|
364
|
+
const partial = {
|
|
365
|
+
applied,
|
|
366
|
+
fromVersion,
|
|
367
|
+
toVersion: '2.0.0',
|
|
368
|
+
projectRoot: input.projectRoot,
|
|
369
|
+
steps,
|
|
370
|
+
passedCount,
|
|
371
|
+
failedCount,
|
|
372
|
+
skippedCount,
|
|
373
|
+
auditBefore,
|
|
374
|
+
auditAfter,
|
|
375
|
+
upgradeRecordPath: null,
|
|
376
|
+
nextActions,
|
|
377
|
+
warnings,
|
|
378
|
+
};
|
|
379
|
+
const upgradeRecordPath = writeUpgradeRecord(input.projectRoot, partial);
|
|
380
|
+
return { ...partial, upgradeRecordPath };
|
|
381
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { DEFAULT_CONFIG } from '../config/config-types.js';
|
|
2
1
|
import { getConfiguredExecutionModelId, STRONGEST_MODEL_ID } from '../config/model-routing.js';
|
|
3
2
|
import { getLocalArtifactPath } from '../artifacts/workspace-service.js';
|
|
4
3
|
import { createRdSwarmPlan } from '../rd/rd-service.js';
|
|
@@ -167,9 +166,21 @@ export function createWorkflowRouterPlan(request) {
|
|
|
167
166
|
validateChangeIdOrThrow(request.changeId);
|
|
168
167
|
const goal = normalizeGoal(request.goal);
|
|
169
168
|
const maxWorkers = request.maxWorkers ?? 40;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
// Slice 2.0.1-bug1 round 3: project policy defaults. The slim 2.0.1 DEFAULT_CONFIG
|
|
170
|
+
// no longer carries economyMode / swarmMode (those moved to per-project preferences),
|
|
171
|
+
// so we cannot fall back to `DEFAULT_CONFIG.economyMode` / `swarmMode` here. Both
|
|
172
|
+
// flags are project-policy opt-outs: the absence of an explicit `false` means
|
|
173
|
+
// "enabled" (matches the pre-2.0.1 implicit default).
|
|
174
|
+
const economyMode = request.config?.economyMode ?? true;
|
|
175
|
+
const swarmMode = request.config?.swarmMode ?? true;
|
|
176
|
+
// Pre-2.0.1 DEFAULT_CONFIG carried an implicit `minimax-2.7` provider
|
|
177
|
+
// for test fixtures that did not pass `config.providers`. The slim
|
|
178
|
+
// DEFAULT_CONFIG removed that field, so we re-supply it here only when
|
|
179
|
+
// the caller did not pass any providers at all. An explicit empty
|
|
180
|
+
// object (`config: { providers: {} }`) still surfaces the "must be
|
|
181
|
+
// configured" error from `getConfiguredExecutionModelId`.
|
|
182
|
+
const effectiveProviders = request.config?.providers ?? { minimax: { model: 'minimax-2.7' } };
|
|
183
|
+
const executionModelId = economyMode !== false ? getConfiguredExecutionModelId(effectiveProviders) : STRONGEST_MODEL_ID;
|
|
173
184
|
const modeStatus = createModeStatus(economyMode, swarmMode, executionModelId, economyMode ? 'config.providers' : 'planner-reviewer-strongest-model');
|
|
174
185
|
const soloMode = getSoloMode(request.mode, request.soloMode);
|
|
175
186
|
const decisionProfile = getDecisionProfileSummary(request.mode, soloMode);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2.0.1-bug3-fact-forcing-bypass — pure-data template for the
|
|
3
|
+
* consumer-project `.claude/settings.local.json` file.
|
|
4
|
+
*
|
|
5
|
+
* The template is a PreToolUse hook allow-list that bypasses the
|
|
6
|
+
* Claude Code [Fact-Forcing Gate] for tool calls whose paths or
|
|
7
|
+
* commands target the peaks-managed `.peaks/` workspace. Without this
|
|
8
|
+
* bypass, `peaks workspace init` (Step 0 of every peaks-solo session)
|
|
9
|
+
* is unrunnable in a consumer project because the gate blocks the
|
|
10
|
+
* very first Write.
|
|
11
|
+
*
|
|
12
|
+
* The template is a pure-data function (no filesystem, no clock) so
|
|
13
|
+
* it can be unit-tested in isolation and so the on-disk file matches
|
|
14
|
+
* the in-memory template byte-for-byte.
|
|
15
|
+
*
|
|
16
|
+
* Two matchers are emitted:
|
|
17
|
+
* 1. `Write|Edit|MultiEdit` — a node one-liner that path-matches
|
|
18
|
+
* `.peaks/_runtime/` and `.peaks/<changeId>/`. Exits 0 (allow)
|
|
19
|
+
* for those paths, non-zero (deny → fall through to gate) for
|
|
20
|
+
* everything else.
|
|
21
|
+
* 2. `Bash` — a node one-liner that allows command strings starting
|
|
22
|
+
* with `peaks ` (whitelisted subcommand prefix). Exits 0 for
|
|
23
|
+
* `peaks <subcommand> ...`, non-zero otherwise.
|
|
24
|
+
*
|
|
25
|
+
* The Bash allow-list is conservative: it whitelists the documented
|
|
26
|
+
* peaks subcommands the skill family invokes during Step 0 (workspace,
|
|
27
|
+
* skill presence, request, session, scan, sub-agent, gate, standards,
|
|
28
|
+
* hooks, statusline). See peaks-solo/references/runbook.md for the
|
|
29
|
+
* canonical list.
|
|
30
|
+
*/
|
|
31
|
+
export declare const CLAUDE_SETTINGS_LOCAL_FILENAME = ".claude/settings.local.json";
|
|
32
|
+
type ClaudeHookCommand = {
|
|
33
|
+
type: 'command';
|
|
34
|
+
command: string;
|
|
35
|
+
};
|
|
36
|
+
type ClaudePreToolUseEntry = {
|
|
37
|
+
matcher: string;
|
|
38
|
+
hooks: ClaudeHookCommand[];
|
|
39
|
+
};
|
|
40
|
+
type ClaudeSettingsLocal = {
|
|
41
|
+
hooks: {
|
|
42
|
+
PreToolUse: ClaudePreToolUseEntry[];
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Build the full template object. The shape is the subset of Claude
|
|
47
|
+
* Code's `.claude/settings.local.json` schema that PreToolUse hooks
|
|
48
|
+
* need — we do not emit the `permissions` block because the fact-
|
|
49
|
+
* forcing gate is a core feature that PreToolUse hooks can short-
|
|
50
|
+
* circuit but that the `permissions` block cannot.
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildClaudeSettingsLocalJson(): ClaudeSettingsLocal;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice 2.0.1-bug3-fact-forcing-bypass — pure-data template for the
|
|
3
|
+
* consumer-project `.claude/settings.local.json` file.
|
|
4
|
+
*
|
|
5
|
+
* The template is a PreToolUse hook allow-list that bypasses the
|
|
6
|
+
* Claude Code [Fact-Forcing Gate] for tool calls whose paths or
|
|
7
|
+
* commands target the peaks-managed `.peaks/` workspace. Without this
|
|
8
|
+
* bypass, `peaks workspace init` (Step 0 of every peaks-solo session)
|
|
9
|
+
* is unrunnable in a consumer project because the gate blocks the
|
|
10
|
+
* very first Write.
|
|
11
|
+
*
|
|
12
|
+
* The template is a pure-data function (no filesystem, no clock) so
|
|
13
|
+
* it can be unit-tested in isolation and so the on-disk file matches
|
|
14
|
+
* the in-memory template byte-for-byte.
|
|
15
|
+
*
|
|
16
|
+
* Two matchers are emitted:
|
|
17
|
+
* 1. `Write|Edit|MultiEdit` — a node one-liner that path-matches
|
|
18
|
+
* `.peaks/_runtime/` and `.peaks/<changeId>/`. Exits 0 (allow)
|
|
19
|
+
* for those paths, non-zero (deny → fall through to gate) for
|
|
20
|
+
* everything else.
|
|
21
|
+
* 2. `Bash` — a node one-liner that allows command strings starting
|
|
22
|
+
* with `peaks ` (whitelisted subcommand prefix). Exits 0 for
|
|
23
|
+
* `peaks <subcommand> ...`, non-zero otherwise.
|
|
24
|
+
*
|
|
25
|
+
* The Bash allow-list is conservative: it whitelists the documented
|
|
26
|
+
* peaks subcommands the skill family invokes during Step 0 (workspace,
|
|
27
|
+
* skill presence, request, session, scan, sub-agent, gate, standards,
|
|
28
|
+
* hooks, statusline). See peaks-solo/references/runbook.md for the
|
|
29
|
+
* canonical list.
|
|
30
|
+
*/
|
|
31
|
+
export const CLAUDE_SETTINGS_LOCAL_FILENAME = '.claude/settings.local.json';
|
|
32
|
+
/**
|
|
33
|
+
* Subcommand allow-list for the Bash matcher. The matcher allows any
|
|
34
|
+
* command that starts with `peaks <subcommand>` for one of these
|
|
35
|
+
* subcommands. Keep this list in sync with peaks-solo/references/runbook.md.
|
|
36
|
+
*/
|
|
37
|
+
const PEAKS_SUBCOMMAND_ALLOWLIST = [
|
|
38
|
+
'workspace',
|
|
39
|
+
'skill',
|
|
40
|
+
'request',
|
|
41
|
+
'session',
|
|
42
|
+
'scan',
|
|
43
|
+
'sub-agent',
|
|
44
|
+
'gate',
|
|
45
|
+
'standards',
|
|
46
|
+
'hooks',
|
|
47
|
+
'statusline',
|
|
48
|
+
'memory',
|
|
49
|
+
'openspec',
|
|
50
|
+
'workflow',
|
|
51
|
+
'doctor',
|
|
52
|
+
'upgrade'
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* Build the Bash matcher command. The command is a node -e one-liner
|
|
56
|
+
* that reads its candidate command string from argv[2] and exits 0
|
|
57
|
+
* iff the command starts with `peaks <whitelisted-subcommand> ` (or
|
|
58
|
+
* is exactly `peaks <whitelisted-subcommand>` with no trailing args).
|
|
59
|
+
*
|
|
60
|
+
* The list is serialised as a JSON array literal embedded in the
|
|
61
|
+
* command string so we avoid regex special-character pitfalls and
|
|
62
|
+
* keep the allow-list declarative.
|
|
63
|
+
*/
|
|
64
|
+
function buildBashHookCommand() {
|
|
65
|
+
const allowlistLiteral = JSON.stringify(PEAKS_SUBCOMMAND_ALLOWLIST);
|
|
66
|
+
// The command reads process.argv[2] (the tool-call command string),
|
|
67
|
+
// checks it starts with `peaks `, splits on whitespace, and looks
|
|
68
|
+
// up the second token in the allowlist. Exit 0 = allow, exit 1 =
|
|
69
|
+
// deny (so the gate fires for non-peaks commands).
|
|
70
|
+
return ('const c=process.argv[1]||"";' +
|
|
71
|
+
'if(!c.startsWith("peaks "))process.exit(1);' +
|
|
72
|
+
'const sub=c.slice(6).trim().split(/\\s+/)[0];' +
|
|
73
|
+
`if(${allowlistLiteral}.indexOf(sub)===-1)process.exit(1);` +
|
|
74
|
+
'process.exit(0)');
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the Write|Edit|MultiEdit matcher command. The command reads
|
|
78
|
+
* the candidate file path from argv[2] and exits 0 iff the path
|
|
79
|
+
* contains `.peaks/_runtime/` or `.peaks/<changeId>/` (the change-id
|
|
80
|
+
* segment is the next path component after `.peaks/`). All other
|
|
81
|
+
* paths exit 1 so the gate fires normally.
|
|
82
|
+
*
|
|
83
|
+
* The matcher is intentionally narrow: it only fires for tools that
|
|
84
|
+
* take a `file_path` (Write/Edit/MultiEdit) and for the Bash
|
|
85
|
+
* subcommand allow-list. It does NOT silently allow arbitrary paths
|
|
86
|
+
* under `.peaks/<changeId>/` — only those matching the documented
|
|
87
|
+
* pattern. Future slice work can broaden the allow-list if the
|
|
88
|
+
* peaks-solo workflow needs more paths.
|
|
89
|
+
*/
|
|
90
|
+
function buildWriteHookCommand() {
|
|
91
|
+
// Path-matching: allow when the path contains `.peaks/_runtime/`
|
|
92
|
+
// OR when the second `.peaks/` segment starts with anything that
|
|
93
|
+
// looks like a change-id (kebab-case slug). Exit 0 for allow, exit
|
|
94
|
+
// 1 for deny.
|
|
95
|
+
return ('const p=process.argv[1]||"";' +
|
|
96
|
+
'if(p.includes(".peaks/_runtime/"))process.exit(0);' +
|
|
97
|
+
'const m=p.match(/\\.peaks\\/([a-z0-9][a-z0-9.-]*)\\//);' +
|
|
98
|
+
'if(m&&m[1]&&m[1]!=="_runtime"&&m[1]!=="_dogfood"&&m[1]!=="_sub_agents"&&m[1]!=="_archive"&&m[1]!=="memory"&&m[1]!=="issues"&&m[1]!=="sops"&&m[1]!=="retrospective"&&m[1]!=="project-scan"&&m[1]!=="perf-baseline")process.exit(0);' +
|
|
99
|
+
'process.exit(1)');
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build the full template object. The shape is the subset of Claude
|
|
103
|
+
* Code's `.claude/settings.local.json` schema that PreToolUse hooks
|
|
104
|
+
* need — we do not emit the `permissions` block because the fact-
|
|
105
|
+
* forcing gate is a core feature that PreToolUse hooks can short-
|
|
106
|
+
* circuit but that the `permissions` block cannot.
|
|
107
|
+
*/
|
|
108
|
+
export function buildClaudeSettingsLocalJson() {
|
|
109
|
+
return {
|
|
110
|
+
hooks: {
|
|
111
|
+
PreToolUse: [
|
|
112
|
+
{
|
|
113
|
+
matcher: 'Write|Edit|MultiEdit',
|
|
114
|
+
hooks: [
|
|
115
|
+
{
|
|
116
|
+
type: 'command',
|
|
117
|
+
command: buildWriteHookCommand()
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
matcher: 'Bash',
|
|
123
|
+
hooks: [
|
|
124
|
+
{
|
|
125
|
+
type: 'command',
|
|
126
|
+
command: buildBashHookCommand()
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SID naming guard. Enforces the "two-axis" convention from spec §0:
|
|
3
|
+
* session id: YYYY-MM-DD-session-<3-6 chars lowercase alnum>
|
|
4
|
+
* change id: kebab-case
|
|
5
|
+
*
|
|
6
|
+
* Spec §8.7 — bare forms (sid-3 / sid-h / sid-r / unknown-sid) are
|
|
7
|
+
* migrated to `_archive/invalid-sids/`, NOT tolerated.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SID_FORMAT_DESCRIPTION = "<YYYY-MM-DD>-session-<3-6 chars lowercase alnum>, e.g. 2026-06-11-session-abc123";
|
|
10
|
+
export declare function isValidSessionId(sid: string): boolean;
|
|
11
|
+
export declare function isValidChangeId(cid: string): boolean;
|
|
12
|
+
export declare function isBareSid(name: string): boolean;
|
|
13
|
+
export declare function assertValidSessionId(sid: string): void;
|
|
14
|
+
export declare function assertValidChangeId(cid: string): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SID naming guard. Enforces the "two-axis" convention from spec §0:
|
|
3
|
+
* session id: YYYY-MM-DD-session-<3-6 chars lowercase alnum>
|
|
4
|
+
* change id: kebab-case
|
|
5
|
+
*
|
|
6
|
+
* Spec §8.7 — bare forms (sid-3 / sid-h / sid-r / unknown-sid) are
|
|
7
|
+
* migrated to `_archive/invalid-sids/`, NOT tolerated.
|
|
8
|
+
*/
|
|
9
|
+
export const SID_FORMAT_DESCRIPTION = '<YYYY-MM-DD>-session-<3-6 chars lowercase alnum>, e.g. 2026-06-11-session-abc123';
|
|
10
|
+
const VALID_SID_REGEX = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])-session-[0-9a-z]{3,6}$/;
|
|
11
|
+
const BARE_SID_REGEX = /^(sid-[a-z0-9]+|unknown-sid)$/;
|
|
12
|
+
const VALID_CHANGE_ID_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
13
|
+
export function isValidSessionId(sid) {
|
|
14
|
+
return VALID_SID_REGEX.test(sid);
|
|
15
|
+
}
|
|
16
|
+
export function isValidChangeId(cid) {
|
|
17
|
+
return VALID_CHANGE_ID_REGEX.test(cid);
|
|
18
|
+
}
|
|
19
|
+
export function isBareSid(name) {
|
|
20
|
+
return BARE_SID_REGEX.test(name);
|
|
21
|
+
}
|
|
22
|
+
export function assertValidSessionId(sid) {
|
|
23
|
+
if (!isValidSessionId(sid)) {
|
|
24
|
+
throw new Error(`NAMING_INVALID: session id "${sid}" does not match required format ${SID_FORMAT_DESCRIPTION}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function assertValidChangeId(cid) {
|
|
28
|
+
if (!isValidChangeId(cid)) {
|
|
29
|
+
throw new Error(`NAMING_INVALID: change id "${cid}" must be kebab-case (lowercase alnum and dashes only)`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ArchivePlan {
|
|
2
|
+
sid: string;
|
|
3
|
+
sourcePath: string;
|
|
4
|
+
targetPath: string;
|
|
5
|
+
sourceExists: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ArchiveOptions {
|
|
8
|
+
sid: string;
|
|
9
|
+
apply: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ArchiveResult {
|
|
12
|
+
moved: string[];
|
|
13
|
+
skipped: {
|
|
14
|
+
sid: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
18
|
+
export declare function planArchive(projectRoot: string, sid: string): ArchivePlan;
|
|
19
|
+
export declare function archiveSession(projectRoot: string, options: ArchiveOptions): ArchiveResult;
|