peaks-cli 1.3.2 → 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/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/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 +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/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 +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/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/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/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-qa/SKILL.md +57 -1
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +50 -8
- package/skills/peaks-solo/SKILL.md +77 -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 +17 -0
- package/skills/peaks-ui/SKILL.md +27 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice #009 / G5 RL-8 — sub-agent dispatch record archival + 30-day GC.
|
|
3
|
+
*
|
|
4
|
+
* Called by the `peaks session finish` / `peaks session abandon` /
|
|
5
|
+
* new-rid-startup hooks. Walks the per-session `.peaks/_sub_agents/<sid>/`
|
|
6
|
+
* tree and moves completed + disposed records to
|
|
7
|
+
* `.peaks/_runtime/<sid>/_archive/_sub_agents/<sliceId>/`.
|
|
8
|
+
*
|
|
9
|
+
* GC policy:
|
|
10
|
+
* - Records with `disposed: true` AND `outcome ∈ {success, failed,
|
|
11
|
+
* timeout, cancelled}` (not "no-execution") → archive + 30-day GC.
|
|
12
|
+
* - Records with `disposed: false` (any outcome) → archive but NOT
|
|
13
|
+
* GC'd. Next session will see them as still-pending in
|
|
14
|
+
* `_archive/.../in-flight/` and can resume reducer work.
|
|
15
|
+
*
|
|
16
|
+
* Lazy GC: `archiveSubAgentRecords` also scans the archive dir and
|
|
17
|
+
* deletes entries older than 30 days. No cron, no background daemon.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync } from 'node:fs';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { isDispatchStatus, isOutcome } from '../dispatch/dispatch-record-writer.js';
|
|
22
|
+
export const ARCHIVE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
23
|
+
/** Build the canonical archive dir for a given session + slice id. */
|
|
24
|
+
export function archiveDir(projectRoot, sessionId, sliceId) {
|
|
25
|
+
return join(projectRoot, '.peaks', '_runtime', sessionId, '_archive', '_sub_agents', sliceId);
|
|
26
|
+
}
|
|
27
|
+
/** Build the in-flight subdir (records not yet disposed). */
|
|
28
|
+
export function inFlightArchiveDir(projectRoot, sessionId, sliceId) {
|
|
29
|
+
return join(archiveDir(projectRoot, sessionId, sliceId), 'in-flight');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Archive the current `.peaks/_sub_agents/<sid>/` tree under
|
|
33
|
+
* `<archiveDir>` (per the sliceId), separating completed vs in-flight.
|
|
34
|
+
* Then run the 30-day GC over the archive dir.
|
|
35
|
+
*/
|
|
36
|
+
export function archiveSubAgentRecords(projectRoot, options) {
|
|
37
|
+
const now = options.now ?? (() => new Date());
|
|
38
|
+
const sourceDir = join(projectRoot, '.peaks', '_sub_agents', options.sessionId);
|
|
39
|
+
const completedDir = archiveDir(projectRoot, options.sessionId, options.sliceId);
|
|
40
|
+
const inFlightDir = inFlightArchiveDir(projectRoot, options.sessionId, options.sliceId);
|
|
41
|
+
mkdirSync(completedDir, { recursive: true });
|
|
42
|
+
mkdirSync(inFlightDir, { recursive: true });
|
|
43
|
+
let archivedCompleted = 0;
|
|
44
|
+
let archivedInFlight = 0;
|
|
45
|
+
if (existsSync(sourceDir)) {
|
|
46
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
47
|
+
if (!entry.startsWith('dispatch-') || !entry.endsWith('.json'))
|
|
48
|
+
continue;
|
|
49
|
+
const src = join(sourceDir, entry);
|
|
50
|
+
const record = readRecordOrNull(src);
|
|
51
|
+
if (record === null)
|
|
52
|
+
continue;
|
|
53
|
+
if (record.disposed === true && isCompletedOutcome(record.outcome)) {
|
|
54
|
+
renameSync(src, join(completedDir, entry));
|
|
55
|
+
archivedCompleted += 1;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
renameSync(src, join(inFlightDir, entry));
|
|
59
|
+
archivedInFlight += 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// 30-day GC: walk the archive root (any sliceId), delete entries
|
|
64
|
+
// whose file mtime is older than the retention.
|
|
65
|
+
const gcDeleted = runGarbageCollection(completedDir, now().getTime());
|
|
66
|
+
return { archivedCompleted, archivedInFlight, gcDeleted };
|
|
67
|
+
}
|
|
68
|
+
function isCompletedOutcome(outcome) {
|
|
69
|
+
return outcome === 'success' || outcome === 'failed' || outcome === 'timeout' || outcome === 'cancelled';
|
|
70
|
+
}
|
|
71
|
+
function readRecordOrNull(path) {
|
|
72
|
+
try {
|
|
73
|
+
const raw = readFileSync(path, 'utf8');
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
76
|
+
return null;
|
|
77
|
+
const obj = parsed;
|
|
78
|
+
if (!isOutcome(obj.outcome) || !isDispatchStatus(obj.status))
|
|
79
|
+
return null;
|
|
80
|
+
if (typeof obj.disposed !== 'boolean')
|
|
81
|
+
return null;
|
|
82
|
+
return obj;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function runGarbageCollection(dir, nowMs) {
|
|
89
|
+
if (!existsSync(dir))
|
|
90
|
+
return 0;
|
|
91
|
+
let deleted = 0;
|
|
92
|
+
for (const entry of readdirSync(dir)) {
|
|
93
|
+
const full = join(dir, entry);
|
|
94
|
+
try {
|
|
95
|
+
const stat = statSync(full);
|
|
96
|
+
if (stat.isDirectory()) {
|
|
97
|
+
deleted += runGarbageCollection(full, nowMs);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const ageMs = nowMs - stat.mtimeMs;
|
|
101
|
+
if (ageMs > ARCHIVE_RETENTION_MS) {
|
|
102
|
+
unlinkSync(full);
|
|
103
|
+
deleted += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* skip unreadable; do not crash the archive op */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return deleted;
|
|
111
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { summarize, type SubAgentLiveView } from './status-line-renderer.js';
|
|
2
|
+
export declare const DEFAULT_POLL_INTERVAL_MS = 10000;
|
|
3
|
+
export declare const DEFAULT_STALE_THRESHOLD_MS: number;
|
|
4
|
+
export type PollRecord = {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly role: string;
|
|
7
|
+
};
|
|
8
|
+
export type PollerStatusEvent = {
|
|
9
|
+
readonly kind: 'status';
|
|
10
|
+
readonly line: string;
|
|
11
|
+
readonly summary: ReturnType<typeof summarize>;
|
|
12
|
+
readonly views: readonly SubAgentLiveView[];
|
|
13
|
+
};
|
|
14
|
+
export type PollerStaleEvent = {
|
|
15
|
+
readonly kind: 'stale';
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly role: string;
|
|
18
|
+
readonly lastBeatAgoSec: number;
|
|
19
|
+
readonly thresholdSec: number;
|
|
20
|
+
};
|
|
21
|
+
export type PollerDoneEvent = {
|
|
22
|
+
readonly kind: 'done';
|
|
23
|
+
readonly summary: ReturnType<typeof summarize>;
|
|
24
|
+
};
|
|
25
|
+
export type PollerEvent = PollerStatusEvent | PollerStaleEvent | PollerDoneEvent;
|
|
26
|
+
export type PollerHandlers = {
|
|
27
|
+
onStatus?: (event: PollerStatusEvent) => void;
|
|
28
|
+
onStale?: (event: PollerStaleEvent) => void;
|
|
29
|
+
onDone?: (event: PollerDoneEvent) => void;
|
|
30
|
+
onError?: (error: unknown) => void;
|
|
31
|
+
};
|
|
32
|
+
export type PollerOptions = {
|
|
33
|
+
prefix: string;
|
|
34
|
+
intervalMs?: number;
|
|
35
|
+
staleThresholdMs?: number;
|
|
36
|
+
now?: () => Date;
|
|
37
|
+
};
|
|
38
|
+
export declare class BatchHeartbeatPoller {
|
|
39
|
+
private readonly records;
|
|
40
|
+
private readonly handlers;
|
|
41
|
+
private readonly options;
|
|
42
|
+
private timer;
|
|
43
|
+
private prevStale;
|
|
44
|
+
private prevSummaryDone;
|
|
45
|
+
private running;
|
|
46
|
+
constructor(records: readonly PollRecord[], handlers: PollerHandlers, options: PollerOptions);
|
|
47
|
+
start(): void;
|
|
48
|
+
stop(): void;
|
|
49
|
+
/** Force a tick (testing seam + low-level access for explicit invocation). */
|
|
50
|
+
tick(): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readRecords } from '../dispatch/dispatch-record-writer.js';
|
|
2
|
+
import { renderStatusLine, summarize, viewSubAgent } from './status-line-renderer.js';
|
|
3
|
+
export const DEFAULT_POLL_INTERVAL_MS = 10_000;
|
|
4
|
+
export const DEFAULT_STALE_THRESHOLD_MS = 5 * 60 * 1000;
|
|
5
|
+
const TERMINAL_STATUSES = ['done', 'failed'];
|
|
6
|
+
const TERMINAL_RECORD_STATUSES = [
|
|
7
|
+
'done',
|
|
8
|
+
'failed',
|
|
9
|
+
'cancelled',
|
|
10
|
+
'no-execution'
|
|
11
|
+
];
|
|
12
|
+
export class BatchHeartbeatPoller {
|
|
13
|
+
records;
|
|
14
|
+
handlers;
|
|
15
|
+
options;
|
|
16
|
+
timer = null;
|
|
17
|
+
prevStale = new Set();
|
|
18
|
+
prevSummaryDone = 0;
|
|
19
|
+
running = false;
|
|
20
|
+
constructor(records, handlers, options) {
|
|
21
|
+
this.records = records;
|
|
22
|
+
this.handlers = handlers;
|
|
23
|
+
this.options = options;
|
|
24
|
+
}
|
|
25
|
+
start() {
|
|
26
|
+
if (this.running)
|
|
27
|
+
return;
|
|
28
|
+
this.running = true;
|
|
29
|
+
this.tick();
|
|
30
|
+
const interval = this.options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
31
|
+
this.timer = setInterval(() => this.tick(), interval);
|
|
32
|
+
// Don't keep the process alive just for the poller.
|
|
33
|
+
this.timer.unref?.();
|
|
34
|
+
}
|
|
35
|
+
stop() {
|
|
36
|
+
this.running = false;
|
|
37
|
+
if (this.timer) {
|
|
38
|
+
clearInterval(this.timer);
|
|
39
|
+
this.timer = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Force a tick (testing seam + low-level access for explicit invocation). */
|
|
43
|
+
tick() {
|
|
44
|
+
let recs;
|
|
45
|
+
try {
|
|
46
|
+
recs = readRecords(this.records.map((r) => r.path));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.handlers.onError?.(error);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const now = this.options.now ?? (() => new Date());
|
|
53
|
+
const summary = summarize(recs);
|
|
54
|
+
const views = recs.map((r) => viewSubAgent(r, now));
|
|
55
|
+
const line = renderStatusLine(this.options.prefix, recs, now);
|
|
56
|
+
this.handlers.onStatus?.({ kind: 'status', line, summary, views });
|
|
57
|
+
const staleThresholdSec = Math.floor((this.options.staleThresholdMs ?? DEFAULT_STALE_THRESHOLD_MS) / 1000);
|
|
58
|
+
for (const rec of recs) {
|
|
59
|
+
const v = viewSubAgent(rec, now);
|
|
60
|
+
if (v.isStale) {
|
|
61
|
+
const key = `${rec.role}::${rec.requestId}`;
|
|
62
|
+
if (!this.prevStale.has(key)) {
|
|
63
|
+
this.prevStale.add(key);
|
|
64
|
+
this.handlers.onStale?.({
|
|
65
|
+
kind: 'stale',
|
|
66
|
+
path: this.records.find((p) => p.role === rec.role)?.path ?? '',
|
|
67
|
+
role: rec.role,
|
|
68
|
+
lastBeatAgoSec: v.lastBeatAgoSec ?? -1,
|
|
69
|
+
thresholdSec: staleThresholdSec
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (summary.total > 0 && summary.done === summary.total && this.prevSummaryDone !== summary.done) {
|
|
75
|
+
this.prevSummaryDone = summary.done;
|
|
76
|
+
this.handlers.onDone?.({ kind: 'done', summary });
|
|
77
|
+
this.stop();
|
|
78
|
+
}
|
|
79
|
+
else if (recs.length > 0 &&
|
|
80
|
+
recs.every((r) => TERMINAL_RECORD_STATUSES.includes(r.status) || TERMINAL_STATUSES.includes(r.status))) {
|
|
81
|
+
this.handlers.onDone?.({ kind: 'done', summary });
|
|
82
|
+
this.stop();
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this.prevSummaryDone = summary.done;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G6.5 — status line renderer for the peaks-solo batch-sync wait period.
|
|
3
|
+
*
|
|
4
|
+
* Single line, 80-120 chars, status-line-friendly. The shape is
|
|
5
|
+
* documented in PRD §G6.5:
|
|
6
|
+
*
|
|
7
|
+
* [peaks-solo] swarm 3/3 running | rd-planning 45% (12s ago) | qa-test-cases 30% (5s ago) | ui-design 20% (2s ago)
|
|
8
|
+
* [peaks-solo] swarm 3/3 running | rd-planning 70% (8s ago) | qa-test-cases 50% (3s ago) | ui-design 30% (6s ago)
|
|
9
|
+
* ...
|
|
10
|
+
* [peaks-solo] swarm 3/3 done in 47.3s
|
|
11
|
+
*
|
|
12
|
+
* Pure helper; the poller calls it once per tick. No IO.
|
|
13
|
+
*/
|
|
14
|
+
import type { DispatchRecord } from '../dispatch/dispatch-record-writer.js';
|
|
15
|
+
export type SubAgentLiveView = {
|
|
16
|
+
readonly role: string;
|
|
17
|
+
readonly status: string;
|
|
18
|
+
readonly progress: number | null;
|
|
19
|
+
readonly lastBeatAgoSec: number | null;
|
|
20
|
+
readonly isStale: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type SwarmSummary = {
|
|
23
|
+
readonly total: number;
|
|
24
|
+
readonly running: number;
|
|
25
|
+
readonly done: number;
|
|
26
|
+
readonly failed: number;
|
|
27
|
+
readonly stale: number;
|
|
28
|
+
};
|
|
29
|
+
/** Build a per-sub-agent view of the current state of one record. */
|
|
30
|
+
export declare function viewSubAgent(record: DispatchRecord, now?: () => Date): SubAgentLiveView;
|
|
31
|
+
/** Aggregate swarm summary. */
|
|
32
|
+
export declare function summarize(records: readonly DispatchRecord[]): SwarmSummary;
|
|
33
|
+
/** Render a single status line. */
|
|
34
|
+
export declare function renderStatusLine(prefix: string, records: readonly DispatchRecord[], now?: () => Date): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const STALE_THRESHOLD_SEC = 5 * 60;
|
|
2
|
+
/** Build a per-sub-agent view of the current state of one record. */
|
|
3
|
+
export function viewSubAgent(record, now = () => new Date()) {
|
|
4
|
+
const latest = record.heartbeats[record.heartbeats.length - 1];
|
|
5
|
+
const lastBeatAgo = record.lastBeatAt
|
|
6
|
+
? Math.max(0, Math.floor((now().getTime() - new Date(record.lastBeatAt).getTime()) / 1000))
|
|
7
|
+
: null;
|
|
8
|
+
const isStale = lastBeatAgo !== null && lastBeatAgo > STALE_THRESHOLD_SEC;
|
|
9
|
+
return {
|
|
10
|
+
role: record.role,
|
|
11
|
+
status: record.status,
|
|
12
|
+
progress: latest ? latest.progress : null,
|
|
13
|
+
lastBeatAgoSec: lastBeatAgo,
|
|
14
|
+
isStale
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Aggregate swarm summary. */
|
|
18
|
+
export function summarize(records) {
|
|
19
|
+
let running = 0;
|
|
20
|
+
let done = 0;
|
|
21
|
+
let failed = 0;
|
|
22
|
+
let stale = 0;
|
|
23
|
+
for (const r of records) {
|
|
24
|
+
const v = viewSubAgent(r);
|
|
25
|
+
if (v.isStale)
|
|
26
|
+
stale += 1;
|
|
27
|
+
if (r.status === 'done')
|
|
28
|
+
done += 1;
|
|
29
|
+
else if (r.status === 'failed' || r.status === 'cancelled')
|
|
30
|
+
failed += 1;
|
|
31
|
+
else
|
|
32
|
+
running += 1;
|
|
33
|
+
}
|
|
34
|
+
return { total: records.length, running, done, failed, stale };
|
|
35
|
+
}
|
|
36
|
+
/** Render a single status line. */
|
|
37
|
+
export function renderStatusLine(prefix, records, now = () => new Date()) {
|
|
38
|
+
if (records.length === 0) {
|
|
39
|
+
return `${prefix} swarm 0/0 idle`;
|
|
40
|
+
}
|
|
41
|
+
const summary = summarize(records);
|
|
42
|
+
const allDone = summary.done === summary.total;
|
|
43
|
+
if (allDone) {
|
|
44
|
+
return `${prefix} swarm ${summary.done}/${summary.total} done`;
|
|
45
|
+
}
|
|
46
|
+
const parts = records.map((r) => renderOne(r, now));
|
|
47
|
+
return `${prefix} swarm ${summary.running}/${summary.total} running | ${parts.join(' | ')}`;
|
|
48
|
+
}
|
|
49
|
+
function renderOne(record, now) {
|
|
50
|
+
const view = viewSubAgent(record, now);
|
|
51
|
+
const pct = view.progress !== null ? `${view.progress}%` : '?%';
|
|
52
|
+
const ago = view.lastBeatAgoSec !== null ? `${view.lastBeatAgoSec}s ago` : 'no beat';
|
|
53
|
+
const stale = view.isStale ? ' ⚠ stale' : '';
|
|
54
|
+
return `${view.role} ${pct} (${ago})${stale}`;
|
|
55
|
+
}
|
|
@@ -144,6 +144,42 @@ export declare function migrateOldRuntimeState(projectRoot: string): {
|
|
|
144
144
|
message: string;
|
|
145
145
|
}>;
|
|
146
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* One-time sub-agent state migration (slice 2026-06-06-sub-agent-spawn-bug-and-decouple).
|
|
149
|
+
*
|
|
150
|
+
* Move the legacy per-session sub-agent state files at:
|
|
151
|
+
* - `.peaks/<sid>/system/subagent-progress.json`
|
|
152
|
+
* - `.peaks/<sid>/system/progress-spawn.json`
|
|
153
|
+
* into the new canonical home at:
|
|
154
|
+
* - `.peaks/_sub_agents/<sid>/subagent-progress.json`
|
|
155
|
+
* - `.peaks/_sub_agents/<sid>/progress-spawn.json`
|
|
156
|
+
*
|
|
157
|
+
* Behavior:
|
|
158
|
+
* - Idempotent: re-running on a tree that is already on the new layout
|
|
159
|
+
* produces `migratedFiles: []`.
|
|
160
|
+
* - Best-effort: uses `fs.renameSync` and falls back to `copyFileSync +
|
|
161
|
+
* unlinkSync` if rename throws (e.g. cross-device move on Windows).
|
|
162
|
+
* - Empty `<sid>/system/` dir removal (R-2 guard): the legacy `system/`
|
|
163
|
+
* subdir is only removed when it has zero other files, so a tree where
|
|
164
|
+
* the user had unrelated content in `system/` is left untouched.
|
|
165
|
+
* - New-path-wins: when both old and new files exist, the old file is
|
|
166
|
+
* removed (the new path is authoritative).
|
|
167
|
+
*
|
|
168
|
+
* Walks every discovered session — not just the canonical one — so a user
|
|
169
|
+
* with 6 pre-migration sessions gets all of them migrated in one reconcile
|
|
170
|
+
* pass.
|
|
171
|
+
*
|
|
172
|
+
* @returns `{ migratedFiles, errors }`. `migratedFiles` lists the *old*
|
|
173
|
+
* relative paths (e.g. `.peaks/<sid>/system/subagent-progress.json`) that
|
|
174
|
+
* were successfully moved. `errors` lists per-file failures.
|
|
175
|
+
*/
|
|
176
|
+
export declare function migrateSubAgentState(projectRoot: string): {
|
|
177
|
+
migratedFiles: string[];
|
|
178
|
+
errors: Array<{
|
|
179
|
+
path: string;
|
|
180
|
+
message: string;
|
|
181
|
+
}>;
|
|
182
|
+
};
|
|
147
183
|
/**
|
|
148
184
|
* Top-level orchestrator. Wires migration (added in slice
|
|
149
185
|
* 2026-06-05-peaks-runtime-layer), discovery, canonical pick, re-point,
|
|
@@ -16,11 +16,20 @@
|
|
|
16
16
|
* Pure hand-rolled; uses only node:fs, node:path, and the existing
|
|
17
17
|
* session-manager helper for writing the binding. No new dependencies.
|
|
18
18
|
*/
|
|
19
|
-
import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, unlinkSync } from 'node:fs';
|
|
19
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync } from 'node:fs';
|
|
20
20
|
import { dirname, join, resolve } from 'node:path';
|
|
21
21
|
import { getSessionIdCanonical, setCurrentSessionBinding } from '../session/session-manager.js';
|
|
22
22
|
const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]+$/;
|
|
23
23
|
const META_FILE = 'session.json';
|
|
24
|
+
// Sub-agent state file basenames (slice 2026-06-06-sub-agent-spawn-bug-and-decouple).
|
|
25
|
+
// The legacy location was `.peaks/<sid>/system/<filename>`; the canonical new
|
|
26
|
+
// location is `.peaks/_sub_agents/<sid>/<filename>`. `migrateSubAgentState`
|
|
27
|
+
// moves the two files between these homes on every `reconcileWorkspace` run.
|
|
28
|
+
const SUB_AGENT_MIGRATION_FILES = [
|
|
29
|
+
'subagent-progress.json',
|
|
30
|
+
'progress-spawn.json'
|
|
31
|
+
];
|
|
32
|
+
const SUB_AGENTS_DIR = '_sub_agents';
|
|
24
33
|
// As of slice 2026-06-05-peaks-runtime-layer these old paths are the
|
|
25
34
|
// back-compat read-only fallbacks; the canonical new home is
|
|
26
35
|
// `.peaks/_runtime/`. `migrateOldRuntimeState` moves them to the new
|
|
@@ -488,6 +497,89 @@ function copyDirRecursiveSync(src, dest) {
|
|
|
488
497
|
}
|
|
489
498
|
}
|
|
490
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* One-time sub-agent state migration (slice 2026-06-06-sub-agent-spawn-bug-and-decouple).
|
|
502
|
+
*
|
|
503
|
+
* Move the legacy per-session sub-agent state files at:
|
|
504
|
+
* - `.peaks/<sid>/system/subagent-progress.json`
|
|
505
|
+
* - `.peaks/<sid>/system/progress-spawn.json`
|
|
506
|
+
* into the new canonical home at:
|
|
507
|
+
* - `.peaks/_sub_agents/<sid>/subagent-progress.json`
|
|
508
|
+
* - `.peaks/_sub_agents/<sid>/progress-spawn.json`
|
|
509
|
+
*
|
|
510
|
+
* Behavior:
|
|
511
|
+
* - Idempotent: re-running on a tree that is already on the new layout
|
|
512
|
+
* produces `migratedFiles: []`.
|
|
513
|
+
* - Best-effort: uses `fs.renameSync` and falls back to `copyFileSync +
|
|
514
|
+
* unlinkSync` if rename throws (e.g. cross-device move on Windows).
|
|
515
|
+
* - Empty `<sid>/system/` dir removal (R-2 guard): the legacy `system/`
|
|
516
|
+
* subdir is only removed when it has zero other files, so a tree where
|
|
517
|
+
* the user had unrelated content in `system/` is left untouched.
|
|
518
|
+
* - New-path-wins: when both old and new files exist, the old file is
|
|
519
|
+
* removed (the new path is authoritative).
|
|
520
|
+
*
|
|
521
|
+
* Walks every discovered session — not just the canonical one — so a user
|
|
522
|
+
* with 6 pre-migration sessions gets all of them migrated in one reconcile
|
|
523
|
+
* pass.
|
|
524
|
+
*
|
|
525
|
+
* @returns `{ migratedFiles, errors }`. `migratedFiles` lists the *old*
|
|
526
|
+
* relative paths (e.g. `.peaks/<sid>/system/subagent-progress.json`) that
|
|
527
|
+
* were successfully moved. `errors` lists per-file failures.
|
|
528
|
+
*/
|
|
529
|
+
export function migrateSubAgentState(projectRoot) {
|
|
530
|
+
const root = resolve(projectRoot);
|
|
531
|
+
const newDir = join(root, '.peaks', SUB_AGENTS_DIR);
|
|
532
|
+
const migratedFiles = [];
|
|
533
|
+
const errors = [];
|
|
534
|
+
for (const session of discoverSessions(projectRoot)) {
|
|
535
|
+
const oldSystemDir = join(session.path, 'system');
|
|
536
|
+
if (!existsSync(oldSystemDir))
|
|
537
|
+
continue;
|
|
538
|
+
const newSessionDir = join(newDir, session.sessionId);
|
|
539
|
+
mkdirSync(newSessionDir, { recursive: true });
|
|
540
|
+
for (const fname of SUB_AGENT_MIGRATION_FILES) {
|
|
541
|
+
const oldPath = join(oldSystemDir, fname);
|
|
542
|
+
const newPath = join(newSessionDir, fname);
|
|
543
|
+
if (!existsSync(oldPath))
|
|
544
|
+
continue;
|
|
545
|
+
if (existsSync(newPath)) {
|
|
546
|
+
// New path is authoritative; remove stale old file.
|
|
547
|
+
try {
|
|
548
|
+
rmSync(oldPath, { force: true });
|
|
549
|
+
}
|
|
550
|
+
catch { /* best effort */ }
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
try {
|
|
555
|
+
renameSync(oldPath, newPath);
|
|
556
|
+
}
|
|
557
|
+
catch (renameError) {
|
|
558
|
+
// Cross-device or locked-file fallback: copy + unlink.
|
|
559
|
+
copyFileSync(oldPath, newPath);
|
|
560
|
+
unlinkSync(oldPath);
|
|
561
|
+
}
|
|
562
|
+
migratedFiles.push(join('.peaks', session.sessionId, 'system', fname));
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
errors.push({
|
|
566
|
+
path: oldPath,
|
|
567
|
+
message: error instanceof Error ? error.message : String(error)
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
// R-2 guard: only remove the legacy system/ dir when it has zero
|
|
572
|
+
// remaining files (the user might have unrelated content there).
|
|
573
|
+
try {
|
|
574
|
+
const remaining = readdirSync(oldSystemDir);
|
|
575
|
+
if (remaining.length === 0) {
|
|
576
|
+
rmdirSync(oldSystemDir);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
catch { /* best effort */ }
|
|
580
|
+
}
|
|
581
|
+
return { migratedFiles, errors };
|
|
582
|
+
}
|
|
491
583
|
/**
|
|
492
584
|
* Top-level orchestrator. Wires migration (added in slice
|
|
493
585
|
* 2026-06-05-peaks-runtime-layer), discovery, canonical pick, re-point,
|
|
@@ -503,11 +595,19 @@ export function reconcileWorkspace(options) {
|
|
|
503
595
|
// before that read means the new path is the only path observed by
|
|
504
596
|
// `getSessionIdCanonical` after this call returns.
|
|
505
597
|
const migration = migrateOldRuntimeState(projectRoot);
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
598
|
+
const subAgentMigration = migrateSubAgentState(projectRoot);
|
|
599
|
+
const migrateErrors = [
|
|
600
|
+
...migration.errors.map((e) => ({
|
|
601
|
+
kind: 'migrate',
|
|
602
|
+
path: e.path,
|
|
603
|
+
message: e.message
|
|
604
|
+
})),
|
|
605
|
+
...subAgentMigration.errors.map((e) => ({
|
|
606
|
+
kind: 'migrate',
|
|
607
|
+
path: e.path,
|
|
608
|
+
message: e.message
|
|
609
|
+
}))
|
|
610
|
+
];
|
|
511
611
|
const sessions = discoverSessions(projectRoot);
|
|
512
612
|
const activeSkillSessionId = readActiveSkillSessionId(projectRoot);
|
|
513
613
|
const canonical = pickCanonicalSession(sessions, activeSkillSessionId);
|
|
@@ -575,6 +675,7 @@ export function reconcileWorkspace(options) {
|
|
|
575
675
|
apply,
|
|
576
676
|
repointed,
|
|
577
677
|
migratedFiles: migration.migratedFiles,
|
|
678
|
+
subAgentStateMigrated: subAgentMigration.migratedFiles.length,
|
|
578
679
|
errors: [...migrateErrors, ...deletionResult.errors],
|
|
579
680
|
changeMarker,
|
|
580
681
|
systemCleaned
|
|
@@ -65,6 +65,18 @@ export type ReconcileResult = {
|
|
|
65
65
|
* consumers can ignore this field.
|
|
66
66
|
*/
|
|
67
67
|
migratedFiles: string[];
|
|
68
|
+
/**
|
|
69
|
+
* Count of legacy per-session sub-agent state files moved from
|
|
70
|
+
* `.peaks/<sid>/system/{subagent-progress,progress-spawn}.json` into
|
|
71
|
+
* `.peaks/_sub_agents/<sid>/` during this reconcile run.
|
|
72
|
+
*
|
|
73
|
+
* Added in slice 2026-06-06-sub-agent-spawn-bug-and-decouple. The
|
|
74
|
+
* detailed list of moved files is not surfaced here (the count is
|
|
75
|
+
* what the CLI summary and QA test assert on); the underlying
|
|
76
|
+
* `migrateSubAgentState` helper returns the full path list for
|
|
77
|
+
* forensics. Additive — older consumers can ignore this field.
|
|
78
|
+
*/
|
|
79
|
+
subAgentStateMigrated: number;
|
|
68
80
|
/**
|
|
69
81
|
* Errors encountered during the migration step. Each entry has a
|
|
70
82
|
* `kind: 'migrate'` discriminator so consumers can tell migration
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.3.
|
|
1
|
+
export declare const CLI_VERSION = "1.3.3";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.3.
|
|
1
|
+
export const CLI_VERSION = "1.3.3";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Cross-AI-IDE workflow-gating CLI + skill family (Claude Code shipped, Trae in progress; Codex / Cursor / Qoder / Tongyi Lingma on the roadmap).",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"@colbymchenry/codegraph": "0.7.10",
|
|
57
57
|
"chalk": "^5.6.2",
|
|
58
58
|
"commander": "^12.1.0",
|
|
59
|
+
"headroom-ai": "0.22.4",
|
|
59
60
|
"ora": "^8.2.0",
|
|
60
61
|
"shadcn": "4.7.0",
|
|
61
62
|
"terminal-kit": "^3.1.2"
|