peaks-cli 1.3.3 → 1.3.5
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/dist/src/cli/commands/core-artifact-commands.js +6 -3
- package/dist/src/cli/commands/hook-handle.d.ts +2 -2
- package/dist/src/cli/commands/hook-handle.js +5 -10
- package/dist/src/cli/commands/hooks-commands.js +44 -29
- package/dist/src/cli/commands/project-commands.js +15 -5
- package/dist/src/cli/commands/workflow-commands.js +2 -1
- package/dist/src/cli/commands/workspace-commands.js +1 -2
- package/dist/src/cli/program.js +3 -2
- 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/sub-agent-dispatcher.d.ts +45 -40
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +25 -20
- package/dist/src/services/ide/adapters/claude-code-adapter.js +27 -2
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +19 -11
- package/dist/src/services/ide/adapters/trae-adapter.js +45 -19
- package/dist/src/services/ide/hook-protocol.d.ts +7 -4
- package/dist/src/services/ide/hook-protocol.js +7 -4
- package/dist/src/services/ide/ide-types.d.ts +61 -16
- 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/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 +23 -103
- package/dist/src/services/progress/progress-service.js +24 -137
- package/dist/src/services/scan/file-size-scan.d.ts +4 -0
- package/dist/src/services/scan/file-size-scan.js +32 -3
- 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/skills/hooks-settings-service.d.ts +57 -5
- package/dist/src/services/skills/hooks-settings-service.js +153 -28
- 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/shared/incrementing-number.d.ts +0 -8
- package/dist/src/shared/incrementing-number.js +11 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/scripts/install-skills.mjs +112 -2
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-ide/references/audit-log-helper.md +52 -0
- package/skills/peaks-qa/SKILL.md +104 -62
- package/skills/peaks-qa/references/qa-fanout-contract.md +6 -6
- package/skills/peaks-rd/SKILL.md +88 -73
- package/skills/peaks-solo/SKILL.md +52 -22
- package/skills/peaks-solo/references/browser-workflow.md +22 -20
- package/skills/peaks-solo/references/runbook.md +21 -21
- package/skills/peaks-solo/references/sub-agent-dispatch.md +44 -1
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +9 -9
- package/skills/peaks-ui/SKILL.md +18 -9
- package/dist/src/cli/commands/progress-close-kill.d.ts +0 -51
- package/dist/src/cli/commands/progress-close-kill.js +0 -152
- package/dist/src/cli/commands/progress-commands.d.ts +0 -3
- package/dist/src/cli/commands/progress-commands.js +0 -379
- package/dist/src/cli/commands/progress-start-spawn.d.ts +0 -59
- package/dist/src/cli/commands/progress-start-spawn.js +0 -140
- package/dist/src/cli/commands/progress-watch-render.d.ts +0 -80
- package/dist/src/cli/commands/progress-watch-render.js +0 -308
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Best-effort close of a spawned `peaks progress watch`
|
|
3
|
-
* window. Used by `peaks progress close` (manual escape
|
|
4
|
-
* hatch) and by the watch-side auto-exit when the sub-agent
|
|
5
|
-
* hits a terminal phase.
|
|
6
|
-
*
|
|
7
|
-
* The close is best-effort by design: we never throw from
|
|
8
|
-
* individual signals. One failed close primitive is a UX
|
|
9
|
-
* paper cut, not a correctness bug — the caller still clears
|
|
10
|
-
* the spawn record after this returns.
|
|
11
|
-
*
|
|
12
|
-
* Cross-platform strategy:
|
|
13
|
-
*
|
|
14
|
-
* - macOS: pkill the watch process by command pattern
|
|
15
|
-
* (matches the project path, so we never close the
|
|
16
|
-
* wrong window), then send AppleScript to Terminal.app
|
|
17
|
-
* to close the window by `custom title`. Terminal.app
|
|
18
|
-
* is the dominant macOS terminal, and `custom title` is
|
|
19
|
-
* the only stable identifier we can target from outside
|
|
20
|
-
* the running shell.
|
|
21
|
-
* - Linux: pkill the watch process, then try `wmctrl -c
|
|
22
|
-
* peaks-cli-progress` to close the terminal window by
|
|
23
|
-
* WM class (set in `progress start` for alacritty /
|
|
24
|
-
* kitty; gnome-terminal / konsole / xfce4-terminal
|
|
25
|
-
* close on their own when the child exits). wmctrl is
|
|
26
|
-
* not always installed; we silently no-op on
|
|
27
|
-
* "command not found" (exit 127) and surface other
|
|
28
|
-
* errors as warnings.
|
|
29
|
-
* - Windows: `taskkill /F /FI "WINDOWTITLE eq
|
|
30
|
-
* peaks-cli:*"` to kill the cmd.exe wrapper. We use
|
|
31
|
-
* the title prefix because the exact title includes the
|
|
32
|
-
* `--reason` suffix which we do not know here.
|
|
33
|
-
*
|
|
34
|
-
* The kill is intentionally not a single primitive (e.g.
|
|
35
|
-
* `process.kill(-pid, 'SIGTERM')` on the process group).
|
|
36
|
-
* The launcher's PID is the spawn-time PID (osascript on
|
|
37
|
-
* macOS, gnome-terminal on Linux), not the long-lived
|
|
38
|
-
* watch process — and the long-lived process is the one we
|
|
39
|
-
* actually need to terminate to make the terminal close.
|
|
40
|
-
* Targeting by command pattern (pkill) + window title
|
|
41
|
-
* (AppleScript / wmctrl / taskkill) is more reliable than
|
|
42
|
-
* PID chasing across detached children.
|
|
43
|
-
*/
|
|
44
|
-
import type { ProgressSpawnRecord } from '../../services/progress/progress-service.js';
|
|
45
|
-
export type KillSpawnedTerminalResult = {
|
|
46
|
-
/** Each signal that was successfully sent. */
|
|
47
|
-
signals: string[];
|
|
48
|
-
/** Soft failures (e.g. pkill matched no process, wmctrl missing). */
|
|
49
|
-
warnings: string[];
|
|
50
|
-
};
|
|
51
|
-
export declare function killSpawnedTerminal(record: ProgressSpawnRecord, canonicalProjectRoot: string, currentPlatform: NodeJS.Platform): Promise<KillSpawnedTerminalResult>;
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Best-effort close of a spawned `peaks progress watch`
|
|
3
|
-
* window. Used by `peaks progress close` (manual escape
|
|
4
|
-
* hatch) and by the watch-side auto-exit when the sub-agent
|
|
5
|
-
* hits a terminal phase.
|
|
6
|
-
*
|
|
7
|
-
* The close is best-effort by design: we never throw from
|
|
8
|
-
* individual signals. One failed close primitive is a UX
|
|
9
|
-
* paper cut, not a correctness bug — the caller still clears
|
|
10
|
-
* the spawn record after this returns.
|
|
11
|
-
*
|
|
12
|
-
* Cross-platform strategy:
|
|
13
|
-
*
|
|
14
|
-
* - macOS: pkill the watch process by command pattern
|
|
15
|
-
* (matches the project path, so we never close the
|
|
16
|
-
* wrong window), then send AppleScript to Terminal.app
|
|
17
|
-
* to close the window by `custom title`. Terminal.app
|
|
18
|
-
* is the dominant macOS terminal, and `custom title` is
|
|
19
|
-
* the only stable identifier we can target from outside
|
|
20
|
-
* the running shell.
|
|
21
|
-
* - Linux: pkill the watch process, then try `wmctrl -c
|
|
22
|
-
* peaks-cli-progress` to close the terminal window by
|
|
23
|
-
* WM class (set in `progress start` for alacritty /
|
|
24
|
-
* kitty; gnome-terminal / konsole / xfce4-terminal
|
|
25
|
-
* close on their own when the child exits). wmctrl is
|
|
26
|
-
* not always installed; we silently no-op on
|
|
27
|
-
* "command not found" (exit 127) and surface other
|
|
28
|
-
* errors as warnings.
|
|
29
|
-
* - Windows: `taskkill /F /FI "WINDOWTITLE eq
|
|
30
|
-
* peaks-cli:*"` to kill the cmd.exe wrapper. We use
|
|
31
|
-
* the title prefix because the exact title includes the
|
|
32
|
-
* `--reason` suffix which we do not know here.
|
|
33
|
-
*
|
|
34
|
-
* The kill is intentionally not a single primitive (e.g.
|
|
35
|
-
* `process.kill(-pid, 'SIGTERM')` on the process group).
|
|
36
|
-
* The launcher's PID is the spawn-time PID (osascript on
|
|
37
|
-
* macOS, gnome-terminal on Linux), not the long-lived
|
|
38
|
-
* watch process — and the long-lived process is the one we
|
|
39
|
-
* actually need to terminate to make the terminal close.
|
|
40
|
-
* Targeting by command pattern (pkill) + window title
|
|
41
|
-
* (AppleScript / wmctrl / taskkill) is more reliable than
|
|
42
|
-
* PID chasing across detached children.
|
|
43
|
-
*/
|
|
44
|
-
import { execFile } from 'node:child_process';
|
|
45
|
-
import { promisify } from 'node:util';
|
|
46
|
-
import { getErrorMessage } from '../cli-helpers.js';
|
|
47
|
-
const execFileAsync = promisify(execFile);
|
|
48
|
-
export async function killSpawnedTerminal(record, canonicalProjectRoot, currentPlatform) {
|
|
49
|
-
const signals = [];
|
|
50
|
-
const warnings = [];
|
|
51
|
-
// The watch command we spawned, escaped for use as a pkill
|
|
52
|
-
// pattern. We anchor on `progress watch` (NOT `peaks progress
|
|
53
|
-
// watch`) because the actual cmdline is `.../peaks.js progress
|
|
54
|
-
// watch --project /path` — the literal substring
|
|
55
|
-
// `peaks progress watch` does NOT appear in the cmdline
|
|
56
|
-
// (there is a `.js` between `peaks` and `progress`).
|
|
57
|
-
// Anchoring on the verb + the project path is specific
|
|
58
|
-
// enough to not hit any user-owned `progress watch` process
|
|
59
|
-
// for a different project.
|
|
60
|
-
const watchPattern = `progress watch.*--project ${canonicalProjectRoot.replace(/[\\"\s]/g, '\\$&')}`;
|
|
61
|
-
if (currentPlatform === 'darwin') {
|
|
62
|
-
// pkill exit codes: 0 = matched & signalled, 1 = no processes
|
|
63
|
-
// matched (silent miss), 2 = syntax error (warning), 3 = fatal
|
|
64
|
-
// (warning). macOS pkill writes nothing to stderr on a clean
|
|
65
|
-
// miss, so the exit code is the only signal we have.
|
|
66
|
-
await trySignal('pkill', ['-f', watchPattern], signals, 'pkill-watch', warnings, /no.*process/i, new Set([1]));
|
|
67
|
-
// AppleScript to close the Terminal.app window by
|
|
68
|
-
// custom title. We use `every window whose custom title
|
|
69
|
-
// is` so we only close the right tab. AppleScript returns
|
|
70
|
-
// a non-zero exit when the window is already gone, the
|
|
71
|
-
// app is not running, or the title does not match — all
|
|
72
|
-
// of which are silent misses from the user's perspective
|
|
73
|
-
// (the user-facing outcome is identical to the success
|
|
74
|
-
// case: the window is no longer visible). Treat any
|
|
75
|
-
// non-zero exit as silent.
|
|
76
|
-
try {
|
|
77
|
-
const escapedTitle = record.windowTitle.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
78
|
-
await execFileAsync('osascript', [
|
|
79
|
-
'-e',
|
|
80
|
-
`tell application "Terminal" to close (every window whose custom title is "${escapedTitle}")`
|
|
81
|
-
]);
|
|
82
|
-
signals.push('osascript-close-window');
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
// Silent miss. See comment above.
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
else if (currentPlatform === 'linux') {
|
|
89
|
-
// Same pkill exit code semantics as macOS.
|
|
90
|
-
await trySignal('pkill', ['-f', watchPattern], signals, 'pkill-watch', warnings, /no.*process/i, new Set([1]));
|
|
91
|
-
// wmctrl by WM class (set in `progress start`). Missing
|
|
92
|
-
// wmctrl is silent (exit 127) — most distros ship it but
|
|
93
|
-
// headless / minimal installs do not.
|
|
94
|
-
await trySignal('wmctrl', ['-c', 'peaks-cli-progress'], signals, 'wmctrl-close-class', warnings, /not found|No such file/i, new Set([127]));
|
|
95
|
-
}
|
|
96
|
-
else if (currentPlatform === 'win32') {
|
|
97
|
-
// Title prefix is set in `progress start` to `peaks-cli:`.
|
|
98
|
-
// We match the prefix because the full title includes
|
|
99
|
-
// the `--reason` suffix which we do not know here.
|
|
100
|
-
// taskkill exit codes: 0 = success, 1 = no tasks matched
|
|
101
|
-
// (silent miss — the window is already gone), 128 = error.
|
|
102
|
-
const titlePrefix = 'peaks-cli:';
|
|
103
|
-
await trySignal('taskkill', ['/F', '/FI', `WINDOWTITLE eq ${titlePrefix}*`], signals, 'taskkill-window-title', warnings, /no.*task/i, new Set([1]));
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
warnings.push(`unsupported platform: ${currentPlatform}`);
|
|
107
|
-
}
|
|
108
|
-
return { signals, warnings };
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Run a single close primitive. If it throws AND either
|
|
112
|
-
* (a) the error matches the "expected" stderr pattern
|
|
113
|
-
* (e.g. "no process matched" for pkill, "command not
|
|
114
|
-
* found" for wmctrl) — most platforms print this on
|
|
115
|
-
* stderr; or
|
|
116
|
-
* (b) the exit code is in `silentMissExitCodes` (pkill 1,
|
|
117
|
-
* wmctrl 127, taskkill 1) — the primitive ran, found
|
|
118
|
-
* nothing, and is not telling us via stderr,
|
|
119
|
-
* we silently no-op — that is the success case for the
|
|
120
|
-
* primitive. Other errors are appended to `warnings` for
|
|
121
|
-
* the caller to surface. On a clean resolve, the named
|
|
122
|
-
* signal is appended to `signals`.
|
|
123
|
-
*/
|
|
124
|
-
async function trySignal(command, args, signals, signal, warnings, expectedFailurePattern, silentMissExitCodes) {
|
|
125
|
-
try {
|
|
126
|
-
await execFileAsync(command, args);
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
// execFile's error object exposes `code` as either a
|
|
130
|
-
// numeric exit code (when the process ran) or a string
|
|
131
|
-
// system code like 'ENOENT' (when the binary itself
|
|
132
|
-
// is missing). Only numeric exit codes are candidates
|
|
133
|
-
// for silent-miss.
|
|
134
|
-
const execError = error;
|
|
135
|
-
if (typeof execError.code === 'number' && silentMissExitCodes.has(execError.code)) {
|
|
136
|
-
// Exit code says "ran, but found nothing to act on".
|
|
137
|
-
// The user-facing outcome is identical to the success
|
|
138
|
-
// case, so do not surface a warning.
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const message = getErrorMessage(error);
|
|
142
|
-
if (expectedFailurePattern.test(message)) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
warnings.push(`${command}: ${message}`);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
// Reached only if execFile resolves (exit 0). All three
|
|
149
|
-
// primitives exit non-zero on a miss, so a clean resolve
|
|
150
|
-
// means the signal landed.
|
|
151
|
-
signals.push(signal);
|
|
152
|
-
}
|
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { platform } from 'node:os';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import ora from 'ora';
|
|
5
|
-
import { clearSpawnRecord, isRecentSpawn, phaseAutoClosesSpawn, readSpawnRecord, readSubAgentProgress, resolveProgressProjectRoot, subAgentProgressPath, subAgentSpawnPath, writeSpawnRecord, writeSubAgentProgress } from '../../services/progress/progress-service.js';
|
|
6
|
-
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
7
|
-
import { fail, ok } from '../../shared/result.js';
|
|
8
|
-
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
9
|
-
import { killSpawnedTerminal } from './progress-close-kill.js';
|
|
10
|
-
import { buildStartSpawn } from './progress-start-spawn.js';
|
|
11
|
-
import { WatchRenderer } from './progress-watch-render.js';
|
|
12
|
-
export function registerProgressCommands(program, io) {
|
|
13
|
-
const progress = program.command('progress').description('Sub-agent progress surfacing (LLM-side step writes, user-side watch, auto-spawn new terminal)');
|
|
14
|
-
// ─────────────────────────────────────────────────────────────────
|
|
15
|
-
// peaks progress step
|
|
16
|
-
// LLM-side: called by the LLM on phase transitions. Near-zero
|
|
17
|
-
// token cost — one Bash call per phase change. Writes
|
|
18
|
-
// `.peaks/_sub_agents/<sid>/subagent-progress.json`. No auto-spawn
|
|
19
|
-
// here; the LLM invokes `peaks progress start` separately when
|
|
20
|
-
// the user-visible window needs to open.
|
|
21
|
-
// ─────────────────────────────────────────────────────────────────
|
|
22
|
-
addJsonOption(progress
|
|
23
|
-
.command('step')
|
|
24
|
-
.description('Record a sub-agent step / phase transition. Idempotent on (step, phase); transitions append to history.')
|
|
25
|
-
.requiredOption('--request-id <rid>', 'the same <rid> used by peaks request init')
|
|
26
|
-
.requiredOption('--role <role>', 'rd | qa | ui | sc | prd (the role of the sub-agent calling this)')
|
|
27
|
-
.requiredOption('--step <text>', 'free-form human-readable step label, e.g. "running test/ut"')
|
|
28
|
-
.requiredOption('--phase <phase>', 'starting | running | verifying | completing | finished | failed | idle')
|
|
29
|
-
.option('--verdict <verdict>', 'pass | return-to-rd | blocked (only when phase is finished or failed)')
|
|
30
|
-
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
31
|
-
.option('--reason <text>', 'human-readable reason for the step write, recorded in the response data')).action((options) => {
|
|
32
|
-
try {
|
|
33
|
-
const projectRoot = options.project !== undefined
|
|
34
|
-
? options.project
|
|
35
|
-
: resolveProgressProjectRoot(undefined, process.cwd());
|
|
36
|
-
const canonical = resolveCanonicalProjectRoot(projectRoot);
|
|
37
|
-
const data = writeSubAgentProgress({
|
|
38
|
-
projectRoot: canonical,
|
|
39
|
-
requestId: options.requestId,
|
|
40
|
-
role: options.role,
|
|
41
|
-
step: options.step,
|
|
42
|
-
phase: options.phase,
|
|
43
|
-
...(options.verdict !== undefined ? { verdict: options.verdict } : {}),
|
|
44
|
-
...(options.reason !== undefined ? { outerSessionId: options.reason } : {})
|
|
45
|
-
});
|
|
46
|
-
printResult(io, ok('progress.step', {
|
|
47
|
-
projectRoot: canonical,
|
|
48
|
-
path: subAgentProgressPath(canonical),
|
|
49
|
-
sessionId: data.sessionId,
|
|
50
|
-
requestId: data.requestId,
|
|
51
|
-
role: data.role,
|
|
52
|
-
phase: data.current.phase,
|
|
53
|
-
step: data.current.step,
|
|
54
|
-
startedAt: data.current.startedAt,
|
|
55
|
-
updatedAt: data.current.updatedAt
|
|
56
|
-
}), options.json);
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
printResult(io, fail('progress.step', 'PROGRESS_STEP_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and that peaks workspace init has been run for it']), options.json);
|
|
60
|
-
process.exitCode = 1;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
// ─────────────────────────────────────────────────────────────────
|
|
64
|
-
// peaks progress watch
|
|
65
|
-
// User-side: run in a separate terminal. 1s poll + ASCII
|
|
66
|
-
// spinner + elapsed. --once for a single snapshot.
|
|
67
|
-
// ─────────────────────────────────────────────────────────────────
|
|
68
|
-
addJsonOption(progress
|
|
69
|
-
.command('watch')
|
|
70
|
-
.description('Watch the sub-agent progress file in a loop (1s poll + ASCII spinner). --once for a single snapshot.')
|
|
71
|
-
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
72
|
-
.option('--once', 'print a single snapshot and exit (for use in scripts or statusline hooks)', false)
|
|
73
|
-
.option('--interval-ms <ms>', 'poll interval in milliseconds (default 1000)', (value) => Number.parseInt(value, 10))).action(async (options) => {
|
|
74
|
-
try {
|
|
75
|
-
const projectRoot = options.project !== undefined
|
|
76
|
-
? options.project
|
|
77
|
-
: resolveProgressProjectRoot(undefined, process.cwd());
|
|
78
|
-
const canonical = resolveCanonicalProjectRoot(projectRoot);
|
|
79
|
-
const intervalMs = options.intervalMs !== undefined && Number.isFinite(options.intervalMs) && options.intervalMs > 0
|
|
80
|
-
? options.intervalMs
|
|
81
|
-
: 1000;
|
|
82
|
-
if (options.once === true) {
|
|
83
|
-
const result = readSubAgentProgress({ projectRoot: canonical });
|
|
84
|
-
if (!result.ok) {
|
|
85
|
-
printResult(io, fail('progress.watch', 'NO_PROGRESS_DATA', `No progress file present yet (${result.reason})`, { projectRoot: canonical, path: subAgentProgressPath(canonical) }, ['Run peaks progress step once on the LLM side to bootstrap the file']), options.json);
|
|
86
|
-
process.exitCode = 1;
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
printResult(io, ok('progress.watch.snapshot', {
|
|
90
|
-
projectRoot: canonical,
|
|
91
|
-
path: result.path,
|
|
92
|
-
data: result.data
|
|
93
|
-
}), options.json);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
// Long-running watch loop. The render layer lives in
|
|
97
|
-
// ./progress-watch-render.ts; the watch loop just polls
|
|
98
|
-
// the file and calls renderer.tick(data, n). The renderer
|
|
99
|
-
// owns cursor positioning and in-place overwrite so the
|
|
100
|
-
// output does not grow line by line as it did in the
|
|
101
|
-
// previous console.log-based implementation.
|
|
102
|
-
const renderer = new WatchRenderer({
|
|
103
|
-
projectRoot: canonical,
|
|
104
|
-
progressFilePath: subAgentProgressPath(canonical)
|
|
105
|
-
});
|
|
106
|
-
renderer.start();
|
|
107
|
-
// Hint line is painted once BELOW the dynamic block, so
|
|
108
|
-
// it is not erased on each tick. The user sees the
|
|
109
|
-
// spinner + bar above, the static hint at the bottom.
|
|
110
|
-
io.stdout(chalk.gray(' press Ctrl-C to stop watching\n'));
|
|
111
|
-
let tick = 0;
|
|
112
|
-
// eslint-disable-next-line no-constant-condition
|
|
113
|
-
while (true) {
|
|
114
|
-
const result = readSubAgentProgress({ projectRoot: canonical });
|
|
115
|
-
const data = result.ok ? result.data : null;
|
|
116
|
-
renderer.tick(data, tick);
|
|
117
|
-
tick += 1;
|
|
118
|
-
// Auto-close: when the sub-agent reaches a terminal
|
|
119
|
-
// phase (finished or failed per phaseAutoClosesSpawn),
|
|
120
|
-
// paint the final frame so the user can read the
|
|
121
|
-
// verdict, then exit. Exiting the watch process makes
|
|
122
|
-
// most terminal emulators close the window
|
|
123
|
-
// (Terminal.app / gnome-terminal / konsole all do;
|
|
124
|
-
// alacritty / kitty keep it). We also clear the
|
|
125
|
-
// spawn record so a subsequent `peaks progress close`
|
|
126
|
-
// reports "nothing to close" instead of "closed a
|
|
127
|
-
// ghost record".
|
|
128
|
-
//
|
|
129
|
-
// We deliberately do NOT auto-close on `blocked` —
|
|
130
|
-
// `blocked` means the user needs to read the watch
|
|
131
|
-
// output and decide what to do.
|
|
132
|
-
if (data !== null && phaseAutoClosesSpawn(data.current.phase)) {
|
|
133
|
-
renderer.finalize(data);
|
|
134
|
-
clearSpawnRecord(canonical);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
await new Promise((resolveWait) => setTimeout(resolveWait, intervalMs));
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
printResult(io, fail('progress.watch', 'PROGRESS_WATCH_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and that the progress file is readable']), options.json);
|
|
142
|
-
process.exitCode = 1;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
// ─────────────────────────────────────────────────────────────────
|
|
146
|
-
// peaks progress start
|
|
147
|
-
// The "auto-spawn a new terminal running watch" entry point.
|
|
148
|
-
// Called by the LLM at the first phase transition of a slice,
|
|
149
|
-
// once per session. Cross-platform: macOS uses osascript with
|
|
150
|
-
// Terminal.app; Linux tries gnome-terminal / konsole /
|
|
151
|
-
// xterm in order; Windows uses `start cmd`. The user can
|
|
152
|
-
// close the new terminal at any time; re-invoking is a no-op
|
|
153
|
-
// if a watch is already running in another terminal.
|
|
154
|
-
// ─────────────────────────────────────────────────────────────────
|
|
155
|
-
addJsonOption(progress
|
|
156
|
-
.command('start')
|
|
157
|
-
.description('Auto-spawn a new terminal running `peaks progress watch` for this project. Called by the LLM at the first phase transition; the user can close the new terminal at any time.')
|
|
158
|
-
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
159
|
-
.option('--reason <text>', 'human-readable reason for the auto-spawn, recorded in the response data')
|
|
160
|
-
.option('--quiet', 'suppress human-readable output (the Task-tool PreToolUse hook uses this to keep the LLM context clean)')).action(async (options) => {
|
|
161
|
-
try {
|
|
162
|
-
const projectRoot = options.project !== undefined
|
|
163
|
-
? options.project
|
|
164
|
-
: resolveProgressProjectRoot(undefined, process.cwd());
|
|
165
|
-
const canonical = resolveCanonicalProjectRoot(projectRoot);
|
|
166
|
-
// Idempotency check: when the Task-tool PreToolUse hook fires
|
|
167
|
-
// `peaks progress start` on every Task call, a fresh terminal
|
|
168
|
-
// should NOT be spawned if a watch window was already opened for
|
|
169
|
-
// this session within the last 5 minutes. The user closes the
|
|
170
|
-
// window deliberately; we honor that until the record ages out.
|
|
171
|
-
const recent = isRecentSpawn(canonical);
|
|
172
|
-
if (recent.recent) {
|
|
173
|
-
if (options.quiet !== true) {
|
|
174
|
-
// Non-hook path: keep the human feedback (so an LLM running
|
|
175
|
-
// peaks progress start manually understands the no-op).
|
|
176
|
-
}
|
|
177
|
-
printResult(io, ok('progress.start', {
|
|
178
|
-
projectRoot: canonical,
|
|
179
|
-
spawned: false,
|
|
180
|
-
idempotent: true,
|
|
181
|
-
reason: recent.reason,
|
|
182
|
-
ageMs: recent.ageMs,
|
|
183
|
-
note: 'a recent spawn record exists; the watch window is presumed open. Re-run after 5 min (TTL) or `peaks progress close` to force a fresh spawn.'
|
|
184
|
-
}, [], recent.reason === 'recent-spawn'
|
|
185
|
-
? []
|
|
186
|
-
: ['run `peaks progress close` to clear the stale record and force a fresh spawn']), options.json);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const currentPlatform = platform();
|
|
190
|
-
const peaksBin = process.argv[1] ?? 'peaks';
|
|
191
|
-
const reasonSuffix = options.reason !== undefined ? ` — ${options.reason}` : '';
|
|
192
|
-
// Window / tab title shared across platforms. The user
|
|
193
|
-
// asked for visible "this is peaks-cli" branding so the
|
|
194
|
-
// spawned terminal is identifiable at a glance; the title
|
|
195
|
-
// also makes `peaks progress close` self-documenting.
|
|
196
|
-
//
|
|
197
|
-
// Em-dash (U+2014) instead of a colon. On Windows, cmd /c's
|
|
198
|
-
// script parser interprets a `:` even inside quotes as a
|
|
199
|
-
// drive-letter prefix, so `peaks-cli: sub-agent progress`
|
|
200
|
-
// triggers the "Windows 找不到文件 'sub-agent'" dialog. The
|
|
201
|
-
// em-dash is a no-op for cmd / c, bash, and AppleScript
|
|
202
|
-
// string parsing — the visible branding is preserved.
|
|
203
|
-
const windowTitle = `peaks-cli — sub-agent progress${reasonSuffix}`;
|
|
204
|
-
const watchCommand = `${peaksBin} progress watch --project "${canonical}"`;
|
|
205
|
-
// Build the platform-specific spawn command + args. This
|
|
206
|
-
// is extracted to ./progress-start-spawn.ts so the three
|
|
207
|
-
// platform branches can be unit-tested without spawning
|
|
208
|
-
// a real terminal.
|
|
209
|
-
const spawnSpec = buildStartSpawn({
|
|
210
|
-
peaksBin,
|
|
211
|
-
projectRoot: canonical,
|
|
212
|
-
windowTitle,
|
|
213
|
-
platform: currentPlatform
|
|
214
|
-
});
|
|
215
|
-
if (!spawnSpec.ok) {
|
|
216
|
-
printResult(io, fail('progress.start', 'UNSUPPORTED_PLATFORM', `Cannot auto-spawn a terminal on platform "${currentPlatform}". Run \`peaks progress watch --project "${canonical}"\` in a new terminal yourself.`, { projectRoot: canonical }, ['macOS / Linux / Windows are supported; other platforms need a manual terminal']), options.json);
|
|
217
|
-
process.exitCode = 1;
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
const spawnCommand = spawnSpec.command;
|
|
221
|
-
const spawnArgs = spawnSpec.args;
|
|
222
|
-
// Brief ora feedback while the terminal is launching.
|
|
223
|
-
// Skipped entirely in non-TTY mode (the LLM calls this
|
|
224
|
-
// from a Bash tool, where ora would just hang on
|
|
225
|
-
// animation) and in --json mode (where the structured
|
|
226
|
-
// response is the only signal the caller consumes).
|
|
227
|
-
const showSpinner = process.stdout.isTTY === true && options.json !== true;
|
|
228
|
-
const spinner = showSpinner
|
|
229
|
-
? ora(`auto-spawning ${spawnCommand}…`).start()
|
|
230
|
-
: null;
|
|
231
|
-
try {
|
|
232
|
-
// spawn() with detached:true + unref() is the documented
|
|
233
|
-
// Node.js way to start a long-lived child from a CLI
|
|
234
|
-
// without blocking. We ignore stdio because the spawned
|
|
235
|
-
// terminal owns the child process group from now on; the
|
|
236
|
-
// peaks CLI exits and the terminal keeps running.
|
|
237
|
-
const child = spawn(spawnCommand, spawnArgs, { detached: true, stdio: 'ignore' });
|
|
238
|
-
child.unref();
|
|
239
|
-
// Give the spawn a beat to surface EACCES/ENOENT. We do
|
|
240
|
-
// not await the child (it is intentionally long-lived).
|
|
241
|
-
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
242
|
-
const timer = setTimeout(() => resolveSpawn(), 200);
|
|
243
|
-
child.once('error', (spawnError) => {
|
|
244
|
-
clearTimeout(timer);
|
|
245
|
-
rejectSpawn(spawnError);
|
|
246
|
-
});
|
|
247
|
-
child.once('spawn', () => {
|
|
248
|
-
clearTimeout(timer);
|
|
249
|
-
resolveSpawn();
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
if (spinner !== null) {
|
|
253
|
-
spinner.succeed(`spawned ${spawnCommand} (new window is opening)`);
|
|
254
|
-
}
|
|
255
|
-
// Persist the spawn record so `peaks progress close` (and
|
|
256
|
-
// the watch-side auto-exit) can find and kill the window
|
|
257
|
-
// later. We write the record AFTER the spawn fires so a
|
|
258
|
-
// failed spawn never leaves a stale record behind. The
|
|
259
|
-
// record is per-session: a session rotation invalidates it
|
|
260
|
-
// because the new session gets a fresh record path.
|
|
261
|
-
const spawnRecord = writeSpawnRecord({
|
|
262
|
-
projectRoot: canonical,
|
|
263
|
-
pid: child.pid ?? 0,
|
|
264
|
-
platform: currentPlatform,
|
|
265
|
-
command: spawnCommand,
|
|
266
|
-
args: spawnArgs,
|
|
267
|
-
...(options.reason !== undefined ? { reason: options.reason } : {}),
|
|
268
|
-
windowTitle
|
|
269
|
-
});
|
|
270
|
-
printResult(io, ok('progress.start', {
|
|
271
|
-
projectRoot: canonical,
|
|
272
|
-
platform: currentPlatform,
|
|
273
|
-
spawned: `${spawnCommand} ${spawnArgs.join(' ')}`,
|
|
274
|
-
watchCommand,
|
|
275
|
-
...(options.reason !== undefined ? { reason: options.reason } : {}),
|
|
276
|
-
...(spawnRecord === null
|
|
277
|
-
? {
|
|
278
|
-
spawnRecord: null,
|
|
279
|
-
warning: 'no peaks session binding — `peaks progress close` will not be able to find this window. Close it manually.'
|
|
280
|
-
}
|
|
281
|
-
: {
|
|
282
|
-
spawnRecord: {
|
|
283
|
-
path: subAgentSpawnPath(canonical),
|
|
284
|
-
windowTitle: spawnRecord.windowTitle,
|
|
285
|
-
spawnedAt: spawnRecord.spawnedAt
|
|
286
|
-
}
|
|
287
|
-
}),
|
|
288
|
-
autoClose: 'the watch window will close itself when the sub-agent hits `finished` or `failed`',
|
|
289
|
-
note: 'A new terminal window is opening in the background. It will run `peaks progress watch` and refresh every second. Close the new terminal at any time, or run `peaks progress close` to programmatically close it.'
|
|
290
|
-
}), options.json);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
catch (spawnError) {
|
|
294
|
-
if (spinner !== null) {
|
|
295
|
-
spinner.fail(`auto-spawn failed: ${getErrorMessage(spawnError)}`);
|
|
296
|
-
}
|
|
297
|
-
printResult(io, fail('progress.start', 'TERMINAL_SPAWN_FAILED', `Auto-spawn failed: ${getErrorMessage(spawnError)}. Run \`peaks progress watch --project "${canonical}"\` in a new terminal yourself.`, { projectRoot: canonical, platform: currentPlatform, attempted: `${spawnCommand} ${spawnArgs.join(' ')}` }, ['Verify a terminal emulator is installed (e.g. gnome-terminal / Terminal.app)']), options.json);
|
|
298
|
-
process.exitCode = 1;
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
catch (error) {
|
|
303
|
-
printResult(io, fail('progress.start', 'PROGRESS_START_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and a terminal emulator is available']), options.json);
|
|
304
|
-
process.exitCode = 1;
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
// ─────────────────────────────────────────────────────────────────
|
|
308
|
-
// peaks progress close
|
|
309
|
-
// Manual escape hatch: kill the spawned watch window and
|
|
310
|
-
// clear the spawn record. Idempotent — re-running is a no-op
|
|
311
|
-
// once the record is gone, and the response distinguishes
|
|
312
|
-
// "nothing to close" from "closed it" so callers / hooks can
|
|
313
|
-
// tell the difference. The close is best-effort: if the
|
|
314
|
-
// watch process has already exited but the record is stale,
|
|
315
|
-
// we still clear the record.
|
|
316
|
-
// ─────────────────────────────────────────────────────────────────
|
|
317
|
-
addJsonOption(progress
|
|
318
|
-
.command('close')
|
|
319
|
-
.description('Close the spawned `peaks progress watch` window for this session. Idempotent: re-running when no window is open is a no-op.')
|
|
320
|
-
.option('--project <path>', 'target project root (defaults to git root or cwd)')).action(async (options) => {
|
|
321
|
-
try {
|
|
322
|
-
const projectRoot = options.project !== undefined
|
|
323
|
-
? options.project
|
|
324
|
-
: resolveProgressProjectRoot(undefined, process.cwd());
|
|
325
|
-
const canonical = resolveCanonicalProjectRoot(projectRoot);
|
|
326
|
-
const result = readSpawnRecord(canonical);
|
|
327
|
-
if (!result.ok) {
|
|
328
|
-
// Differentiate the failure modes so callers can decide
|
|
329
|
-
// whether to surface a warning. no-binding means peaks
|
|
330
|
-
// workspace init has not been run; no-spawn-record /
|
|
331
|
-
// invalid-json means there is nothing to close (start
|
|
332
|
-
// has not been called this session, or the window
|
|
333
|
-
// already auto-closed and the record was cleared).
|
|
334
|
-
if (result.reason === 'no-binding') {
|
|
335
|
-
printResult(io, fail('progress.close', 'NO_BINDING', 'no peaks session binding — nothing to close', { projectRoot: canonical, path: subAgentSpawnPath(canonical) }, ['Run peaks workspace init for this project first']), options.json);
|
|
336
|
-
process.exitCode = 1;
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
printResult(io, ok('progress.close', {
|
|
340
|
-
projectRoot: canonical,
|
|
341
|
-
closed: false,
|
|
342
|
-
reason: result.reason,
|
|
343
|
-
note: 'no spawn record found — nothing to close (start has not been called this session, or the window has already auto-closed)'
|
|
344
|
-
}), options.json);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
const record = result.data;
|
|
348
|
-
// Best-effort close. We try three signals in order:
|
|
349
|
-
// (1) `pkill -f <watch command>` — the long-lived watch
|
|
350
|
-
// process. Killing it makes the terminal emulator
|
|
351
|
-
// close on most platforms (Terminal.app, gnome-
|
|
352
|
-
// terminal, konsole) but not all (alacritty, kitty
|
|
353
|
-
// keep the window).
|
|
354
|
-
// (2) macOS: AppleScript to close the Terminal.app
|
|
355
|
-
// window with the matching custom title.
|
|
356
|
-
// (3) Linux: wmctrl/xdotool by WM class as a fallback.
|
|
357
|
-
// Windows: taskkill /F /FI on the window title.
|
|
358
|
-
// We never throw from the close path — a failed close is
|
|
359
|
-
// a UX paper cut, not a correctness bug. The record is
|
|
360
|
-
// still cleared so the next `progress start` does not
|
|
361
|
-
// see a stale record.
|
|
362
|
-
const closeResult = await killSpawnedTerminal(record, canonical, platform());
|
|
363
|
-
clearSpawnRecord(canonical);
|
|
364
|
-
printResult(io, ok('progress.close', {
|
|
365
|
-
projectRoot: canonical,
|
|
366
|
-
closed: closeResult.signals.length > 0,
|
|
367
|
-
signals: closeResult.signals,
|
|
368
|
-
warnings: closeResult.warnings,
|
|
369
|
-
windowTitle: record.windowTitle,
|
|
370
|
-
spawnedAt: record.spawnedAt,
|
|
371
|
-
note: 'spawn record cleared. The next `peaks progress start` will spawn a fresh window.'
|
|
372
|
-
}), options.json);
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
printResult(io, fail('progress.close', 'PROGRESS_CLOSE_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and that peaks workspace init has been run for it']), options.json);
|
|
376
|
-
process.exitCode = 1;
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure platform-specific spawn-args construction for
|
|
3
|
-
* `peaks progress start`. Extracted out of the commander
|
|
4
|
-
* action handler so the three platform branches (darwin /
|
|
5
|
-
* linux / win32) can be unit-tested without spawning real
|
|
6
|
-
* terminals.
|
|
7
|
-
*
|
|
8
|
-
* The CLI action calls `buildStartSpawn` once per start
|
|
9
|
-
* attempt and feeds the returned `{ command, args }` pair to
|
|
10
|
-
* `child_process.spawn(..., { detached: true, stdio: 'ignore' })`.
|
|
11
|
-
*
|
|
12
|
-
* Cross-platform strategy (see progress-commands.ts for the
|
|
13
|
-
* full design notes):
|
|
14
|
-
*
|
|
15
|
-
* - macOS: `osascript -e 'tell application "Terminal" to
|
|
16
|
-
* do script "<shell>"'`. The shell command sets
|
|
17
|
-
* the title via the OSC 0 escape (printf), runs
|
|
18
|
-
* the brand banner, then exec's the watch.
|
|
19
|
-
* - Linux: First existing terminal emulator in
|
|
20
|
-
* { gnome-terminal, konsole, xfce4-terminal,
|
|
21
|
-
* tilix, alacritty, kitty }, with per-emulator
|
|
22
|
-
* flag translation. The shell inside also sets
|
|
23
|
-
* the title via OSC 0 (emulator --title is
|
|
24
|
-
* best-effort only — the shell overrides it).
|
|
25
|
-
* - Win32: `cmd /c start "<title>" cmd /k <shell>`. The
|
|
26
|
-
* shell uses the `title` builtin (cmd.exe
|
|
27
|
-
* builtin) to re-anchor the title before the
|
|
28
|
-
* watch runs, because cmd.exe overrides the
|
|
29
|
-
* start-title with the running command name.
|
|
30
|
-
*
|
|
31
|
-
* The function never throws on unsupported platforms; it
|
|
32
|
-
* returns a discriminated `unsupported` result so the caller
|
|
33
|
-
* can surface a clean error envelope.
|
|
34
|
-
*/
|
|
35
|
-
export type StartSpawnSpec = {
|
|
36
|
-
ok: true;
|
|
37
|
-
command: string;
|
|
38
|
-
args: string[];
|
|
39
|
-
} | {
|
|
40
|
-
ok: false;
|
|
41
|
-
unsupported: true;
|
|
42
|
-
};
|
|
43
|
-
export type BuildStartSpawnOptions = {
|
|
44
|
-
/** The peaks binary path the spawned shell will invoke. */
|
|
45
|
-
peaksBin: string;
|
|
46
|
-
/** Canonical project root (used to build the watch command). */
|
|
47
|
-
projectRoot: string;
|
|
48
|
-
/** Window/tab title shared across platforms. */
|
|
49
|
-
windowTitle: string;
|
|
50
|
-
/** Current platform (from `os.platform()`). */
|
|
51
|
-
platform: NodeJS.Platform;
|
|
52
|
-
/**
|
|
53
|
-
* Override for terminal detection on Linux. Defaults to
|
|
54
|
-
* `existsSync('/usr/bin/<name>')` for each candidate. Tests
|
|
55
|
-
* pass a stub to make the linux branch deterministic.
|
|
56
|
-
*/
|
|
57
|
-
linuxTerminalExists?: (name: string) => boolean;
|
|
58
|
-
};
|
|
59
|
-
export declare function buildStartSpawn(options: BuildStartSpawnOptions): StartSpawnSpec;
|