peaks-cli 1.3.2 → 1.3.4
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/dist/src/cli/commands/core-artifact-commands.js +6 -3
- 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/project-commands.js +8 -4
- 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/workflow-commands.js +2 -1
- package/dist/src/cli/commands/workspace-commands.js +3 -0
- 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/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/dashboard/project-dashboard-service.d.ts +23 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +21 -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/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +80 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +42 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +98 -0
- package/dist/src/services/ide/hook-protocol.d.ts +47 -0
- package/dist/src/services/ide/hook-protocol.js +74 -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 +180 -0
- package/dist/src/services/ide/ide-types.js +2 -0
- package/dist/src/services/ide/resource-profile.d.ts +52 -0
- package/dist/src/services/ide/resource-profile.js +33 -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/memory/project-context-service.js +2 -1
- package/dist/src/services/memory/project-memory-service.js +4 -3
- package/dist/src/services/perf/perf-baseline-service.js +2 -1
- 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/getSessionDir.d.ts +1 -0
- package/dist/src/services/session/getSessionDir.js +27 -0
- package/dist/src/services/session/index.d.ts +1 -0
- package/dist/src/services/session/index.js +1 -0
- 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/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/standards/ide-aware-standards-service.d.ts +94 -0
- package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
- package/dist/src/services/standards/project-standards-service.d.ts +1 -2
- package/dist/src/services/workspace/reconcile-service.d.ts +36 -0
- package/dist/src/services/workspace/reconcile-service.js +107 -6
- package/dist/src/services/workspace/reconcile-types.d.ts +12 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/scripts/install-skills.mjs +112 -2
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-ide/references/audit-log-helper.md +52 -0
- package/skills/peaks-qa/SKILL.md +153 -55
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +134 -62
- package/skills/peaks-solo/SKILL.md +124 -37
- package/skills/peaks-solo/references/browser-workflow.md +22 -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 +261 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +17 -0
- package/skills/peaks-ui/SKILL.md +45 -10
|
@@ -4,6 +4,8 @@ import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
|
4
4
|
import { buildStatusLineModel, parseStatusLineStdin } from '../../services/skills/skill-statusline-service.js';
|
|
5
5
|
import { renderStatusLine } from '../../services/skills/skill-statusline-renderer.js';
|
|
6
6
|
import { applyStatusLineInstall, planStatusLineInstall, removeStatusLineInstall } from '../../services/skills/statusline-settings-service.js';
|
|
7
|
+
import { readHookStatus as readSettingsStatus } from '../../services/skills/hooks-settings-service.js';
|
|
8
|
+
import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
|
|
7
9
|
const STDIN_READ_TIMEOUT_MS = 250;
|
|
8
10
|
/** Read piped stdin if present; resolve quickly with '' when attached to a TTY. */
|
|
9
11
|
function readStdin() {
|
|
@@ -39,72 +41,128 @@ function readStdin() {
|
|
|
39
41
|
function resolveScope(options) {
|
|
40
42
|
return options.global ? 'global' : 'project';
|
|
41
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the IDE the install should target. The CLI user can override with
|
|
46
|
+
* `--ide <id>`. Otherwise we delegate to `detectIdeFromContext` which checks
|
|
47
|
+
* `process.env[adapter.envVar]` → stdin shape → cwd `.trae`/`.claude` →
|
|
48
|
+
* fallback `'claude-code'`. Pass `parsedStdin: null` since `peaks statusline
|
|
49
|
+
* install` is not invoked from inside an IDE hook — there's no stdin payload.
|
|
50
|
+
*/
|
|
51
|
+
function resolveIdeForCommand(options, projectRoot) {
|
|
52
|
+
if (options.ide !== undefined && options.ide.length > 0) {
|
|
53
|
+
return options.ide;
|
|
54
|
+
}
|
|
55
|
+
return detectIdeFromContext({ env: process.env, cwd: projectRoot ?? process.cwd(), parsedStdin: null });
|
|
56
|
+
}
|
|
42
57
|
export function registerStatusLineCommands(program, io) {
|
|
43
|
-
|
|
58
|
+
// Top-level `peaks statusline` — register as a group with subcommands
|
|
59
|
+
// (install | uninstall | status). When the user runs `peaks statusline` with
|
|
60
|
+
// no subcommand, commander falls back to a hidden render subcommand so the
|
|
61
|
+
// status line still renders. This pattern is required by commander 12.x:
|
|
62
|
+
// when a command has both an action AND subcommands, commander's option
|
|
63
|
+
// parser conflates the parent's options with the subcommand's and drops
|
|
64
|
+
// flags. Routing through a subcommand (even one that's hidden) avoids the
|
|
65
|
+
// option-shadowing bug.
|
|
66
|
+
const statusline = addJsonOption(program
|
|
44
67
|
.command('statusline')
|
|
45
|
-
.description('Render the Peaks skill status line for
|
|
46
|
-
|
|
68
|
+
.description('Render the Peaks skill status line for the current session, or manage the adapter-driven statusLine entry. Run with no subcommand to render; with a subcommand (install | uninstall | status) to manage.'));
|
|
69
|
+
// Hidden render subcommand. This is the default behavior when the user types
|
|
70
|
+
// `peaks statusline` with no subcommand. We mark it hidden so `peaks
|
|
71
|
+
// statusline --help` does not show "render" as a subcommand.
|
|
72
|
+
statusline
|
|
73
|
+
.command('render', { hidden: true })
|
|
74
|
+
.description('Render the Peaks skill status line for the current session (reads session JSON on stdin; honors --project for the project label).')
|
|
75
|
+
.option('--project <path>', 'project root path (used to label the status line when stdin is absent)')
|
|
47
76
|
.action(async (options) => {
|
|
48
77
|
const raw = await readStdin();
|
|
49
78
|
const stdin = parseStatusLineStdin(raw);
|
|
50
|
-
// When a project override is passed (or no stdin), seed cwd so detection works.
|
|
51
79
|
const seeded = options.project
|
|
52
80
|
? { ...(stdin ?? {}), workspace: { current_dir: options.project } }
|
|
53
81
|
: stdin;
|
|
54
82
|
const model = buildStatusLineModel(seeded, Date.now());
|
|
55
|
-
|
|
83
|
+
const text = renderStatusLine(model);
|
|
84
|
+
if (options.json === true) {
|
|
85
|
+
io.stdout(JSON.stringify({ ok: true, command: 'statusline.render', data: { text } }, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
io.stdout(text);
|
|
56
89
|
});
|
|
57
90
|
addJsonOption(statusline
|
|
58
91
|
.command('install')
|
|
59
|
-
.description(
|
|
92
|
+
.description("Install the Peaks status line into the adapter's settings.json (project scope by default).")
|
|
60
93
|
.option('--global', 'install into the user-level ~/.claude/settings.json instead of the project')
|
|
61
94
|
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
95
|
+
.option('--ide <id>', 'target adapter id (claude-code | trae); default: auto-detect from env/cwd')
|
|
62
96
|
.option('--force', 'overwrite an existing non-Peaks statusLine entry')
|
|
63
97
|
.option('--dry-run', 'show what would change without writing')).action((options) => {
|
|
64
98
|
const scope = resolveScope(options);
|
|
65
99
|
const projectRoot = scope === 'project'
|
|
66
100
|
? (options.project ?? findProjectRoot(process.cwd()) ?? process.cwd())
|
|
67
101
|
: undefined;
|
|
102
|
+
const ide = resolveIdeForCommand(options, projectRoot);
|
|
68
103
|
try {
|
|
69
104
|
if (options.dryRun) {
|
|
70
|
-
const plan = planStatusLineInstall(scope, projectRoot);
|
|
105
|
+
const plan = planStatusLineInstall(scope, projectRoot, { ide });
|
|
71
106
|
const warnings = plan.conflict
|
|
72
107
|
? [`An existing statusLine command is set: ${plan.conflictCommand}. Rerun with --force to overwrite.`]
|
|
73
108
|
: [];
|
|
74
|
-
printResult(io, ok('statusline.install', { ...plan, applied: false, dryRun: true }, warnings), options.json);
|
|
109
|
+
printResult(io, ok('statusline.install', { ...plan, ide, applied: false, dryRun: true }, warnings), options.json);
|
|
75
110
|
return;
|
|
76
111
|
}
|
|
77
|
-
const result = applyStatusLineInstall(scope, projectRoot, options.force
|
|
112
|
+
const result = applyStatusLineInstall(scope, projectRoot, { force: options.force === true, ide });
|
|
78
113
|
const warnings = result.conflict && !result.applied
|
|
79
114
|
? [`An existing statusLine command is set: ${result.conflictCommand}. Rerun with --force to overwrite.`]
|
|
80
115
|
: [];
|
|
81
116
|
const nextActions = result.applied
|
|
82
|
-
? ['Restart
|
|
117
|
+
? ['Restart the IDE (or reload the workspace) so the status line takes effect']
|
|
83
118
|
: [];
|
|
84
|
-
printResult(io, ok('statusline.install', { ...result, dryRun: false }, warnings, nextActions), options.json);
|
|
119
|
+
printResult(io, ok('statusline.install', { ...result, ide, dryRun: false }, warnings, nextActions), options.json);
|
|
85
120
|
}
|
|
86
121
|
catch (error) {
|
|
87
122
|
const message = getErrorMessage(error);
|
|
88
|
-
printResult(io, fail('statusline.install', 'STATUSLINE_INSTALL_FAILED', message, { scope, applied: false }, [message]), options.json);
|
|
123
|
+
printResult(io, fail('statusline.install', 'STATUSLINE_INSTALL_FAILED', message, { scope, ide, applied: false }, [message]), options.json);
|
|
89
124
|
process.exitCode = 1;
|
|
90
125
|
}
|
|
91
126
|
});
|
|
92
127
|
addJsonOption(statusline
|
|
93
128
|
.command('uninstall')
|
|
94
|
-
.description(
|
|
129
|
+
.description("Remove the Peaks status line from the adapter's settings.json.")
|
|
95
130
|
.option('--global', 'remove from the user-level ~/.claude/settings.json instead of the project')
|
|
96
|
-
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
131
|
+
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
132
|
+
.option('--ide <id>', 'target adapter id (claude-code | trae); default: auto-detect from env/cwd')).action((options) => {
|
|
133
|
+
const scope = resolveScope(options);
|
|
134
|
+
const projectRoot = scope === 'project'
|
|
135
|
+
? (options.project ?? findProjectRoot(process.cwd()) ?? process.cwd())
|
|
136
|
+
: undefined;
|
|
137
|
+
const ide = resolveIdeForCommand(options, projectRoot);
|
|
138
|
+
try {
|
|
139
|
+
const result = removeStatusLineInstall(scope, projectRoot, { ide });
|
|
140
|
+
printResult(io, ok('statusline.uninstall', { ...result, ide }), options.json);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const message = getErrorMessage(error);
|
|
144
|
+
printResult(io, fail('statusline.uninstall', 'STATUSLINE_UNINSTALL_FAILED', message, { scope, ide, removed: false }, [message]), options.json);
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
addJsonOption(statusline
|
|
149
|
+
.command('status')
|
|
150
|
+
.description('Report whether the Peaks status line is installed in the adapter settings.json.')
|
|
151
|
+
.option('--global', 'inspect the user-level ~/.claude/settings.json instead of the project')
|
|
152
|
+
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
|
|
153
|
+
.option('--ide <id>', 'target adapter id (claude-code | trae); default: auto-detect from env/cwd')).action((options) => {
|
|
97
154
|
const scope = resolveScope(options);
|
|
98
155
|
const projectRoot = scope === 'project'
|
|
99
156
|
? (options.project ?? findProjectRoot(process.cwd()) ?? process.cwd())
|
|
100
157
|
: undefined;
|
|
158
|
+
const ide = resolveIdeForCommand(options, projectRoot);
|
|
101
159
|
try {
|
|
102
|
-
const
|
|
103
|
-
printResult(io, ok('statusline.
|
|
160
|
+
const status = readSettingsStatus(scope, projectRoot, { ide });
|
|
161
|
+
printResult(io, ok('statusline.status', { ...status, ide, command: 'peaks statusline' }), options.json);
|
|
104
162
|
}
|
|
105
163
|
catch (error) {
|
|
106
164
|
const message = getErrorMessage(error);
|
|
107
|
-
printResult(io, fail('statusline.
|
|
165
|
+
printResult(io, fail('statusline.status', 'STATUSLINE_STATUS_FAILED', message, { scope, ide }, [message]), options.json);
|
|
108
166
|
process.exitCode = 1;
|
|
109
167
|
}
|
|
110
168
|
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
3
|
+
export declare function registerSubAgentCommands(program: Command, io: ProgramIO): void;
|
|
4
|
+
/** Validate a role string. Returns null if valid, otherwise the rejection reason. */
|
|
5
|
+
export declare function validateRole(role: string): string | null;
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks sub-agent` CLI commands — slice 2026-06-07-sub-agent-context-governance.
|
|
3
|
+
*
|
|
4
|
+
* Five sub-commands live in this file:
|
|
5
|
+
* 1. `dispatch <role>` — G2 + G7 + G7.7 + G9: emit a per-IDE tool-call
|
|
6
|
+
* descriptor. New flags: --write-artifact (G7), --use-headroom
|
|
7
|
+
* (G7.7/G9), --force (G9 CLI 兜底).
|
|
8
|
+
* 2. `heartbeat --record <path> ...` — G6: append a heartbeat.
|
|
9
|
+
* 3. `share --batch ... --key ... --value ...` — G8.4: write a shared
|
|
10
|
+
* channel entry (dispatcher-mediated cross sub-agent signal).
|
|
11
|
+
* 4. `shared-read --batch ...` — G8.4: read sibling shared entries.
|
|
12
|
+
* 5. (reserved) `list / show / gc` — G5.3 RL-10: stub for future
|
|
13
|
+
* slices.
|
|
14
|
+
*
|
|
15
|
+
* Skill-first / CLI-auxiliary red line (PB-4 / AC-19/20):
|
|
16
|
+
* These commands are primitives that the peaks-solo / peaks-rd /
|
|
17
|
+
* peaks-qa SKILL.md compose. Users do NOT invoke them directly. The
|
|
18
|
+
* --help text is explicit about this; the dispatch envelope's
|
|
19
|
+
* `nextActions` reinforces the point.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import { randomUUID } from 'node:crypto';
|
|
23
|
+
import { fail, getErrorMessage, ok } from '../../shared/result.js';
|
|
24
|
+
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
25
|
+
import { detectInstalledIde } from '../../services/ide/ide-detector.js';
|
|
26
|
+
import { getAdapter } from '../../services/ide/ide-registry.js';
|
|
27
|
+
import { SubAgentNotSupportedError } from '../../services/dispatch/sub-agent-dispatcher.js';
|
|
28
|
+
import { noteDispatched, BATCH_LIMIT } from '../../services/dispatch/batch-counter.js';
|
|
29
|
+
import { appendHeartbeat, writeInitialDispatchRecord } from '../../services/dispatch/dispatch-record-writer.js';
|
|
30
|
+
import { assertSafeDispatchRecordPath } from '../../services/security/safe-settings-path.js';
|
|
31
|
+
import { evaluatePromptSize } from '../../services/context/context-guard.js';
|
|
32
|
+
import { buildArtifactMeta, buildContextImpact } from '../../services/context/artifact-meta.js';
|
|
33
|
+
import { assertSafeArtifactPath } from '../../services/context/dispatch-context-guard.js';
|
|
34
|
+
import { compressPrompt } from '../../services/context/headroom-client.js';
|
|
35
|
+
import { readSharedChannel, writeSharedEntry, SHARED_CHANNEL_SOFT_VALUE_WARN } from '../../services/context/shared-channel.js';
|
|
36
|
+
const RECOMMENDED_ROLES = 'rd | qa | ui | txt | qa-business | qa-perf | qa-security | qa-business-<*> | general-purpose';
|
|
37
|
+
const HEARTBEAT_STATUSES = [
|
|
38
|
+
'queued', 'running', 'finalizing', 'done', 'failed', 'stale'
|
|
39
|
+
];
|
|
40
|
+
const PROMPT_LIMIT_BYTES = 256 * 1024;
|
|
41
|
+
const HEADROOM_MODES = ['balanced', 'aggressive', 'conservative'];
|
|
42
|
+
export function registerSubAgentCommands(program, io) {
|
|
43
|
+
const subAgent = program
|
|
44
|
+
.command('sub-agent')
|
|
45
|
+
.description('Sub-agent dispatch primitive (skill-first / CLI-auxiliary). ' +
|
|
46
|
+
'These commands are the primitives that peaks-solo / peaks-rd / ' +
|
|
47
|
+
'peaks-qa SKILL.md compose. Users do not invoke this directly.');
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────
|
|
49
|
+
// peaks sub-agent dispatch <role> --prompt ... --json
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────
|
|
51
|
+
addJsonOption(subAgent
|
|
52
|
+
.command('dispatch')
|
|
53
|
+
.description('Build an IDE-specific tool-call descriptor for a sub-agent dispatch. ' +
|
|
54
|
+
'Dry-run by design; the LLM executes the returned toolCall in its own ' +
|
|
55
|
+
'environment. Flags: --write-artifact (G7), --use-headroom (G7.7), ' +
|
|
56
|
+
'--force (G9 CLI 兜底). ' +
|
|
57
|
+
'See skills/peaks-solo/references/sub-agent-dispatch.md for the ' +
|
|
58
|
+
'orchestrator contract.')
|
|
59
|
+
.argument('<role>', 'sub-agent role (e.g. rd | qa | ui | txt | qa-business | qa-business-api)')
|
|
60
|
+
.requiredOption('--prompt <text>', 'the prompt to send to the sub-agent')
|
|
61
|
+
.option('--prompt-length <bytes>', 'DOGFOOD ONLY: synthesize a prompt of this size (overrides --prompt content for size only; content is "x" repeated)')
|
|
62
|
+
.option('--request-id <rid>', 'the same <rid> used by peaks request init')
|
|
63
|
+
.option('--session-id <sid>', 'override active session id (default: peaks session info --active)')
|
|
64
|
+
.option('--project <path>', 'target project root (defaults to cwd)')
|
|
65
|
+
.option('--batch-id <uuid>', 'batch id for the dispatch (default: auto-generated UUID)')
|
|
66
|
+
.option('--write-artifact <path>', 'G7: register an artifact file at <path>; CLI computes sha256 + size + writes ArtifactMeta to the dispatch record')
|
|
67
|
+
.option('--use-headroom', 'G7.7/G9: compress the prompt via headroom-ai before dispatch (opt-in; falls back to G7 metadata-only if headroom unavailable)')
|
|
68
|
+
.option('--headroom-mode <mode>', `G7.7: headroom mode (${HEADROOM_MODES.join(' | ')}); default balanced`)
|
|
69
|
+
.option('--force', 'G9: override the 80% hard reject threshold at CLI (NOT allowed at hook layer per RL-30 strict)')).action(async (role, options) => {
|
|
70
|
+
const asJson = options.json === true;
|
|
71
|
+
const validation = validateRole(role);
|
|
72
|
+
if (validation !== null) {
|
|
73
|
+
printResult(io, fail('sub-agent.dispatch', 'INVALID_ROLE', validation, { role, toolCall: null, dispatchRecordPath: null }, [
|
|
74
|
+
'Use a non-empty role string with no control characters.',
|
|
75
|
+
`Recommended: ${RECOMMENDED_ROLES}.`
|
|
76
|
+
]), asJson);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (!options.prompt || options.prompt.length === 0) {
|
|
81
|
+
printResult(io, fail('sub-agent.dispatch', 'MISSING_PROMPT', '--prompt is required', { role, toolCall: null, dispatchRecordPath: null }, [
|
|
82
|
+
'Re-run with a non-empty --prompt value.'
|
|
83
|
+
]), asJson);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// DOGFOOD ONLY: --prompt-length overrides the actual prompt content with
|
|
88
|
+
// a synthetic prompt of the given size in bytes. The original --prompt
|
|
89
|
+
// is still required (commander needs it). This avoids ARG_MAX limits
|
|
90
|
+
// on Windows when the dogfood prompt is > 200KB.
|
|
91
|
+
if (typeof options.promptLength === 'string' && options.promptLength.length > 0) {
|
|
92
|
+
const len = Number.parseInt(options.promptLength, 10);
|
|
93
|
+
if (Number.isInteger(len) && len > 0) {
|
|
94
|
+
options.prompt = 'x'.repeat(len);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (options.prompt.length > PROMPT_LIMIT_BYTES) {
|
|
98
|
+
printResult(io, fail('sub-agent.dispatch', 'PROMPT_TOO_LARGE', `prompt exceeds ${PROMPT_LIMIT_BYTES} bytes (got ${options.prompt.length})`, { role, toolCall: null, dispatchRecordPath: null }, [
|
|
99
|
+
'Truncate the prompt or split into multiple dispatches.',
|
|
100
|
+
'Pass --force to override the 80% threshold at CLI (NOT allowed at hook layer).'
|
|
101
|
+
]), asJson);
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// G9 CLI 兜底 — evaluate prompt size against the threshold table.
|
|
106
|
+
const decision = evaluatePromptSize(options.prompt.length, { force: options.force === true });
|
|
107
|
+
if (!decision.allow) {
|
|
108
|
+
printResult(io, fail('sub-agent.dispatch', decision.code, `prompt size ${options.prompt.length} bytes exceeds threshold (tier=${decision.evaluation.tier}, ratio=${decision.evaluation.ratio.toFixed(3)})`, {
|
|
109
|
+
role,
|
|
110
|
+
toolCall: null,
|
|
111
|
+
dispatchRecordPath: null
|
|
112
|
+
}, [
|
|
113
|
+
decision.suggest ?? 'Trim prompt or pass --force to override at CLI.',
|
|
114
|
+
'PreToolUse hook layer will still reject regardless of --force (RL-30 strict).'
|
|
115
|
+
]), asJson);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const projectRoot = options.project ?? process.cwd();
|
|
121
|
+
const sid = options.sessionId ?? 'unknown-sid';
|
|
122
|
+
const rid = options.requestId ?? 'unknown-rid';
|
|
123
|
+
const batchId = options.batchId ?? randomUUID();
|
|
124
|
+
const ide = detectInstalledIde(projectRoot) ?? 'claude-code';
|
|
125
|
+
const adapter = getAdapter(ide);
|
|
126
|
+
if (!adapter.subAgentDispatcher.supportsRole(role)) {
|
|
127
|
+
printResult(io, fail('sub-agent.dispatch', 'IDE_NOT_SUPPORTED', `IDE ${ide} does not support role "${role}"`, { role, toolCall: null, dispatchRecordPath: null }, [
|
|
128
|
+
'Switch to a registered IDE (e.g. claude-code) or pick a role the current IDE supports.'
|
|
129
|
+
]), asJson);
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// G7.7 headroom compress (opt-in). If headroom fails or is unavailable,
|
|
134
|
+
// fall back to the original prompt + emit warning.
|
|
135
|
+
let effectivePrompt = options.prompt;
|
|
136
|
+
let headroomCompressed = false;
|
|
137
|
+
let headroomResult = null;
|
|
138
|
+
const warnings = [...decision.warnings];
|
|
139
|
+
if (options.useHeadroom === true) {
|
|
140
|
+
const mode = isHeadroomMode(options.headroomMode) ? options.headroomMode : 'balanced';
|
|
141
|
+
headroomResult = await compressPrompt(effectivePrompt, mode);
|
|
142
|
+
if (headroomResult.warning !== null) {
|
|
143
|
+
warnings.push(headroomResult.warning);
|
|
144
|
+
}
|
|
145
|
+
if (headroomResult.compressed && headroomResult.compressedPrompt !== null) {
|
|
146
|
+
effectivePrompt = headroomResult.compressedPrompt;
|
|
147
|
+
headroomCompressed = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let toolCall;
|
|
151
|
+
try {
|
|
152
|
+
toolCall = adapter.subAgentDispatcher.buildToolCall({ role, prompt: effectivePrompt, requestId: rid, sessionId: sid });
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error instanceof SubAgentNotSupportedError) {
|
|
156
|
+
printResult(io, fail('sub-agent.dispatch', 'IDE_NOT_SUPPORTED', error.message, { role, toolCall: null, dispatchRecordPath: null }, [
|
|
157
|
+
'Switch IDE or pick a role the current IDE supports.'
|
|
158
|
+
]), asJson);
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
// G7 — optional --write-artifact: build ArtifactMeta, attach to record.
|
|
165
|
+
let artifactMeta = null;
|
|
166
|
+
if (typeof options.writeArtifact === 'string' && options.writeArtifact.length > 0) {
|
|
167
|
+
try {
|
|
168
|
+
assertSafeArtifactPath(options.writeArtifact, projectRoot);
|
|
169
|
+
if (!existsSync(options.writeArtifact)) {
|
|
170
|
+
warnings.push('ARTIFACT_NOT_FOUND');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
artifactMeta = buildArtifactMeta({
|
|
174
|
+
path: options.writeArtifact,
|
|
175
|
+
rid,
|
|
176
|
+
role,
|
|
177
|
+
idx: 1, // single dispatch, idx=1
|
|
178
|
+
summary: null
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
warnings.push(`ARTIFACT_PATH_INVALID: ${getErrorMessage(err)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const { path: dispatchRecordPath } = writeInitialDispatchRecord({
|
|
187
|
+
projectRoot,
|
|
188
|
+
sessionId: sid,
|
|
189
|
+
requestId: rid,
|
|
190
|
+
role,
|
|
191
|
+
prompt: effectivePrompt,
|
|
192
|
+
toolCall,
|
|
193
|
+
batchId
|
|
194
|
+
});
|
|
195
|
+
const counter = noteDispatched(projectRoot, sid, batchId);
|
|
196
|
+
if (counter.warning) {
|
|
197
|
+
warnings.push(counter.warning.message);
|
|
198
|
+
}
|
|
199
|
+
const contextImpact = buildContextImpact({
|
|
200
|
+
promptSize: effectivePrompt.length,
|
|
201
|
+
artifactSizes: artifactMeta ? [artifactMeta.size] : []
|
|
202
|
+
});
|
|
203
|
+
const nextActions = [
|
|
204
|
+
'Tool call is dry-run; LLM must execute the tool to actually dispatch the sub-agent.',
|
|
205
|
+
'After dispatching, the sub-agent should call `peaks sub-agent heartbeat --record ' + dispatchRecordPath + '` periodically.'
|
|
206
|
+
];
|
|
207
|
+
if (counter.warning) {
|
|
208
|
+
nextActions.push(`Batch is over the RL-1 limit (${BATCH_LIMIT}); consider splitting into multiple batches.`);
|
|
209
|
+
}
|
|
210
|
+
if (headroomResult && headroomResult.warning === 'HEADROOM_UNAVAILABLE') {
|
|
211
|
+
nextActions.push('Headroom daemon unavailable; dispatched with G7 metadata-only fallback.');
|
|
212
|
+
}
|
|
213
|
+
printResult(io, ok('sub-agent.dispatch', {
|
|
214
|
+
role,
|
|
215
|
+
ide: adapter.subAgentDispatcher.label,
|
|
216
|
+
prompt: effectivePrompt,
|
|
217
|
+
originalPromptSize: options.prompt.length,
|
|
218
|
+
promptSize: effectivePrompt.length,
|
|
219
|
+
toolCall,
|
|
220
|
+
dispatchRecordPath,
|
|
221
|
+
batchId,
|
|
222
|
+
dispatchedInBatch: counter.count,
|
|
223
|
+
headroomCompressed,
|
|
224
|
+
headroomResult: headroomResult
|
|
225
|
+
? {
|
|
226
|
+
mode: headroomResult.mode,
|
|
227
|
+
compressed: headroomResult.compressed,
|
|
228
|
+
compressionRatio: headroomResult.compressionRatio,
|
|
229
|
+
tokensSaved: headroomResult.tokensSaved,
|
|
230
|
+
warning: headroomResult.warning
|
|
231
|
+
}
|
|
232
|
+
: null,
|
|
233
|
+
forcedAt: decision.forcedAt,
|
|
234
|
+
contextImpact,
|
|
235
|
+
artifactMetas: artifactMeta ? [artifactMeta] : []
|
|
236
|
+
}, warnings, nextActions), asJson);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
printResult(io, fail('sub-agent.dispatch', 'DISPATCH_ERROR', getErrorMessage(error), { role, toolCall: null, dispatchRecordPath: null }, [
|
|
240
|
+
'See error message; if you are dispatching from a SKILL.md, the LLM should retry with a smaller prompt or pick a different role.'
|
|
241
|
+
]), asJson);
|
|
242
|
+
process.exitCode = 1;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────
|
|
246
|
+
// peaks sub-agent heartbeat --record <path> --status <state> --progress <pct> --json
|
|
247
|
+
// ─────────────────────────────────────────────────────────────────
|
|
248
|
+
addJsonOption(subAgent
|
|
249
|
+
.command('heartbeat')
|
|
250
|
+
.description('Append a heartbeat entry to a dispatch record. Fire-and-forget: ' +
|
|
251
|
+
'the parent Dispatcher polls this record during the batch-sync ' +
|
|
252
|
+
'wait and renders a status line. Sub-agents should call this at ' +
|
|
253
|
+
'least every 30s (configurable via SKILL.md heartbeatIntervalSec).')
|
|
254
|
+
.requiredOption('--record <path>', 'absolute path to a dispatch record JSON')
|
|
255
|
+
.requiredOption('--status <state>', 'queued | running | finalizing | done | failed | stale')
|
|
256
|
+
.requiredOption('--progress <pct>', 'integer 0-100')
|
|
257
|
+
.option('--note <text>', 'free-form progress note (≤ 200 chars)')).action((options) => {
|
|
258
|
+
const asJson = options.json === true;
|
|
259
|
+
if (!options.record || !existsSync(options.record)) {
|
|
260
|
+
printResult(io, fail('sub-agent.heartbeat', 'INVALID_RECORD_PATH', `record not found: ${options.record ?? '(empty)'}`, { recordPath: options.record ?? null, truncated: false }, [
|
|
261
|
+
'Pass the absolute path from the `peaks sub-agent dispatch` envelope.'
|
|
262
|
+
]), asJson);
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!HEARTBEAT_STATUSES.includes(options.status)) {
|
|
267
|
+
printResult(io, fail('sub-agent.heartbeat', 'INVALID_STATUS', `--status must be one of ${HEARTBEAT_STATUSES.join(' | ')} (got ${options.status})`, { recordPath: options.record, truncated: false }, [
|
|
268
|
+
'Use one of the documented statuses; poller compares lastBeatAt against now() - 5min to set `stale`.'
|
|
269
|
+
]), asJson);
|
|
270
|
+
process.exitCode = 1;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const progress = Number.parseInt(options.progress ?? 'NaN', 10);
|
|
274
|
+
if (!Number.isInteger(progress) || progress < 0 || progress > 100) {
|
|
275
|
+
printResult(io, fail('sub-agent.heartbeat', 'INVALID_PROGRESS', `--progress must be integer 0-100 (got ${options.progress})`, { recordPath: options.record, truncated: false }, [
|
|
276
|
+
'Use 0..100 inclusive.'
|
|
277
|
+
]), asJson);
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (options.note !== undefined && options.note.length > 200) {
|
|
282
|
+
printResult(io, fail('sub-agent.heartbeat', 'NOTE_TOO_LONG', `--note must be ≤ 200 chars (got ${options.note.length})`, { recordPath: options.record, truncated: false }, [
|
|
283
|
+
'Shorten the note; the record file is not a log file.'
|
|
284
|
+
]), asJson);
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
// R-2 guard: ensure the path lives under `.peaks/_sub_agents/`.
|
|
290
|
+
assertSafeDispatchRecordPath(options.record, deriveProjectRoot(options.record));
|
|
291
|
+
const result = appendHeartbeat({
|
|
292
|
+
recordPath: options.record,
|
|
293
|
+
status: options.status,
|
|
294
|
+
progress,
|
|
295
|
+
...(options.note !== undefined ? { note: options.note } : {})
|
|
296
|
+
});
|
|
297
|
+
printResult(io, ok('sub-agent.heartbeat', {
|
|
298
|
+
recordPath: options.record,
|
|
299
|
+
heartbeatCount: result.record.heartbeats.length,
|
|
300
|
+
lastBeatAt: result.record.lastBeatAt,
|
|
301
|
+
status: result.record.status,
|
|
302
|
+
truncated: result.truncated
|
|
303
|
+
}, [], ['Continue business logic; heartbeat is fire-and-forget.']), asJson);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
const code = error.code ?? 'HEARTBEAT_ERROR';
|
|
307
|
+
printResult(io, fail('sub-agent.heartbeat', code, getErrorMessage(error), { recordPath: options.record ?? null, truncated: false }, [
|
|
308
|
+
'See error message; if the record file is missing or corrupted, the parent Dispatcher will mark the sub-agent as stale after 5 minutes.'
|
|
309
|
+
]), asJson);
|
|
310
|
+
process.exitCode = 1;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
// ─────────────────────────────────────────────────────────────────
|
|
314
|
+
// peaks sub-agent share --batch <batchId> --key <k> --value <json> --json
|
|
315
|
+
// G8.4: cross sub-agent shared channel write.
|
|
316
|
+
// ─────────────────────────────────────────────────────────────────
|
|
317
|
+
addJsonOption(subAgent
|
|
318
|
+
.command('share')
|
|
319
|
+
.description('G8.4: write a shared entry to the cross sub-agent shared channel. ' +
|
|
320
|
+
'Dispatcher-mediated indirect signal: sub-agent A writes, dispatcher ' +
|
|
321
|
+
'stores, sub-agent B (still in flight) reads via `peaks sub-agent ' +
|
|
322
|
+
'shared-read`. Not peer-to-peer; pseudo-swarm property 3 preserved.')
|
|
323
|
+
.requiredOption('--batch <batchId>', 'batchId (from `peaks sub-agent dispatch` envelope)')
|
|
324
|
+
.requiredOption('--key <k>', 'entry key (convention: "<role>.<event>")')
|
|
325
|
+
.requiredOption('--value <json>', 'JSON object value (≤ 1KB soft warn, ≥ 64KB rejected)')
|
|
326
|
+
.option('--from <role>', 'sub-agent role string; defaults to dispatch record role if available')
|
|
327
|
+
.option('--request-id <rid>', 'request id (default: "unknown-rid")')
|
|
328
|
+
.option('--session-id <sid>', 'session id (default: "unknown-sid")')
|
|
329
|
+
.option('--project <path>', 'target project root (defaults to cwd)')).action((options) => {
|
|
330
|
+
const asJson = options.json === true;
|
|
331
|
+
if (!options.batch || !options.key || !options.value) {
|
|
332
|
+
printResult(io, fail('sub-agent.share', 'MISSING_ARG', '--batch, --key, and --value are required', { ok: false }, [
|
|
333
|
+
'Re-run with --batch <batchId> --key <key> --value <jsonObject>.'
|
|
334
|
+
]), asJson);
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
let parsedValue;
|
|
339
|
+
try {
|
|
340
|
+
const parsed = JSON.parse(options.value);
|
|
341
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
342
|
+
throw new Error('value must be a JSON object');
|
|
343
|
+
}
|
|
344
|
+
parsedValue = parsed;
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
printResult(io, fail('sub-agent.share', 'INVALID_VALUE', `value must be a JSON object: ${getErrorMessage(err)}`, { ok: false }, [
|
|
348
|
+
'Pass --value as a JSON object literal, e.g. --value \'{"reason":"x"}\'.'
|
|
349
|
+
]), asJson);
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const projectRoot = options.project ?? process.cwd();
|
|
355
|
+
const sid = options.sessionId ?? 'unknown-sid';
|
|
356
|
+
const rid = options.requestId ?? 'unknown-rid';
|
|
357
|
+
const from = options.from ?? 'unknown-role';
|
|
358
|
+
const result = writeSharedEntry({
|
|
359
|
+
projectRoot,
|
|
360
|
+
sid,
|
|
361
|
+
rid,
|
|
362
|
+
batchId: options.batch,
|
|
363
|
+
key: options.key,
|
|
364
|
+
from,
|
|
365
|
+
value: parsedValue
|
|
366
|
+
});
|
|
367
|
+
if (!result.ok) {
|
|
368
|
+
const code = result.code;
|
|
369
|
+
printResult(io, fail('sub-agent.share', code, result.message, { ok: false, batchId: options.batch }, [
|
|
370
|
+
code === 'VALUE_TOO_LARGE'
|
|
371
|
+
? 'Reduce value size; 1KB is a soft warning, 64KB is a hard reject.'
|
|
372
|
+
: 'See error message; check --batch, --key, --value arguments.'
|
|
373
|
+
]), asJson);
|
|
374
|
+
process.exitCode = 1;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const warnings = [];
|
|
378
|
+
if (result.lastWriteWins) {
|
|
379
|
+
warnings.push('LAST_WRITE_WINS');
|
|
380
|
+
}
|
|
381
|
+
if (result.softWarning) {
|
|
382
|
+
warnings.push(`VALUE_SIZE_SOFT_WARN: ${result.entry.valueSize} > ${SHARED_CHANNEL_SOFT_VALUE_WARN} bytes`);
|
|
383
|
+
}
|
|
384
|
+
printResult(io, ok('sub-agent.share', {
|
|
385
|
+
ok: true,
|
|
386
|
+
batchId: options.batch,
|
|
387
|
+
entryKey: options.key,
|
|
388
|
+
writtenAt: result.entry.at,
|
|
389
|
+
channelSize: result.channelSize,
|
|
390
|
+
lastWriteWins: result.lastWriteWins,
|
|
391
|
+
valueSize: result.entry.valueSize
|
|
392
|
+
}, warnings, [
|
|
393
|
+
'Sub-agents in the same batch can read this entry via `peaks sub-agent shared-read --batch ' + options.batch + '`.'
|
|
394
|
+
]), asJson);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
const code = error.code ?? 'SHARE_ERROR';
|
|
398
|
+
printResult(io, fail('sub-agent.share', code, getErrorMessage(error), { ok: false, batchId: options.batch }, [
|
|
399
|
+
'See error message; check that the path lives under .peaks/_sub_agents/<sid>/shared/.'
|
|
400
|
+
]), asJson);
|
|
401
|
+
process.exitCode = 1;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
// ─────────────────────────────────────────────────────────────────
|
|
405
|
+
// peaks sub-agent shared-read --batch <batchId> --json
|
|
406
|
+
// G8.4: cross sub-agent shared channel read.
|
|
407
|
+
// ─────────────────────────────────────────────────────────────────
|
|
408
|
+
addJsonOption(subAgent
|
|
409
|
+
.command('shared-read')
|
|
410
|
+
.description('G8.4: read entries from the cross sub-agent shared channel. ' +
|
|
411
|
+
'Returns sibling sub-agent status. Supports --since (ISO8601) ' +
|
|
412
|
+
'and --key (glob pattern with * wildcard).')
|
|
413
|
+
.requiredOption('--batch <batchId>', 'batchId (from `peaks sub-agent dispatch` envelope)')
|
|
414
|
+
.option('--since <iso>', 'only return entries written after this ISO8601 timestamp')
|
|
415
|
+
.option('--key <pattern>', 'glob pattern, e.g. "rd.*" or "*.completed"')
|
|
416
|
+
.option('--request-id <rid>', 'request id (default: "unknown-rid")')
|
|
417
|
+
.option('--session-id <sid>', 'session id (default: "unknown-sid")')
|
|
418
|
+
.option('--project <path>', 'target project root (defaults to cwd)')).action((options) => {
|
|
419
|
+
const asJson = options.json === true;
|
|
420
|
+
if (!options.batch) {
|
|
421
|
+
printResult(io, fail('sub-agent.shared-read', 'MISSING_BATCH', '--batch is required', { ok: false }, [
|
|
422
|
+
'Re-run with --batch <batchId>.'
|
|
423
|
+
]), asJson);
|
|
424
|
+
process.exitCode = 1;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const projectRoot = options.project ?? process.cwd();
|
|
429
|
+
const sid = options.sessionId ?? 'unknown-sid';
|
|
430
|
+
const rid = options.requestId ?? 'unknown-rid';
|
|
431
|
+
const channel = readSharedChannel({
|
|
432
|
+
projectRoot,
|
|
433
|
+
sid,
|
|
434
|
+
rid,
|
|
435
|
+
batchId: options.batch,
|
|
436
|
+
...(options.since !== undefined ? { since: options.since } : {}),
|
|
437
|
+
...(options.key !== undefined ? { keyPattern: options.key } : {})
|
|
438
|
+
});
|
|
439
|
+
printResult(io, ok('sub-agent.shared-read', {
|
|
440
|
+
ok: true,
|
|
441
|
+
batchId: options.batch,
|
|
442
|
+
entries: channel.entries,
|
|
443
|
+
totalEntries: Object.keys(channel.entries).length,
|
|
444
|
+
channelSize: JSON.stringify(channel).length,
|
|
445
|
+
updatedAt: channel.updatedAt
|
|
446
|
+
}, [], [
|
|
447
|
+
'Shared channel is dispatcher-mediated; do not attempt to read sibling dispatch records directly.'
|
|
448
|
+
]), asJson);
|
|
449
|
+
}
|
|
450
|
+
catch (error) {
|
|
451
|
+
const code = error.code ?? 'SHARED_READ_ERROR';
|
|
452
|
+
printResult(io, fail('sub-agent.shared-read', code, getErrorMessage(error), { ok: false, batchId: options.batch }, [
|
|
453
|
+
'See error message; check that the batchId matches the dispatch envelope.'
|
|
454
|
+
]), asJson);
|
|
455
|
+
process.exitCode = 1;
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/** Validate a role string. Returns null if valid, otherwise the rejection reason. */
|
|
460
|
+
export function validateRole(role) {
|
|
461
|
+
if (typeof role !== 'string' || role.length === 0) {
|
|
462
|
+
return 'role must be a non-empty string';
|
|
463
|
+
}
|
|
464
|
+
if (role.length > 256) {
|
|
465
|
+
return 'role must be ≤ 256 chars';
|
|
466
|
+
}
|
|
467
|
+
for (let i = 0; i < role.length; i += 1) {
|
|
468
|
+
const code = role.charCodeAt(i);
|
|
469
|
+
if (code <= 0x20 || code === 0x7F) {
|
|
470
|
+
return 'role must not contain whitespace or control characters';
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
function isHeadroomMode(value) {
|
|
476
|
+
if (typeof value !== 'string')
|
|
477
|
+
return false;
|
|
478
|
+
return HEADROOM_MODES.includes(value);
|
|
479
|
+
}
|
|
480
|
+
/** Best-effort project root derivation for the R-2 path guard. */
|
|
481
|
+
function deriveProjectRoot(recordPath) {
|
|
482
|
+
const parts = recordPath.split(/[\\/]/);
|
|
483
|
+
const idx = parts.lastIndexOf('.peaks');
|
|
484
|
+
if (idx <= 0) {
|
|
485
|
+
return process.cwd();
|
|
486
|
+
}
|
|
487
|
+
return parts.slice(0, idx).join('/') || '/';
|
|
488
|
+
}
|