peaks-cli 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +49 -11
- package/dist/src/cli/commands/gate-commands.js +28 -19
- package/dist/src/cli/commands/hook-handle.d.ts +17 -0
- package/dist/src/cli/commands/hook-handle.js +111 -0
- package/dist/src/cli/commands/hooks-commands.js +72 -21
- package/dist/src/cli/commands/progress-commands.js +9 -2
- package/dist/src/cli/commands/progress-start-spawn.js +30 -4
- package/dist/src/cli/commands/slice-commands.js +4 -2
- package/dist/src/cli/commands/statusline-commands.js +75 -17
- package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
- package/dist/src/cli/commands/sub-agent-commands.js +488 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
- package/dist/src/cli/commands/workspace-commands.js +70 -14
- package/dist/src/cli/program.js +9 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
- package/dist/src/services/artifacts/artifact-prerequisites.d.ts +12 -0
- package/dist/src/services/artifacts/artifact-prerequisites.js +39 -8
- package/dist/src/services/artifacts/request-artifact-service.js +116 -76
- package/dist/src/services/config/config-types.d.ts +1 -1
- package/dist/src/services/context/artifact-meta.d.ts +72 -0
- package/dist/src/services/context/artifact-meta.js +105 -0
- package/dist/src/services/context/context-guard.d.ts +49 -0
- package/dist/src/services/context/context-guard.js +91 -0
- package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
- package/dist/src/services/context/dispatch-context-guard.js +192 -0
- package/dist/src/services/context/headroom-client.d.ts +34 -0
- package/dist/src/services/context/headroom-client.js +117 -0
- package/dist/src/services/context/shared-channel.d.ts +92 -0
- package/dist/src/services/context/shared-channel.js +285 -0
- package/dist/src/services/context/threshold.d.ts +35 -0
- package/dist/src/services/context/threshold.js +76 -0
- package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
- package/dist/src/services/dispatch/batch-counter.js +85 -0
- package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
- package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
- package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
- package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
- package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
- package/dist/src/services/dispatch/leak-detector.js +72 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
- package/dist/src/services/doctor/doctor-service.d.ts +62 -0
- package/dist/src/services/doctor/doctor-service.js +276 -1
- package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
- package/dist/src/services/ide/hook-protocol.d.ts +44 -0
- package/dist/src/services/ide/hook-protocol.js +71 -0
- package/dist/src/services/ide/hook-translator.d.ts +72 -0
- package/dist/src/services/ide/hook-translator.js +128 -0
- package/dist/src/services/ide/ide-detector.d.ts +10 -0
- package/dist/src/services/ide/ide-detector.js +19 -0
- package/dist/src/services/ide/ide-registry.d.ts +14 -0
- package/dist/src/services/ide/ide-registry.js +45 -0
- package/dist/src/services/ide/ide-types.d.ts +120 -0
- package/dist/src/services/ide/ide-types.js +2 -0
- package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
- package/dist/src/services/ide/shared/atomic-json.js +58 -0
- package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
- package/dist/src/services/ide/shared/safe-path.js +29 -0
- package/dist/src/services/progress/progress-service.d.ts +1 -1
- package/dist/src/services/progress/progress-service.js +18 -14
- package/dist/src/services/security/safe-settings-path.d.ts +12 -0
- package/dist/src/services/security/safe-settings-path.js +104 -0
- package/dist/src/services/session/session-manager.d.ts +22 -1
- package/dist/src/services/session/session-manager.js +137 -28
- package/dist/src/services/signal/cancel-handler.d.ts +14 -0
- package/dist/src/services/signal/cancel-handler.js +76 -0
- package/dist/src/services/skill/resume-detector.d.ts +54 -0
- package/dist/src/services/skill/resume-detector.js +334 -0
- package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
- package/dist/src/services/skill/skill-scheduler.js +53 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
- package/dist/src/services/skills/hooks-settings-service.js +190 -144
- package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
- package/dist/src/services/skills/statusline-settings-service.js +31 -34
- package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
- package/dist/src/services/slice/slice-archive-service.js +111 -0
- package/dist/src/services/slice/slice-check-service.js +20 -1
- package/dist/src/services/slice/slice-check-types.d.ts +9 -0
- package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
- package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
- package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
- package/dist/src/services/solo/status-line-renderer.js +55 -0
- package/dist/src/services/workspace/migrate-service.js +124 -2
- package/dist/src/services/workspace/migrate-types.d.ts +50 -7
- package/dist/src/services/workspace/reconcile-service.d.ts +69 -0
- package/dist/src/services/workspace/reconcile-service.js +267 -48
- package/dist/src/services/workspace/reconcile-types.d.ts +37 -0
- package/dist/src/services/workspace/workspace-service.js +29 -62
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-qa/SKILL.md +58 -1
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +52 -9
- package/skills/peaks-solo/SKILL.md +83 -20
- package/skills/peaks-solo/references/context-governance.md +144 -0
- package/skills/peaks-solo/references/headroom-integration.md +107 -0
- package/skills/peaks-solo/references/runbook.md +3 -3
- package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +19 -0
- package/skills/peaks-ui/SKILL.md +28 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks sub-agent-dispatch-guard` — G9.5 / RL-30 strict hook-only atom.
|
|
3
|
+
*
|
|
4
|
+
* This is the **second-layer** gate (PreToolUse hook) for the G9 forced
|
|
5
|
+
* compression threshold. It re-validates the prompt size against the
|
|
6
|
+
* threshold table in `src/services/context/threshold.ts` and returns
|
|
7
|
+
* `{allow: true/false, reason, suggest}` JSON to the LLM platform.
|
|
8
|
+
*
|
|
9
|
+
* **NO `--force` flag is exposed at this layer** (RL-30 strict). The
|
|
10
|
+
* hook is the strictest layer in the G9 chain. If the CLI is bypassed
|
|
11
|
+
* (e.g. a user manually invokes the dispatch CLI with `--force` to
|
|
12
|
+
* override the 80% threshold), the hook catches it and returns
|
|
13
|
+
* `{allow: false}` regardless.
|
|
14
|
+
*
|
|
15
|
+
* This atom is **hidden from `peaks --help`** per dev-preference
|
|
16
|
+
* "skill-first / CLI-auxiliary" + PB-2 byte-stable. It is registered
|
|
17
|
+
* via the LLM platform's PreToolUse hook chain (e.g. Claude Code's
|
|
18
|
+
* `settings.json` `PreToolUse` array) and is not a user-facing command.
|
|
19
|
+
*
|
|
20
|
+
* The `peaks hooks install` command reads `IdeAdapter.promptSizeAware`
|
|
21
|
+
* to decide whether to register this hook for a given IDE.
|
|
22
|
+
*/
|
|
23
|
+
import { Command } from 'commander';
|
|
24
|
+
import { type ContextGuardDecision } from '../../services/context/context-guard.js';
|
|
25
|
+
export declare const HOOK_GUARD_RESULT_TYPE: "peaks-hook-guard/v1";
|
|
26
|
+
export interface HookGuardResult {
|
|
27
|
+
readonly schema: typeof HOOK_GUARD_RESULT_TYPE;
|
|
28
|
+
readonly allow: boolean;
|
|
29
|
+
readonly code: ContextGuardDecision['code'];
|
|
30
|
+
readonly reason: string;
|
|
31
|
+
readonly suggest: string | null;
|
|
32
|
+
readonly tier: ContextGuardDecision['evaluation']['tier'];
|
|
33
|
+
readonly ratio: number;
|
|
34
|
+
readonly bytesUsed: number;
|
|
35
|
+
readonly capacityBytes: number;
|
|
36
|
+
readonly warnings: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the hook-guard result for a given prompt size. Pure function;
|
|
40
|
+
* no IO. The CLI atom (registered below) calls this and prints JSON.
|
|
41
|
+
*
|
|
42
|
+
* Even if the caller passes `force = true` in the input (it shouldn't —
|
|
43
|
+
* the hook CLI doesn't expose that flag), this function ignores it
|
|
44
|
+
* and treats the prompt as if no override were available. This is the
|
|
45
|
+
* RL-30 strict semantics.
|
|
46
|
+
*/
|
|
47
|
+
export declare function evaluateHookGuard(promptSize: number): HookGuardResult;
|
|
48
|
+
/**
|
|
49
|
+
* Register the `peaks sub-agent-dispatch-guard` command. Intentionally
|
|
50
|
+
* NOT registered in the main `peaks --help` quickstart (dev-preference
|
|
51
|
+
* PB-2 byte-stable). The caller (the `peaks hooks install` flow) calls
|
|
52
|
+
* this directly via the imported function; the CLI registration in
|
|
53
|
+
* `src/cli/index.ts` uses a hidden command (no `description`, no help).
|
|
54
|
+
*/
|
|
55
|
+
export declare function registerSubAgentDispatchGuard(program: Command): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { evaluatePromptSize } from '../../services/context/context-guard.js';
|
|
2
|
+
export const HOOK_GUARD_RESULT_TYPE = 'peaks-hook-guard/v1';
|
|
3
|
+
/**
|
|
4
|
+
* Build the hook-guard result for a given prompt size. Pure function;
|
|
5
|
+
* no IO. The CLI atom (registered below) calls this and prints JSON.
|
|
6
|
+
*
|
|
7
|
+
* Even if the caller passes `force = true` in the input (it shouldn't —
|
|
8
|
+
* the hook CLI doesn't expose that flag), this function ignores it
|
|
9
|
+
* and treats the prompt as if no override were available. This is the
|
|
10
|
+
* RL-30 strict semantics.
|
|
11
|
+
*/
|
|
12
|
+
export function evaluateHookGuard(promptSize) {
|
|
13
|
+
// Intentionally pass `force: false` always. The hook layer is strict.
|
|
14
|
+
const decision = evaluatePromptSize(promptSize, { force: false });
|
|
15
|
+
return {
|
|
16
|
+
schema: HOOK_GUARD_RESULT_TYPE,
|
|
17
|
+
allow: decision.allow,
|
|
18
|
+
code: decision.code,
|
|
19
|
+
reason: decision.allow
|
|
20
|
+
? `prompt size ${promptSize} bytes within threshold (tier=${decision.evaluation.tier})`
|
|
21
|
+
: `prompt size ${promptSize} bytes exceeds threshold (tier=${decision.evaluation.tier}, ratio=${decision.evaluation.ratio.toFixed(3)})`,
|
|
22
|
+
suggest: decision.suggest,
|
|
23
|
+
tier: decision.evaluation.tier,
|
|
24
|
+
ratio: decision.evaluation.ratio,
|
|
25
|
+
bytesUsed: decision.evaluation.bytesUsed,
|
|
26
|
+
capacityBytes: decision.evaluation.capacityBytes,
|
|
27
|
+
warnings: decision.warnings
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register the `peaks sub-agent-dispatch-guard` command. Intentionally
|
|
32
|
+
* NOT registered in the main `peaks --help` quickstart (dev-preference
|
|
33
|
+
* PB-2 byte-stable). The caller (the `peaks hooks install` flow) calls
|
|
34
|
+
* this directly via the imported function; the CLI registration in
|
|
35
|
+
* `src/cli/index.ts` uses a hidden command (no `description`, no help).
|
|
36
|
+
*/
|
|
37
|
+
export function registerSubAgentDispatchGuard(program) {
|
|
38
|
+
program
|
|
39
|
+
.command('sub-agent-dispatch-guard')
|
|
40
|
+
.description('INTERNAL: PreToolUse hook guard (G9.5 / RL-30 strict)')
|
|
41
|
+
.requiredOption('--prompt <text>', 'the prompt to validate (size in bytes is what gets checked)')
|
|
42
|
+
.option('--prompt-length <bytes>', 'DOGFOOD ONLY: synthesize a prompt of this size (overrides --prompt content for size only)')
|
|
43
|
+
.action((options) => {
|
|
44
|
+
let prompt = options.prompt;
|
|
45
|
+
if (typeof options.promptLength === 'string' && options.promptLength.length > 0) {
|
|
46
|
+
const len = Number.parseInt(options.promptLength, 10);
|
|
47
|
+
if (Number.isInteger(len) && len > 0) {
|
|
48
|
+
prompt = 'x'.repeat(len);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const promptSize = Buffer.byteLength(prompt, 'utf8');
|
|
52
|
+
const result = evaluateHookGuard(promptSize);
|
|
53
|
+
// Always exit 0 — the LLM platform reads `allow` from JSON.
|
|
54
|
+
// The decision is encoded in `allow` / `code`, not the exit code.
|
|
55
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -89,7 +89,7 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
89
89
|
const workspace = program.command('workspace').description('Manage the Peaks per-session artifact workspace (.peaks/<session-id>/)');
|
|
90
90
|
addJsonOption(workspace
|
|
91
91
|
.command('init')
|
|
92
|
-
.description('Create the .peaks/<session-id>/ directory
|
|
92
|
+
.description('Create the .peaks/_runtime/<session-id>/ directory with ONLY the session.json metadata file (slice 006: role subdirs prd/ui/rd/qa/sc/txt and the system/ subdir are created lazily by writers, not pre-created at init). When --change-id is given, also creates the .peaks/<change-id>/ dir. Pass --session-id to use a specific id, or omit it to auto-generate one (and adopt an existing binding if present). On the first call for a project, also handles the one-time "install peaks hooks" decision (sticky-marker stored in .peaks/.peaks-init-hooks-decision.json).')
|
|
93
93
|
.requiredOption('--project <path>', 'target project root')
|
|
94
94
|
.option('--session-id <id>', 'optional session id in YYYY-MM-DD-<kebab-slug> format. When omitted, the CLI is the single source of truth: an existing binding is reused, otherwise a fresh id is auto-generated.')
|
|
95
95
|
.option('--allow-session-rebind', 'overwrite an existing session binding when the requested session id differs from the project current one', false)
|
|
@@ -199,14 +199,24 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
199
199
|
});
|
|
200
200
|
addJsonOption(workspace
|
|
201
201
|
.command('reconcile')
|
|
202
|
-
.description('Scan .peaks/2026-MM-DD-session-*/ directories and
|
|
203
|
-
'
|
|
204
|
-
'
|
|
205
|
-
'.peaks/.
|
|
206
|
-
'
|
|
207
|
-
|
|
208
|
-
'
|
|
209
|
-
'
|
|
202
|
+
.description('Scan .peaks/2026-MM-DD-session-*/ directories and consolidate the runtime state. ' +
|
|
203
|
+
'By default (no --apply) the command performs four actions:\n' +
|
|
204
|
+
' 1. Migrates legacy runtime files into .peaks/_runtime/: ' +
|
|
205
|
+
'.peaks/.session.json -> .peaks/_runtime/session.json, ' +
|
|
206
|
+
'.peaks/.active-skill.json -> .peaks/_runtime/active-skill.json, ' +
|
|
207
|
+
'.peaks/sop-state/ -> .peaks/_runtime/sop-state/ ' +
|
|
208
|
+
'(idempotent; no-op if already on the new layout).\n' +
|
|
209
|
+
' 2. Re-points .peaks/_runtime/session.json to the canonical session ' +
|
|
210
|
+
'using a 4-tier heuristic: active-skill binding -> latest session.json mtime -> ' +
|
|
211
|
+
'latest any-file mtime -> dir-name sort.\n' +
|
|
212
|
+
' 3. (slice 006) Syncs the single change/<sid>/ live marker under ' +
|
|
213
|
+
'.peaks/_runtime/change/. The marker is an empty directory; every other ' +
|
|
214
|
+
'entry under change/ is removed. Also cleans up the F3-introduced ' +
|
|
215
|
+
'.peaks/_runtime/<sid>/system/ subdir (no-op if already absent).\n' +
|
|
216
|
+
' 4. REPORTS (but does not delete) session dirs older than --older-than <days> ' +
|
|
217
|
+
`(default ${DEFAULT_RECONCILE_AGE_DAYS}) as deletion candidates; this is the only step that is dry-run by default.\n` +
|
|
218
|
+
'Pass --apply to additionally REMOVE the listed candidate dirs (destructive). ' +
|
|
219
|
+
'Migration (1), repoint (2), and marker sync (3) always run regardless of --apply.')
|
|
210
220
|
.requiredOption('--project <path>', 'target project root')
|
|
211
221
|
.option('--apply', 'actually delete the deletion candidates (destructive); without it, dry-run only', false)
|
|
212
222
|
.option('--older-than <days>', `age threshold in days for deletion candidates (default: ${DEFAULT_RECONCILE_AGE_DAYS})`, (value) => Number.parseFloat(value))).action((options) => {
|
|
@@ -242,6 +252,21 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
242
252
|
if (!apply && result.wouldDelete.length > 0) {
|
|
243
253
|
nextActions.push(`Re-run with --apply to delete ${result.wouldDelete.length} candidate dir(s).`);
|
|
244
254
|
}
|
|
255
|
+
if (result.changeMarker.created !== null) {
|
|
256
|
+
nextActions.push(`Synced change/<${result.changeMarker.created}>/ live marker.`);
|
|
257
|
+
}
|
|
258
|
+
else if (result.canonicalSessionId !== null) {
|
|
259
|
+
nextActions.push(`change/<${result.canonicalSessionId}>/ live marker already in place.`);
|
|
260
|
+
}
|
|
261
|
+
if (result.changeMarker.removed.length > 0) {
|
|
262
|
+
nextActions.push(`Removed ${result.changeMarker.removed.length} stale change/<oldSid>/ marker(s).`);
|
|
263
|
+
}
|
|
264
|
+
if (result.systemCleaned.length > 0) {
|
|
265
|
+
nextActions.push(`Removed ${result.systemCleaned.length} F3 system/ subdir(s).`);
|
|
266
|
+
}
|
|
267
|
+
if (result.subAgentStateMigrated > 0) {
|
|
268
|
+
nextActions.push(`Migrated ${result.subAgentStateMigrated} legacy sub-agent state file(s) into .peaks/_sub_agents/.`);
|
|
269
|
+
}
|
|
245
270
|
printResult(io, ok('workspace.reconcile', result, warnings, nextActions), options.json);
|
|
246
271
|
if (result.errors.length > 0) {
|
|
247
272
|
process.exitCode = 1;
|
|
@@ -262,18 +287,24 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
262
287
|
'response. By default the command is a dry-run: it reports the planned moves + conflicts ' +
|
|
263
288
|
'and the session dirs that WOULD be deleted. Pass --apply to actually `git mv` the files ' +
|
|
264
289
|
'and `rm -rf` the emptied session dirs. Idempotent: re-running on an already-migrated tree ' +
|
|
265
|
-
'is a no-op (all files report conflicts with identical content).'
|
|
290
|
+
'is a no-op (all files report conflicts with identical content).' +
|
|
291
|
+
'\n\nSlice 003 (--to-runtime): moves every top-level `.peaks/<sid>/` to `.peaks/_runtime/<sid>/` ' +
|
|
292
|
+
'for projects still on the pre-runtime-layer layout. Idempotent: re-running on a tree ' +
|
|
293
|
+
'that is already canonical is a no-op. F15 carve-out: top-level `rd/project-scan.md` is ' +
|
|
294
|
+
'never overwritten when the runtime copy already exists with different content.')
|
|
266
295
|
.requiredOption('--project <path>', 'target project root')
|
|
267
|
-
.option('--apply', 'actually `git mv` the files and delete the emptied session dirs (destructive); without it, dry-run only', false)
|
|
296
|
+
.option('--apply', 'actually `git mv` the files and delete the emptied session dirs (destructive); without it, dry-run only', false)
|
|
297
|
+
.option('--to-runtime', 'slice 003: also consolidate every top-level .peaks/<sid>/ dir into .peaks/_runtime/<sid>/. Idempotent; conflicts are logged but never overwrite.', false)).action(async (options) => {
|
|
268
298
|
try {
|
|
269
299
|
const projectRoot = resolveCanonicalProjectRoot(options.project);
|
|
270
300
|
const apply = options.apply === true;
|
|
271
|
-
const
|
|
301
|
+
const toRuntime = options.toRuntime === true;
|
|
302
|
+
const result = await migrateWorkspace({ projectRoot, apply, toRuntime });
|
|
272
303
|
const warnings = [];
|
|
273
|
-
if (result.sessions.length === 0) {
|
|
304
|
+
if (result.sessions.length === 0 && (result.toRuntimePlans?.length ?? 0) === 0) {
|
|
274
305
|
warnings.push('No legacy session directories found under .peaks/. Nothing to migrate.');
|
|
275
306
|
}
|
|
276
|
-
else if (result.wouldMove.length === 0) {
|
|
307
|
+
else if (result.wouldMove.length === 0 && (result.toRuntimePlans?.length ?? 0) === 0) {
|
|
277
308
|
warnings.push('Legacy session dirs found but no reviewable content to migrate (all files were cross-cutting or transient).');
|
|
278
309
|
}
|
|
279
310
|
const nextActions = [];
|
|
@@ -283,6 +314,31 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
283
314
|
if (result.conflicts.length > 0) {
|
|
284
315
|
nextActions.push(`${result.conflicts.length} file(s) already exist at the target path; review before --apply (or re-run after a partial migrate).`);
|
|
285
316
|
}
|
|
317
|
+
if (toRuntime) {
|
|
318
|
+
const plans = result.toRuntimePlans ?? [];
|
|
319
|
+
if (apply) {
|
|
320
|
+
if ((result.toRuntimeMoved?.length ?? 0) > 0) {
|
|
321
|
+
nextActions.push(`Moved ${result.toRuntimeMoved?.length} top-level session dir(s) to .peaks/_runtime/ (slice 003 --to-runtime).`);
|
|
322
|
+
}
|
|
323
|
+
if ((result.toRuntimeConflicts?.length ?? 0) > 0) {
|
|
324
|
+
nextActions.push(`${result.toRuntimeConflicts?.length} --to-runtime conflict(s) — see response. ${plans.filter((p) => p.action === 'f15-conflict-project-scan').length} are F15 carve-outs (deferred to a separate slice).`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
const wouldMoveCount = plans.filter((p) => p.action === 'moved').length;
|
|
329
|
+
const wouldSkipCount = plans.filter((p) => p.action === 'skipped-already-canonical').length;
|
|
330
|
+
if (wouldMoveCount > 0) {
|
|
331
|
+
nextActions.push(`Re-run with --apply to move ${wouldMoveCount} top-level session dir(s) to .peaks/_runtime/; ${wouldSkipCount} already canonical.`);
|
|
332
|
+
}
|
|
333
|
+
else if (wouldSkipCount > 0) {
|
|
334
|
+
nextActions.push(`All ${wouldSkipCount} top-level session dir(s) are already canonical — no moves needed.`);
|
|
335
|
+
}
|
|
336
|
+
const f15Count = plans.filter((p) => p.action === 'f15-conflict-project-scan').length;
|
|
337
|
+
if (f15Count > 0) {
|
|
338
|
+
nextActions.push(`${f15Count} F15 carve-out conflict(s) (rd/project-scan.md differs from runtime copy) — see response.`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
286
342
|
if (apply) {
|
|
287
343
|
if (result.moved.length > 0) {
|
|
288
344
|
nextActions.push(`Migrated ${result.moved.length} file(s) into .peaks/retrospective/.`);
|
package/dist/src/cli/program.js
CHANGED
|
@@ -17,7 +17,10 @@ import { registerScanCommands } from './commands/scan-commands.js';
|
|
|
17
17
|
import { registerShadcnCommands } from './commands/shadcn-commands.js';
|
|
18
18
|
import { registerSliceCommands } from './commands/slice-commands.js';
|
|
19
19
|
import { registerSopCommands } from './commands/sop-commands.js';
|
|
20
|
+
import { registerSubAgentCommands } from './commands/sub-agent-commands.js';
|
|
21
|
+
import { registerSubAgentDispatchGuard } from './commands/sub-agent-dispatch-guard.js';
|
|
20
22
|
import { registerGateCommands } from './commands/gate-commands.js';
|
|
23
|
+
import { registerHookHandleCommand } from './commands/hook-handle.js';
|
|
21
24
|
import { registerHooksCommands } from './commands/hooks-commands.js';
|
|
22
25
|
import { registerStatusLineCommands } from './commands/statusline-commands.js';
|
|
23
26
|
import { registerUnderstandCommands } from './commands/understand-commands.js';
|
|
@@ -92,7 +95,13 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
|
92
95
|
registerShadcnCommands(program, io);
|
|
93
96
|
registerSliceCommands(program, io);
|
|
94
97
|
registerSopCommands(program, io);
|
|
98
|
+
registerSubAgentCommands(program, io);
|
|
99
|
+
// Slice #010 G9.5: register the hook-only internal atom. Hidden from
|
|
100
|
+
// `peaks --help` (no description text); used by `peaks hooks install`
|
|
101
|
+
// to wire the PreToolUse hook chain.
|
|
102
|
+
registerSubAgentDispatchGuard(program);
|
|
95
103
|
registerGateCommands(program, io);
|
|
104
|
+
registerHookHandleCommand(program, io);
|
|
96
105
|
registerHooksCommands(program, io);
|
|
97
106
|
registerStatusLineCommands(program, io);
|
|
98
107
|
registerUnderstandCommands(program, io);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type HookGuardResult } from '../cli/commands/sub-agent-dispatch-guard.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read the prompt size from the LLM platform's hook stdin. Different
|
|
4
|
+
* LLMs send different payload shapes; we accept the most common:
|
|
5
|
+
* - Claude Code: `{"tool_name": "Bash", "tool_input": {"command": "..."}}`
|
|
6
|
+
* - Trae: `{"tool_name": "terminal", "tool_input": {"command": "..."}}`
|
|
7
|
+
*
|
|
8
|
+
* The hook reads the `command` (or `prompt`) field and computes the
|
|
9
|
+
* byte length. If neither field is present, returns 0 (always passes).
|
|
10
|
+
*/
|
|
11
|
+
export declare function readPromptSizeFromHookStdin(stdin: unknown): number;
|
|
12
|
+
/**
|
|
13
|
+
* Execute the hook guard via spawnSync. Returns the parsed result or
|
|
14
|
+
* a fallback (allow: true) on subprocess failure.
|
|
15
|
+
*
|
|
16
|
+
* Prefer the in-process `evaluateHookGuard` (no subprocess) when the
|
|
17
|
+
* hook is called from a TypeScript context. Use `runHookGuardSubprocess`
|
|
18
|
+
* only when the hook needs to be invoked from a non-TypeScript caller
|
|
19
|
+
* (e.g. a shell script that wraps the peaks CLI).
|
|
20
|
+
*/
|
|
21
|
+
export declare function runHookGuardSubprocess(prompt: string): HookGuardResult;
|
|
22
|
+
/**
|
|
23
|
+
* Main entry point for the hook. Reads the LLM platform's stdin,
|
|
24
|
+
* computes the prompt size, and returns the guard result. Used by
|
|
25
|
+
* the LLM platform's hook JSON to decide whether to allow the tool
|
|
26
|
+
* call.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runHookGuard(stdin: unknown): HookGuardResult;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G9.5 PreToolUse hook execution body.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `peaks sub-agent-dispatch-guard` for LLM platform integration.
|
|
5
|
+
* The hook reads the prompt size from the LLM platform's hook stdin
|
|
6
|
+
* (Claude Code / Trae / etc.), invokes the guard CLI, and returns
|
|
7
|
+
* `{allow: true/false, reason, suggest}` JSON to the LLM platform.
|
|
8
|
+
*
|
|
9
|
+
* The hook is registered via `peaks hooks install` in the LLM
|
|
10
|
+
* platform's `settings.json` `PreToolUse` array. Only IDEs with
|
|
11
|
+
* `IdeAdapter.promptSizeAware: true` get the hook installed.
|
|
12
|
+
*
|
|
13
|
+
* The hook layer is the **strictest** layer in the G9 chain (RL-30).
|
|
14
|
+
* The CLI 兜底 layer (`peaks sub-agent dispatch --force`) can override
|
|
15
|
+
* the 80% threshold; the hook layer CANNOT.
|
|
16
|
+
*/
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { evaluateHookGuard } from '../cli/commands/sub-agent-dispatch-guard.js';
|
|
19
|
+
/**
|
|
20
|
+
* Read the prompt size from the LLM platform's hook stdin. Different
|
|
21
|
+
* LLMs send different payload shapes; we accept the most common:
|
|
22
|
+
* - Claude Code: `{"tool_name": "Bash", "tool_input": {"command": "..."}}`
|
|
23
|
+
* - Trae: `{"tool_name": "terminal", "tool_input": {"command": "..."}}`
|
|
24
|
+
*
|
|
25
|
+
* The hook reads the `command` (or `prompt`) field and computes the
|
|
26
|
+
* byte length. If neither field is present, returns 0 (always passes).
|
|
27
|
+
*/
|
|
28
|
+
export function readPromptSizeFromHookStdin(stdin) {
|
|
29
|
+
if (stdin === null || typeof stdin !== 'object') {
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
const obj = stdin;
|
|
33
|
+
const toolInput = obj.tool_input;
|
|
34
|
+
if (toolInput === null || typeof toolInput !== 'object') {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
const ti = toolInput;
|
|
38
|
+
const candidates = ['command', 'prompt', 'text', 'input'];
|
|
39
|
+
for (const key of candidates) {
|
|
40
|
+
const v = ti[key];
|
|
41
|
+
if (typeof v === 'string') {
|
|
42
|
+
return Buffer.byteLength(v, 'utf8');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Execute the hook guard via spawnSync. Returns the parsed result or
|
|
49
|
+
* a fallback (allow: true) on subprocess failure.
|
|
50
|
+
*
|
|
51
|
+
* Prefer the in-process `evaluateHookGuard` (no subprocess) when the
|
|
52
|
+
* hook is called from a TypeScript context. Use `runHookGuardSubprocess`
|
|
53
|
+
* only when the hook needs to be invoked from a non-TypeScript caller
|
|
54
|
+
* (e.g. a shell script that wraps the peaks CLI).
|
|
55
|
+
*/
|
|
56
|
+
export function runHookGuardSubprocess(prompt) {
|
|
57
|
+
const result = spawnSync('node', [
|
|
58
|
+
process.argv[1] ?? 'peaks',
|
|
59
|
+
'sub-agent-dispatch-guard',
|
|
60
|
+
'--prompt', prompt,
|
|
61
|
+
'--json'
|
|
62
|
+
], { encoding: 'utf8' });
|
|
63
|
+
if (result.status !== 0) {
|
|
64
|
+
// Fallback: allow (don't block the dispatch on a guard subprocess failure).
|
|
65
|
+
return {
|
|
66
|
+
schema: 'peaks-hook-guard/v1',
|
|
67
|
+
allow: true,
|
|
68
|
+
code: 'OK',
|
|
69
|
+
reason: `guard subprocess failed (status ${result.status}); falling through`,
|
|
70
|
+
suggest: null,
|
|
71
|
+
tier: 'ok',
|
|
72
|
+
ratio: 0,
|
|
73
|
+
bytesUsed: 0,
|
|
74
|
+
capacityBytes: 0,
|
|
75
|
+
warnings: ['HOOK_GUARD_SUBPROCESS_FAILED']
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(result.stdout);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return {
|
|
83
|
+
schema: 'peaks-hook-guard/v1',
|
|
84
|
+
allow: true,
|
|
85
|
+
code: 'OK',
|
|
86
|
+
reason: 'guard subprocess produced unparseable JSON; falling through',
|
|
87
|
+
suggest: null,
|
|
88
|
+
tier: 'ok',
|
|
89
|
+
ratio: 0,
|
|
90
|
+
bytesUsed: 0,
|
|
91
|
+
capacityBytes: 0,
|
|
92
|
+
warnings: ['HOOK_GUARD_SUBPROCESS_INVALID_JSON']
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Main entry point for the hook. Reads the LLM platform's stdin,
|
|
98
|
+
* computes the prompt size, and returns the guard result. Used by
|
|
99
|
+
* the LLM platform's hook JSON to decide whether to allow the tool
|
|
100
|
+
* call.
|
|
101
|
+
*/
|
|
102
|
+
export function runHookGuard(stdin) {
|
|
103
|
+
const promptSize = readPromptSizeFromHookStdin(stdin);
|
|
104
|
+
return evaluateHookGuard(promptSize);
|
|
105
|
+
}
|
|
@@ -37,6 +37,18 @@ export type CheckPrerequisitesOptions = {
|
|
|
37
37
|
* on-disk path now agree on the same top-level dir.
|
|
38
38
|
*/
|
|
39
39
|
changeId: string;
|
|
40
|
+
/**
|
|
41
|
+
* Session binding (the developer's local session that wrote the
|
|
42
|
+
* request artifact). Read from the file body's `- session:` line.
|
|
43
|
+
* Optional, but when present the gate falls back to
|
|
44
|
+
* `.peaks/_runtime/<sid>/<role>/` and then `.peaks/<sid>/<role>/`
|
|
45
|
+
* for prerequisite artifacts that don't exist at the per-change-id
|
|
46
|
+
* path. This mirrors the F1/F2 back-compat pattern (read new path
|
|
47
|
+
* first, then legacy) and keeps the gate working for users whose
|
|
48
|
+
* QA / tech-doc / initiated artifacts still live under the session
|
|
49
|
+
* dir rather than under the change-id dir.
|
|
50
|
+
*/
|
|
51
|
+
sessionId?: string;
|
|
40
52
|
role: RequestArtifactRole;
|
|
41
53
|
newState: RequestArtifactState;
|
|
42
54
|
requestId: string;
|
|
@@ -162,17 +162,25 @@ export async function checkPrerequisites(options) {
|
|
|
162
162
|
if (requirements.length === 0) {
|
|
163
163
|
return { ok: true, missing: [] };
|
|
164
164
|
}
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
165
|
+
// Slice 006 simplifies the resolution to a 2-tier fallback. The
|
|
166
|
+
// per-change-id scope (`.peaks/<changeId>/<role>/`) is gone — new
|
|
167
|
+
// artifacts go to the session dir directly. The 2 tiers are:
|
|
168
|
+
// 1. `.peaks/_runtime/<sid>/<role>/...` (post-F3 canonical
|
|
169
|
+
// session home; primary).
|
|
170
|
+
// 2. `.peaks/<sid>/<role>/...` (pre-F3 legacy session home;
|
|
171
|
+
// back-compat).
|
|
172
|
+
// The changeId is preserved in the artifact body's frontmatter for
|
|
173
|
+
// human navigation; it is no longer a filesystem path key.
|
|
174
|
+
const canonicalSessionRoot = options.sessionId !== undefined
|
|
175
|
+
? join(options.projectRoot, '.peaks', '_runtime', options.sessionId)
|
|
176
|
+
: null;
|
|
177
|
+
const legacySessionRoot = options.sessionId !== undefined
|
|
178
|
+
? join(options.projectRoot, '.peaks', options.sessionId)
|
|
179
|
+
: null;
|
|
172
180
|
const missing = [];
|
|
173
181
|
for (const prerequisite of requirements) {
|
|
174
182
|
const relative = resolvePrerequisitePath(prerequisite, options.requestId);
|
|
175
|
-
const absolute = await
|
|
183
|
+
const absolute = await resolvePrerequisiteAbsolutePathWithFallback(canonicalSessionRoot, legacySessionRoot, prerequisite, options.requestId);
|
|
176
184
|
if (absolute === null) {
|
|
177
185
|
missing.push({ path: relative, description: prerequisite.description });
|
|
178
186
|
continue;
|
|
@@ -202,3 +210,26 @@ export async function checkPrerequisites(options) {
|
|
|
202
210
|
}
|
|
203
211
|
return { ok: missing.length === 0, missing };
|
|
204
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Resolve a prerequisite to an on-disk path, with a 2-tier fallback
|
|
215
|
+
* (slice 006 — the per-change-id tier was dropped because per-change-id
|
|
216
|
+
* dirs are no longer created):
|
|
217
|
+
* 1. `<canonicalSessionRoot>/<relative>` (post-F3 canonical session
|
|
218
|
+
* home, when `canonicalSessionRoot` is provided).
|
|
219
|
+
* 2. `<legacySessionRoot>/<relative>` (pre-F3 legacy session home,
|
|
220
|
+
* when `legacySessionRoot` is provided).
|
|
221
|
+
* Tolerates the numbered filename prefix that `request init` writes
|
|
222
|
+
* (e.g. `001-<rid>.md`) at every tier. Returns the matched absolute
|
|
223
|
+
* path, or null when nothing matches.
|
|
224
|
+
*/
|
|
225
|
+
async function resolvePrerequisiteAbsolutePathWithFallback(canonicalSessionRoot, legacySessionRoot, prerequisite, requestId) {
|
|
226
|
+
const roots = [canonicalSessionRoot, legacySessionRoot];
|
|
227
|
+
for (const root of roots) {
|
|
228
|
+
if (root === null)
|
|
229
|
+
continue;
|
|
230
|
+
const found = await resolvePrerequisiteAbsolutePath(root, prerequisite, requestId);
|
|
231
|
+
if (found !== null)
|
|
232
|
+
return found;
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
}
|