peaks-loop 4.0.0-beta.4 → 4.0.0-beta.6
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 +5 -0
- package/CHANGELOG.md +16 -0
- package/README-en.md +4 -0
- package/README.md +4 -0
- package/dist/src/cli/commands/adapter-commands-s2a.d.ts +34 -0
- package/dist/src/cli/commands/adapter-commands-s2a.js +92 -0
- package/dist/src/cli/commands/core/skill-command.js +22 -0
- package/dist/src/cli/commands/polyrepo-commands.d.ts +37 -0
- package/dist/src/cli/commands/polyrepo-commands.js +155 -0
- package/dist/src/cli/commands/runtime-commands.d.ts +52 -0
- package/dist/src/cli/commands/runtime-commands.js +150 -0
- package/dist/src/cli/commands/skill-search-commands.d.ts +25 -0
- package/dist/src/cli/commands/skill-search-commands.js +68 -0
- package/dist/src/cli/program.js +16 -0
- package/dist/src/services/adapter/adapter-registry.d.ts +48 -0
- package/dist/src/services/adapter/adapter-registry.js +186 -0
- package/dist/src/services/context/context-schema.d.ts +6 -6
- package/dist/src/services/polyrepo/polyrepo-dispatcher.d.ts +34 -0
- package/dist/src/services/polyrepo/polyrepo-dispatcher.js +119 -0
- package/dist/src/services/polyrepo/polyrepo-scanner.d.ts +9 -0
- package/dist/src/services/polyrepo/polyrepo-scanner.js +94 -0
- package/dist/src/services/polyrepo/polyrepo-service.d.ts +34 -0
- package/dist/src/services/polyrepo/polyrepo-service.js +49 -0
- package/dist/src/services/polyrepo/polyrepo-types.d.ts +55 -0
- package/dist/src/services/polyrepo/polyrepo-types.js +1 -0
- package/dist/src/services/runtime/runtime-detector.d.ts +15 -0
- package/dist/src/services/runtime/runtime-detector.js +62 -0
- package/dist/src/services/runtime/runtime-service.d.ts +59 -0
- package/dist/src/services/runtime/runtime-service.js +51 -0
- package/dist/src/services/runtime/vendor-adapter.d.ts +59 -0
- package/dist/src/services/runtime/vendor-adapter.js +1 -0
- package/dist/src/services/runtime/vendors/claude-code.d.ts +7 -0
- package/dist/src/services/runtime/vendors/claude-code.js +39 -0
- package/dist/src/services/runtime/vendors/codex.d.ts +7 -0
- package/dist/src/services/runtime/vendors/codex.js +43 -0
- package/dist/src/services/runtime/vendors/copilot.d.ts +7 -0
- package/dist/src/services/runtime/vendors/copilot.js +40 -0
- package/dist/src/services/skill/skill-search-service.d.ts +88 -0
- package/dist/src/services/skill/skill-search-service.js +228 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-code/SKILL.md +1 -8
- package/skills/peaks-code/references/external-references.md +1 -3
- package/skills/peaks-code/references/external-skill-invocation.md +1 -1
- package/skills/peaks-code/references/runbook.md +1 -3
- package/skills/peaks-code/references/step-0-55-1x-detection.md +2 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
- package/skills/peaks-solo/SKILL.md +197 -0
- package/skills/peaks-solo/references/fallback-tool-inventory.md +46 -0
- package/skills/peaks-solo/references/sediment-prompt-template.md +62 -0
- package/skills/peaks-solo/references/triage-decision-table.md +31 -0
- package/skills/peaks-code/references/openspec-workflow.md +0 -81
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"irreversible-action gates"
|
|
35
35
|
],
|
|
36
36
|
"skills": [
|
|
37
|
+
{
|
|
38
|
+
"name": "peaks-solo",
|
|
39
|
+
"path": "./skills/peaks-solo",
|
|
40
|
+
"description": "Dispatcher (分诊员) — natural-language front door for the Peaks-Loop skill family. Use when the user describes a task in NL and does not know which peaks-* skill fits. NOT for code/content/doctor/issue-fix-orchestrator/SOP work (use the specific skill)."
|
|
41
|
+
},
|
|
37
42
|
{
|
|
38
43
|
"name": "peaks-code",
|
|
39
44
|
"path": "./skills/peaks-code",
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## 4.0.0-beta.5 — 2026-07-08
|
|
6
|
+
|
|
7
|
+
### Added — peaks-solo dispatcher (分诊员)
|
|
8
|
+
|
|
9
|
+
- **`peaks-solo` skill** — `skills/peaks-solo/SKILL.md` + 3 references (triage / fallback / sediment). Natural-language front door for the Peaks-Loop skill family. Use when the user describes a task in NL and does not know which peaks-* skill fits. 0 breaking change: 3.x / 4.x `/peaks-code` / `/peaks-content` / `/peaks-doctor` etc. continue to work.
|
|
10
|
+
- **`peaks skill search` CLI** — `src/services/skill/skill-search-service.ts` + `src/cli/commands/skill-search-commands.ts`. Query / tag / domain filters; substring match; structured JSON output. Used by `peaks-solo` to find the right leaf skill. Available as a top-level primitive (not dispatcher-specific).
|
|
11
|
+
- **Sub-skills unchanged** — `peaks-code / peaks-content / peaks-doctor / peaks-issue-fix-orchestrator / peaks-sop / etc.` are NOT modified. peaks-solo sits alongside, not on top.
|
|
12
|
+
|
|
13
|
+
### Verification (4.0.0-beta.5)
|
|
14
|
+
|
|
15
|
+
- `peaks skill list` shows `peaks-solo` first
|
|
16
|
+
- `peaks skill search --query "code"` returns `peaks-code` with matchScore > 0
|
|
17
|
+
- `peaks skill search --query "xxxxxxxxxxxxx"` returns `[]` (no error)
|
|
18
|
+
- `pnpm vitest run` — full regression green; `peaks-code / peaks-content / peaks-doctor` tests unchanged
|
|
19
|
+
- `peaks code --help` / `peaks content --help` / `peaks doctor --help` — exit 0, behavior unchanged
|
|
20
|
+
|
|
5
21
|
## 4.0.0-beta.4 — 2026-07-08
|
|
6
22
|
|
|
7
23
|
Loop Engineering crystallization is now the product surface. The 4.0.0-beta.3 line shipped the framework; this release ships the post-run crystallization engine, the Darwin-style ratchet, the bundle share / desktop extension surface, and the karpathy-engineered red-line set that locks them together. Every durable-change entry is gated on a real, completed run; every evolution round is gated on an independent-context evaluation.
|
package/README-en.md
CHANGED
|
@@ -33,6 +33,10 @@ The product is governed by a four-layer asset model and a karpathy × darwin dua
|
|
|
33
33
|
|
|
34
34
|
Summon them, they handle the work. Stand them down when it's done. **No skipped steps. No half-finished hand-offs.** No new AI CLI to learn — it sits on top of Claude Code, Codex, or Copilot you already run.
|
|
35
35
|
|
|
36
|
+
### peaks-solo is the dispatcher (new in 4.0.0-beta.5)
|
|
37
|
+
|
|
38
|
+
If you **don't know which peaks-* skill to use**, just type `/peaks-solo` and describe your task in natural language. It triages for you: dispatches to a matching leaf, or self-plans with deep-search / WebSearch / Bash / Edit markdown if no leaf fits, then asks whether to sediment the result. `/peaks-code` / `/peaks-content` / `/peaks-doctor` etc. continue to work — **zero breaking changes**.
|
|
39
|
+
|
|
36
40
|
**Every gate has a hard exit.** Audit fails = stop. QA fails = stop. Any gate fails = full stop. You don't chase it, you don't patch it. You decide, it runs the next gate.
|
|
37
41
|
|
|
38
42
|
#### Gates aren't decoration — 5,439 tests passed · 19 skipped · 0 failed
|
package/README.md
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
|
|
34
34
|
召之即来,事完收队,**不跳步,不半截扔给你**。它不是新发明一个 AI CLI,而是架在你已有的 Claude Code / Codex / Copilot 之上,把它们都调成同一支团队,只对你这一个开发者的口味。
|
|
35
35
|
|
|
36
|
+
### peaks-solo 是分诊员(新增,4.0.0-beta.5)
|
|
37
|
+
|
|
38
|
+
如果**你不知道该用哪个 peaks-* 技能**,直接用 `/peaks-solo` 描述你的诉求就行。它会替你分诊:有合适的 leaf 就透明转交,没合适的就自己规划 + 跑(deep-search / WebSearch / Bash / Edit markdown),跑完回头问你要不要沉淀。`/peaks-code` / `/peaks-content` / `/peaks-doctor` 等老入口照常可用,**0 breaking**。
|
|
39
|
+
|
|
36
40
|
**每一步都有强退出条件**:审计不通过就停,QA 没过就停,任何一道门失败 = 全流程停。你不必追着它催、也不必替它补,它跑完一道再交你拍板,拍完再走下一道。
|
|
37
41
|
|
|
38
42
|
#### 门禁不是装饰 —— 5439 tests passed · 19 skipped · 0 failed
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks adapter *` — vendor adapter registry CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* Slice S2-a of RD-2 (2026-07-08 session). Two subcommands:
|
|
5
|
+
*
|
|
6
|
+
* list — list registered user adapters (persisted at
|
|
7
|
+
* `.peaks/runtime/adapters.json`).
|
|
8
|
+
* register — register a new adapter. Vendor verb strings live in
|
|
9
|
+
* the binary the user picks, NOT in peaks-loop source.
|
|
10
|
+
*
|
|
11
|
+
* This file coexists with the older `adapter-commands.ts` (slice
|
|
12
|
+
* 2026-07-04-cli-15a) which exposes `peaks skill adapter <verb>` —
|
|
13
|
+
* a different surface for skill materialization (resolveScratchDir /
|
|
14
|
+
* materialize / publish). The two surfaces do NOT collide because
|
|
15
|
+
* one is `peaks skill adapter` and the other is `peaks adapter`.
|
|
16
|
+
*/
|
|
17
|
+
import type { Command } from 'commander';
|
|
18
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
19
|
+
export interface AdapterListOptions {
|
|
20
|
+
json?: boolean;
|
|
21
|
+
project?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface AdapterRegisterOptions {
|
|
24
|
+
json?: boolean;
|
|
25
|
+
id?: string;
|
|
26
|
+
/** Display name override (defaults to the id). */
|
|
27
|
+
name?: string;
|
|
28
|
+
binary?: string;
|
|
29
|
+
/** Repeated --arg values. */
|
|
30
|
+
arg?: string[];
|
|
31
|
+
force?: boolean;
|
|
32
|
+
project?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function registerAdapterS2ACommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
3
|
+
import { fail, ok, getErrorMessage } from '../../shared/result.js';
|
|
4
|
+
import { AdapterRegistry } from '../../services/adapter/adapter-registry.js';
|
|
5
|
+
import { registryFileFor } from './runtime-commands.js';
|
|
6
|
+
function resolveProjectRoot(explicit) {
|
|
7
|
+
if (explicit !== undefined && explicit.length > 0)
|
|
8
|
+
return explicit;
|
|
9
|
+
return process.cwd();
|
|
10
|
+
}
|
|
11
|
+
export function registerAdapterS2ACommands(program, io) {
|
|
12
|
+
const adapter = program
|
|
13
|
+
.command('adapter')
|
|
14
|
+
.description('Vendor adapter registry (user-registered, persisted to .peaks/runtime/adapters.json)');
|
|
15
|
+
// -----------------------------------------------------------------
|
|
16
|
+
// 1. peaks adapter list [--json] [--project <root>]
|
|
17
|
+
// -----------------------------------------------------------------
|
|
18
|
+
addJsonOption(adapter
|
|
19
|
+
.command('list')
|
|
20
|
+
.description('List user-registered vendor adapters')
|
|
21
|
+
.option('--project <path>', 'project root (defaults to cwd)')).action((options) => {
|
|
22
|
+
try {
|
|
23
|
+
const projectRoot = resolveProjectRoot(options.project);
|
|
24
|
+
const file = registryFileFor(projectRoot);
|
|
25
|
+
const reg = new AdapterRegistry();
|
|
26
|
+
if (existsSync(file))
|
|
27
|
+
reg.load(file);
|
|
28
|
+
const records = reg.list();
|
|
29
|
+
printResult(io, ok('adapter.list', { projectRoot, file, records, count: records.length }, [], [
|
|
30
|
+
records.length === 0
|
|
31
|
+
? 'No adapters registered yet. Run `peaks adapter register --id <vendor> --binary <cmd>` to add one.'
|
|
32
|
+
: `Run \`peaks runtime compact --via <id>\` to invoke a registered adapter.`
|
|
33
|
+
]), options.json);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
printResult(io, fail('adapter.list', 'ADAPTER_LIST_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// -----------------------------------------------------------------
|
|
41
|
+
// 2. peaks adapter register --id <id> --binary <cmd> [--arg <a>]... [--force]
|
|
42
|
+
// -----------------------------------------------------------------
|
|
43
|
+
addJsonOption(adapter
|
|
44
|
+
.command('register')
|
|
45
|
+
.description('Register a new vendor adapter; persists to .peaks/runtime/adapters.json')
|
|
46
|
+
.requiredOption('--id <id>', 'vendor adapter id (lowercase, e.g. my-cli)')
|
|
47
|
+
.option('--name <name>', 'human-readable display name (defaults to <id>)')
|
|
48
|
+
.requiredOption('--binary <cmd>', 'binary name to invoke for compact (e.g. my-cli)')
|
|
49
|
+
.option('--arg <value>', 'extra arg appended before --force (repeatable)', (value, previous = []) => [...previous, value])
|
|
50
|
+
.option('--force', 'overwrite an existing adapter with the same id')
|
|
51
|
+
.option('--project <path>', 'project root (defaults to cwd)')).action((options) => {
|
|
52
|
+
try {
|
|
53
|
+
const projectRoot = resolveProjectRoot(options.project);
|
|
54
|
+
const file = registryFileFor(projectRoot);
|
|
55
|
+
const reg = new AdapterRegistry();
|
|
56
|
+
if (existsSync(file))
|
|
57
|
+
reg.load(file);
|
|
58
|
+
const id = options.id ?? '';
|
|
59
|
+
const record = options.arg !== undefined
|
|
60
|
+
? { id, displayName: options.name ?? id, binary: options.binary ?? '', args: options.arg }
|
|
61
|
+
: { id, displayName: options.name ?? id, binary: options.binary ?? '' };
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
result = reg.register(record, { force: options.force === true });
|
|
65
|
+
}
|
|
66
|
+
catch (validationError) {
|
|
67
|
+
printResult(io, fail('adapter.register', 'INVALID_ADAPTER_RECORD', getErrorMessage(validationError), { id }, [
|
|
68
|
+
'Use --id with /^[a-z0-9][a-z0-9._-]*$/, --binary as a binary name (no path separators).'
|
|
69
|
+
]), options.json);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
reg.persist(file);
|
|
74
|
+
const warnings = [];
|
|
75
|
+
if (!result.created && options.force !== true) {
|
|
76
|
+
warnings.push(`adapter "${id}" already registered; pass --force to overwrite`);
|
|
77
|
+
}
|
|
78
|
+
printResult(io, ok('adapter.register', {
|
|
79
|
+
projectRoot,
|
|
80
|
+
file,
|
|
81
|
+
adapter: result.record,
|
|
82
|
+
created: result.created
|
|
83
|
+
}, warnings, [
|
|
84
|
+
`Run \`peaks runtime compact --via ${id}\` to invoke it.`
|
|
85
|
+
]), options.json);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
printResult(io, fail('adapter.register', 'ADAPTER_REGISTER_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -9,6 +9,15 @@ import { generateProjectContext } from '../../../services/memory/project-context
|
|
|
9
9
|
import { getSessionId, setSessionMeta } from '../../../services/session/session-manager.js';
|
|
10
10
|
import { fail, ok } from '../../../shared/result.js';
|
|
11
11
|
import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
|
|
12
|
+
// Slice S0 (4.0.0-beta.5 peaks-solo dispatcher release):
|
|
13
|
+
// `peaks skill search` is the CLI primitive that feeds the
|
|
14
|
+
// peaks-solo dispatcher (S1). Adding a single import + register call
|
|
15
|
+
// here keeps the change surgical and leaves all existing skill
|
|
16
|
+
// subcommands (list / doctor / sync / runbook / presence / heartbeat)
|
|
17
|
+
// untouched. See
|
|
18
|
+
// docs/superpowers/specs/2026-07-08-peaks-solo-dispatcher-design.md §3.2
|
|
19
|
+
// and the S0 plan under docs/superpowers/plans/.
|
|
20
|
+
import { registerSkillSearchCommand } from '../skill-search-commands.js';
|
|
12
21
|
export function registerSkillCommand(program, io) {
|
|
13
22
|
const skill = program.command('skill').description('Manage Peaks skills');
|
|
14
23
|
addJsonOption(skill.command('list').description('List skills derived from skills/*/SKILL.md')).action(async (options) => {
|
|
@@ -18,6 +27,15 @@ export function registerSkillCommand(program, io) {
|
|
|
18
27
|
}
|
|
19
28
|
else {
|
|
20
29
|
const sorted = [...skills].sort((a, b) => {
|
|
30
|
+
// Slice S0 (4.0.0-beta.5): peaks-solo is the dispatcher (front
|
|
31
|
+
// door) — list it FIRST so users discover the dispatcher before
|
|
32
|
+
// any specific leaf. Followed by peaks-sop (current default
|
|
33
|
+
// runbook showcase) and peaks-code (canonical code-domain
|
|
34
|
+
// orchestrator); everything else falls back to alphabetical.
|
|
35
|
+
if (a.name === 'peaks-solo')
|
|
36
|
+
return -1;
|
|
37
|
+
if (b.name === 'peaks-solo')
|
|
38
|
+
return 1;
|
|
21
39
|
if (a.name === 'peaks-sop')
|
|
22
40
|
return -1;
|
|
23
41
|
if (b.name === 'peaks-sop')
|
|
@@ -272,4 +290,8 @@ export function registerSkillCommand(program, io) {
|
|
|
272
290
|
const result = detectPresenceMarker({ project: projectRoot, latestAssistantMessage: message });
|
|
273
291
|
printResult(io, ok('skill.detect-marker-loss', result), options.json);
|
|
274
292
|
});
|
|
293
|
+
// Slice S0 — register `peaks skill search`. Sibling subcommand to
|
|
294
|
+
// list / runbook / presence; preserves the existing surface
|
|
295
|
+
// (HC-10 — 老入口保留).
|
|
296
|
+
registerSkillSearchCommand(program, io);
|
|
275
297
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks polyrepo *` — polyrepo workflow CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* Slice S2-b of RD-2 (2026-07-08 session). Three subcommands:
|
|
5
|
+
*
|
|
6
|
+
* init — scan the parent dir for child git repos + write a
|
|
7
|
+
* polyrepo manifest to `.peaks/polyrepo.json`.
|
|
8
|
+
* status — read the persisted manifest + report children.
|
|
9
|
+
* dispatch — mirror a PRD/RD/QA artifact from the parent into
|
|
10
|
+
* each target child's
|
|
11
|
+
* `.peaks/_runtime/<sid>/<role>/` directory.
|
|
12
|
+
*
|
|
13
|
+
* Per the project's two-axis convention, all reviewable artifacts
|
|
14
|
+
* live under `.peaks/_runtime/<sid>/<role>/` — never as top-level
|
|
15
|
+
* siblings of `.peaks/_runtime/`.
|
|
16
|
+
*/
|
|
17
|
+
import type { Command } from 'commander';
|
|
18
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
19
|
+
export interface PolyrepoInitOptions {
|
|
20
|
+
json?: boolean;
|
|
21
|
+
root?: string;
|
|
22
|
+
children?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PolyrepoStatusOptions {
|
|
25
|
+
json?: boolean;
|
|
26
|
+
root?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PolyrepoDispatchOptions {
|
|
29
|
+
json?: boolean;
|
|
30
|
+
root?: string;
|
|
31
|
+
rid?: string;
|
|
32
|
+
to?: string;
|
|
33
|
+
role?: 'prd' | 'rd' | 'qa';
|
|
34
|
+
artifact?: string;
|
|
35
|
+
sid?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function registerPolyrepoCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
3
|
+
import { fail, ok, getErrorMessage } from '../../shared/result.js';
|
|
4
|
+
import { PolyrepoService } from '../../services/polyrepo/polyrepo-service.js';
|
|
5
|
+
function parseChildren(raw) {
|
|
6
|
+
if (raw === undefined || raw.length === 0)
|
|
7
|
+
return undefined;
|
|
8
|
+
return raw.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
9
|
+
}
|
|
10
|
+
function resolveRoot(explicit) {
|
|
11
|
+
if (explicit !== undefined && explicit.length > 0)
|
|
12
|
+
return explicit;
|
|
13
|
+
return process.cwd();
|
|
14
|
+
}
|
|
15
|
+
export function registerPolyrepoCommands(program, io) {
|
|
16
|
+
const polyrepo = program
|
|
17
|
+
.command('polyrepo')
|
|
18
|
+
.description('Polyrepo workflow (parent dir + child git repos with their own .peaks/ subsets)');
|
|
19
|
+
// -----------------------------------------------------------------
|
|
20
|
+
// 1. peaks polyrepo init [--children <dir1,dir2>] [--root <dir>]
|
|
21
|
+
// -----------------------------------------------------------------
|
|
22
|
+
addJsonOption(polyrepo
|
|
23
|
+
.command('init')
|
|
24
|
+
.description('Scan the parent dir for child git repos and write .peaks/polyrepo.json')
|
|
25
|
+
.option('--children <list>', 'comma-separated child directory names (defaults to auto-discovery)')
|
|
26
|
+
.option('--root <path>', 'parent root (defaults to cwd)')).action((options) => {
|
|
27
|
+
try {
|
|
28
|
+
const root = resolveRoot(options.root);
|
|
29
|
+
const svc = new PolyrepoService();
|
|
30
|
+
const children = parseChildren(options.children);
|
|
31
|
+
const result = children === undefined
|
|
32
|
+
? svc.init({ root })
|
|
33
|
+
: svc.init({ root, children });
|
|
34
|
+
printResult(io, ok('polyrepo.init', {
|
|
35
|
+
root: result.manifest.root,
|
|
36
|
+
detectedAt: result.manifest.detectedAt,
|
|
37
|
+
children: result.manifest.children,
|
|
38
|
+
manifestPath: `${result.manifest.root}/.peaks/polyrepo.json`,
|
|
39
|
+
created: result.created
|
|
40
|
+
}, [], [
|
|
41
|
+
result.manifest.children.length === 0
|
|
42
|
+
? 'No child git repos detected. Pass --children <dir1,dir2> to override.'
|
|
43
|
+
: `Run \`peaks polyrepo dispatch --rid <rid> --to <child-id>\` to mirror an artifact into one or more children.`
|
|
44
|
+
]), options.json);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
printResult(io, fail('polyrepo.init', 'POLYREPO_INIT_FAILED', getErrorMessage(error), {}, ['Verify --root exists and is a directory']), options.json);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
// -----------------------------------------------------------------
|
|
52
|
+
// 2. peaks polyrepo status [--root <dir>]
|
|
53
|
+
// -----------------------------------------------------------------
|
|
54
|
+
addJsonOption(polyrepo
|
|
55
|
+
.command('status')
|
|
56
|
+
.description('Report the persisted polyrepo manifest + children')
|
|
57
|
+
.option('--root <path>', 'parent root (defaults to cwd)')).action((options) => {
|
|
58
|
+
try {
|
|
59
|
+
const root = resolveRoot(options.root);
|
|
60
|
+
const svc = new PolyrepoService();
|
|
61
|
+
const result = svc.status(root);
|
|
62
|
+
if (!result.manifestExists) {
|
|
63
|
+
printResult(io, ok('polyrepo.status', { ...result, initialized: false }, [], [
|
|
64
|
+
'No polyrepo manifest found. Run `peaks polyrepo init [--children <list>]` first.'
|
|
65
|
+
]), options.json);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
printResult(io, ok('polyrepo.status', { ...result, initialized: true }, [], [
|
|
69
|
+
result.children.length === 0
|
|
70
|
+
? 'Manifest exists but has no children. Re-run `peaks polyrepo init --children <list>`.'
|
|
71
|
+
: `Run \`peaks polyrepo dispatch --rid <rid> --to <child-id>\` to mirror an artifact.`
|
|
72
|
+
]), options.json);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
printResult(io, fail('polyrepo.status', 'POLYREPO_STATUS_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
// -----------------------------------------------------------------
|
|
80
|
+
// 3. peaks polyrepo dispatch --rid <rid> [--to <child-id>] --role <prd|rd|qa> --artifact <path>
|
|
81
|
+
// -----------------------------------------------------------------
|
|
82
|
+
addJsonOption(polyrepo
|
|
83
|
+
.command('dispatch')
|
|
84
|
+
.description('Mirror a PRD/RD/QA artifact from the parent into one or more children')
|
|
85
|
+
.requiredOption('--rid <rid>', 'request id (used in the on-disk filename)')
|
|
86
|
+
.option('--to <list>', 'comma-separated child ids (defaults to all children in the manifest)')
|
|
87
|
+
.requiredOption('--role <role>', 'artifact role: prd | rd | qa')
|
|
88
|
+
.requiredOption('--artifact <path>', 'source artifact path (relative to --root or absolute)')
|
|
89
|
+
.option('--sid <sid>', 'session id override (defaults to the canonical binding)')
|
|
90
|
+
.option('--root <path>', 'parent root (defaults to cwd)')).action((options) => {
|
|
91
|
+
try {
|
|
92
|
+
const root = resolveRoot(options.root);
|
|
93
|
+
const sid = options.sid ?? 'default';
|
|
94
|
+
const rid = options.rid ?? '';
|
|
95
|
+
const role = options.role;
|
|
96
|
+
if (role !== 'prd' && role !== 'rd' && role !== 'qa') {
|
|
97
|
+
printResult(io, fail('polyrepo.dispatch', 'INVALID_ROLE', `--role must be prd | rd | qa (got "${role}")`, {}, [
|
|
98
|
+
'Re-run with --role prd (or rd / qa).'
|
|
99
|
+
]), options.json);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const artifact = options.artifact ?? '';
|
|
104
|
+
if (!existsSync(artifact)) {
|
|
105
|
+
printResult(io, fail('polyrepo.dispatch', 'ARTIFACT_NOT_FOUND', `source artifact does not exist: ${artifact}`, {}, [
|
|
106
|
+
'Verify the path or pass an absolute path.'
|
|
107
|
+
]), options.json);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Resolve targets from the manifest first so we can default to
|
|
112
|
+
// "all children" when --to is omitted.
|
|
113
|
+
const svc = new PolyrepoService();
|
|
114
|
+
const status = svc.status(root);
|
|
115
|
+
if (!status.manifestExists) {
|
|
116
|
+
printResult(io, fail('polyrepo.dispatch', 'NO_MANIFEST', `no polyrepo manifest at ${root}/.peaks/polyrepo.json`, {}, [
|
|
117
|
+
'Run `peaks polyrepo init` first.'
|
|
118
|
+
]), options.json);
|
|
119
|
+
process.exitCode = 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let targets;
|
|
123
|
+
if (options.to !== undefined && options.to.length > 0) {
|
|
124
|
+
targets = options.to.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
targets = status.children.map((c) => c.id);
|
|
128
|
+
}
|
|
129
|
+
const result = svc.dispatch(root, {
|
|
130
|
+
sid, rid, targets, role, artifactPath: artifact
|
|
131
|
+
});
|
|
132
|
+
const allOk = result.perChild.every((c) => c.ok);
|
|
133
|
+
printResult(io, ok('polyrepo.dispatch', {
|
|
134
|
+
root,
|
|
135
|
+
sid,
|
|
136
|
+
rid,
|
|
137
|
+
role,
|
|
138
|
+
artifact,
|
|
139
|
+
dispatch: result.dispatch,
|
|
140
|
+
perChild: result.perChild,
|
|
141
|
+
allOk
|
|
142
|
+
}, result.warnings, [
|
|
143
|
+
allOk
|
|
144
|
+
? `Mirrored artifact to ${result.perChild.length} child(ren).`
|
|
145
|
+
: `One or more child mirrors failed; inspect perChild[].ok.`
|
|
146
|
+
]), options.json);
|
|
147
|
+
if (!allOk)
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
printResult(io, fail('polyrepo.dispatch', 'POLYREPO_DISPATCH_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks runtime *` — vendor-runtime detection + compact CLI surface.
|
|
3
|
+
*
|
|
4
|
+
* Slice S2-a of RD-2 (2026-07-08 session). Four subcommands:
|
|
5
|
+
*
|
|
6
|
+
* detect — print which vendor is the active AI runtime
|
|
7
|
+
* (claude-code / codex / copilot / unknown).
|
|
8
|
+
* list — list the built-in vendor adapters registered by
|
|
9
|
+
* RuntimeService (Claude Code / Codex / Copilot).
|
|
10
|
+
* compact — invoke a vendor's compact verb via the adapter. The
|
|
11
|
+
* adapter (NOT this file) chooses the vendor verb;
|
|
12
|
+
* see src/services/runtime/vendors/<vendor>.ts.
|
|
13
|
+
*
|
|
14
|
+
* Vendor verb strings (`claude --compact`, `codex --compact`,
|
|
15
|
+
* `copilot compact`) MUST live ONLY in `src/services/runtime/vendors/`
|
|
16
|
+
* — verified by AC-1: `rg -n "claude --compact|codex --compact|copilot
|
|
17
|
+
* compact" src/services/code/` returns 0 matches.
|
|
18
|
+
*
|
|
19
|
+
* `peaks runtime compact --via <id>` first consults the user
|
|
20
|
+
* adapter registry (`.peaks/runtime/adapters.json`) for the id, then
|
|
21
|
+
* falls back to the built-in adapter list. Missing vendor CLI
|
|
22
|
+
* surfaces as exitCode=127 + warning, NOT as a fatal error — vendor
|
|
23
|
+
* neutrality demands peaks-loop stay alive.
|
|
24
|
+
*/
|
|
25
|
+
import type { Command } from 'commander';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
28
|
+
export interface RuntimeDetectOptions {
|
|
29
|
+
json?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface RuntimeListOptions {
|
|
32
|
+
json?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface RuntimeCompactOptions {
|
|
35
|
+
json?: boolean;
|
|
36
|
+
/** Vendor adapter id (built-in or registered). */
|
|
37
|
+
via?: string;
|
|
38
|
+
force?: boolean;
|
|
39
|
+
/** Override the project root (defaults to .peaks runtime lookup). */
|
|
40
|
+
project?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function registerRuntimeCommands(program: Command, io: ProgramIO): void;
|
|
43
|
+
/** Internal helper exposed for tests + future programmatic callers:
|
|
44
|
+
* resolve the registry file location, ensuring the parent dir
|
|
45
|
+
* exists. Idempotent. */
|
|
46
|
+
export declare function ensureRegistryDir(registryFile: string): void;
|
|
47
|
+
/** Internal helper for tests: locate the registry file under the
|
|
48
|
+
* given project root. Thin wrapper so tests can stub the resolution. */
|
|
49
|
+
export declare function registryFileFor(projectRoot: string): string;
|
|
50
|
+
/** Re-export the path join helper for completeness; not strictly
|
|
51
|
+
* needed by callers but keeps the module self-contained. */
|
|
52
|
+
export { join };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
4
|
+
import { fail, ok } from '../../shared/result.js';
|
|
5
|
+
import { getErrorMessage } from '../../shared/result.js';
|
|
6
|
+
import { detectRuntime } from '../../services/runtime/runtime-detector.js';
|
|
7
|
+
import { RuntimeService } from '../../services/runtime/runtime-service.js';
|
|
8
|
+
import { AdapterRegistry } from '../../services/adapter/adapter-registry.js';
|
|
9
|
+
function resolveProjectRoot(explicit) {
|
|
10
|
+
if (explicit !== undefined && explicit.length > 0)
|
|
11
|
+
return explicit;
|
|
12
|
+
return process.cwd();
|
|
13
|
+
}
|
|
14
|
+
export function registerRuntimeCommands(program, io) {
|
|
15
|
+
const runtime = program
|
|
16
|
+
.command('runtime')
|
|
17
|
+
.description('Vendor runtime detection + compact via adapter (--via <vendor-id>)');
|
|
18
|
+
// -----------------------------------------------------------------
|
|
19
|
+
// 1. peaks runtime detect [--json]
|
|
20
|
+
// -----------------------------------------------------------------
|
|
21
|
+
addJsonOption(runtime
|
|
22
|
+
.command('detect')
|
|
23
|
+
.description('Print the active AI runtime vendor (claude-code / codex / copilot / unknown)')).action((options) => {
|
|
24
|
+
try {
|
|
25
|
+
const result = detectRuntime();
|
|
26
|
+
printResult(io, ok('runtime.detect', result, [], [
|
|
27
|
+
result.vendor === 'unknown'
|
|
28
|
+
? 'No vendor sentinel detected. Use `peaks adapter register --id <vendor> --binary <cmd>` to wire a custom one.'
|
|
29
|
+
: `Run \`peaks runtime compact --via ${result.vendor}\` to invoke its compact verb.`
|
|
30
|
+
]), options.json);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
printResult(io, fail('runtime.detect', 'RUNTIME_DETECT_FAILED', getErrorMessage(error), {}, ['Retry with a clean env']), options.json);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// -----------------------------------------------------------------
|
|
38
|
+
// 2. peaks runtime list [--json]
|
|
39
|
+
// -----------------------------------------------------------------
|
|
40
|
+
addJsonOption(runtime
|
|
41
|
+
.command('list')
|
|
42
|
+
.description('List the built-in vendor adapters registered by RuntimeService')).action((options) => {
|
|
43
|
+
try {
|
|
44
|
+
const svc = new RuntimeService();
|
|
45
|
+
const adapters = svc.listBuiltInAdapters().map((a) => ({ id: a.id, displayName: a.displayName }));
|
|
46
|
+
printResult(io, ok('runtime.list', { builtIn: adapters }, [], [
|
|
47
|
+
`Run \`peaks adapter register --id <vendor> --binary <cmd>\` to add a custom adapter (persists to .peaks/runtime/adapters.json).`
|
|
48
|
+
]), options.json);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
printResult(io, fail('runtime.list', 'RUNTIME_LIST_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// -----------------------------------------------------------------
|
|
56
|
+
// 3. peaks runtime compact [--via <id>] [--force] [--json] [--project <root>]
|
|
57
|
+
// -----------------------------------------------------------------
|
|
58
|
+
addJsonOption(runtime
|
|
59
|
+
.command('compact')
|
|
60
|
+
.description('Compact the active vendor via the chosen adapter (--via <vendor-id>); vendor verbs live ONLY in adapter files')
|
|
61
|
+
.option('--via <id>', 'vendor adapter id (built-in or user-registered)')
|
|
62
|
+
.option('--force', 'ask the vendor to compact unconditionally')
|
|
63
|
+
.option('--project <path>', 'project root (defaults to cwd)')).action(async (options) => {
|
|
64
|
+
try {
|
|
65
|
+
const projectRoot = resolveProjectRoot(options.project);
|
|
66
|
+
const requested = options.via;
|
|
67
|
+
if (requested === undefined || requested.length === 0) {
|
|
68
|
+
// No explicit --via: try to detect first, then fall back.
|
|
69
|
+
const detected = detectRuntime();
|
|
70
|
+
if (detected.vendor === 'unknown') {
|
|
71
|
+
printResult(io, fail('runtime.compact', 'NO_VENDOR_SPECIFIED', 'No --via <id> provided and no vendor detected.', { detected }, [
|
|
72
|
+
'Pass --via <vendor-id>, or run `peaks runtime detect` to see what was detected.'
|
|
73
|
+
]), options.json);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await runCompact(projectRoot, detected.vendor, options.force === true, io, options.json);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await runCompact(projectRoot, requested, options.force === true, io, options.json);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
printResult(io, fail('runtime.compact', 'RUNTIME_COMPACT_FAILED', getErrorMessage(error), {}, []), options.json);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function runCompact(projectRoot, viaId, force, io, asJson) {
|
|
89
|
+
// Layer 1: user-registered adapter (via AdapterRegistry).
|
|
90
|
+
const registryFile = AdapterRegistry.defaultFile(projectRoot);
|
|
91
|
+
const registry = new AdapterRegistry();
|
|
92
|
+
if (existsSync(registryFile)) {
|
|
93
|
+
try {
|
|
94
|
+
registry.load(registryFile);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Treat corrupt registry as empty — vendor-neutrality: a corrupt
|
|
98
|
+
// .peaks/runtime/adapters.json must NOT block peaks runtime.
|
|
99
|
+
// We still try the built-in path below.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const registered = registry.resolve(viaId);
|
|
103
|
+
if (registered !== undefined) {
|
|
104
|
+
const r = await registered.compact({ force });
|
|
105
|
+
const warnings = [];
|
|
106
|
+
if (r.exitCode === 127) {
|
|
107
|
+
warnings.push(`binary for adapter "${viaId}" not found on PATH; compact is a no-op`);
|
|
108
|
+
}
|
|
109
|
+
printResult(io, ok('runtime.compact', { via: viaId, source: 'registry', compact: r }, warnings, [
|
|
110
|
+
r.exitCode === 0
|
|
111
|
+
? 'Compact completed.'
|
|
112
|
+
: `Compact exited with code ${r.exitCode}. Check stderr above.`
|
|
113
|
+
]), asJson);
|
|
114
|
+
if (r.exitCode !== 0)
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Layer 2: built-in adapter (via RuntimeService).
|
|
119
|
+
const svc = new RuntimeService();
|
|
120
|
+
const r = await svc.compactVia(viaId, force);
|
|
121
|
+
const warnings = [];
|
|
122
|
+
if (r.warning !== undefined)
|
|
123
|
+
warnings.push(r.warning);
|
|
124
|
+
if (r.exitCode === 127) {
|
|
125
|
+
warnings.push(`built-in adapter "${viaId}" binary not found on PATH; compact is a no-op`);
|
|
126
|
+
}
|
|
127
|
+
printResult(io, ok('runtime.compact', { via: viaId, source: 'built-in', compact: { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr } }, warnings, [
|
|
128
|
+
r.exitCode === 0
|
|
129
|
+
? 'Compact completed.'
|
|
130
|
+
: `Compact exited with code ${r.exitCode}. Check stderr above.`
|
|
131
|
+
]), asJson);
|
|
132
|
+
if (r.exitCode !== 0)
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
}
|
|
135
|
+
/** Internal helper exposed for tests + future programmatic callers:
|
|
136
|
+
* resolve the registry file location, ensuring the parent dir
|
|
137
|
+
* exists. Idempotent. */
|
|
138
|
+
export function ensureRegistryDir(registryFile) {
|
|
139
|
+
mkdirSync(dirname(registryFile), { recursive: true });
|
|
140
|
+
}
|
|
141
|
+
/** Internal helper for tests: locate the registry file under the
|
|
142
|
+
* given project root. Thin wrapper so tests can stub the resolution. */
|
|
143
|
+
export function registryFileFor(projectRoot) {
|
|
144
|
+
const f = AdapterRegistry.defaultFile(projectRoot);
|
|
145
|
+
ensureRegistryDir(f);
|
|
146
|
+
return f;
|
|
147
|
+
}
|
|
148
|
+
/** Re-export the path join helper for completeness; not strictly
|
|
149
|
+
* needed by callers but keeps the module self-contained. */
|
|
150
|
+
export { join };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks skill search — CLI primitive for S0 of the 4.0.0-beta.5
|
|
3
|
+
* peaks-solo dispatcher release.
|
|
4
|
+
*
|
|
5
|
+
* Spec: docs/superpowers/specs/2026-07-08-peaks-solo-dispatcher-design.md §3.2
|
|
6
|
+
* Plan: docs/superpowers/plans/2026-07-08-peaks-solo-dispatcher/s0-skill-search-cli.md
|
|
7
|
+
*
|
|
8
|
+
* Wired into the existing `peaks skill` group by
|
|
9
|
+
* `src/cli/commands/core/skill-command.ts` (one import + one call).
|
|
10
|
+
* The command ALWAYS emits a JSON array on stdout, even on the
|
|
11
|
+
* no-match case (which exits 0 with `[]` per plan §"Match rules (v1)").
|
|
12
|
+
*
|
|
13
|
+
* Exit codes (locked in plan §"API Contract"):
|
|
14
|
+
* 0 — success (may be empty array)
|
|
15
|
+
* 1 — invalid args (Zod validation failure)
|
|
16
|
+
* 2 — unexpected error (skill pool read fail, etc.)
|
|
17
|
+
*/
|
|
18
|
+
import type { Command } from 'commander';
|
|
19
|
+
import type { ProgramIO } from '../cli-helpers.js';
|
|
20
|
+
/**
|
|
21
|
+
* Validate the raw CLI flags, call the service, and emit a JSON
|
|
22
|
+
* envelope on stdout. Errors flow through stderr + non-zero exit
|
|
23
|
+
* codes per the plan §"API Contract" table.
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerSkillSearchCommand(program: Command, io: ProgramIO): void;
|