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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R-2 path-safety guard for sub-agent state files.
|
|
3
|
+
*
|
|
4
|
+
* Slice 2026-06-07-sub-agent-dispatch-decouple (G4) — reuses the same
|
|
5
|
+
* R-2 symlink/junction guard as `assertSafeSettingsFile` for
|
|
6
|
+
* `.peaks/_sub_agents/<sid>/dispatch-<rid>-<ts>.json` paths.
|
|
7
|
+
*
|
|
8
|
+
* Why a separate helper:
|
|
9
|
+
* - The settings-file guard is a per-IDE 8th field (settings.json path);
|
|
10
|
+
* this one is for runtime sub-agent trace records.
|
|
11
|
+
* - Both reject paths that resolve outside the project root after
|
|
12
|
+
* symlink resolution, and both reject `..` segments before
|
|
13
|
+
* resolution (defense in depth).
|
|
14
|
+
*
|
|
15
|
+
* The guard is intentionally small and pure (no IO beyond `realpathSync`)
|
|
16
|
+
* so it can be called from CLI handlers and from service helpers alike.
|
|
17
|
+
*/
|
|
18
|
+
import { realpathSync } from 'node:fs';
|
|
19
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
20
|
+
const SUB_AGENTS_DIR = '_sub_agents';
|
|
21
|
+
/** Build the canonical record path for a given session/rid/timestamp. */
|
|
22
|
+
export function dispatchRecordPath(projectRoot, sid, rid, ts = new Date()) {
|
|
23
|
+
const safeSid = sanitizeSegment(sid, 'sessionId');
|
|
24
|
+
const safeRid = sanitizeSegment(rid, 'requestId');
|
|
25
|
+
const tsCompact = ts.toISOString().replace(/[:.]/g, '-');
|
|
26
|
+
return resolve(projectRoot, '.peaks', SUB_AGENTS_DIR, safeSid, `dispatch-${safeRid}-${tsCompact}.json`);
|
|
27
|
+
}
|
|
28
|
+
/** The directory under which dispatch records live. */
|
|
29
|
+
export function dispatchRecordsDir(projectRoot, sid) {
|
|
30
|
+
const safeSid = sanitizeSegment(sid, 'sessionId');
|
|
31
|
+
return resolve(projectRoot, '.peaks', SUB_AGENTS_DIR, safeSid);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Assert that `recordPath` lives under `projectRoot/.peaks/_sub_agents/<sid>/`.
|
|
35
|
+
* Rejects symlink/junction escapes and `..` segments.
|
|
36
|
+
*
|
|
37
|
+
* Throws an Error with `.code = 'INVALID_RECORD_PATH'` on rejection so
|
|
38
|
+
* the CLI can map to `{ok: false, code: "INVALID_RECORD_PATH"}`.
|
|
39
|
+
*/
|
|
40
|
+
export function assertSafeDispatchRecordPath(recordPath, projectRoot) {
|
|
41
|
+
if (!isAbsolute(recordPath)) {
|
|
42
|
+
throw invalidPathError(recordPath, 'must be absolute');
|
|
43
|
+
}
|
|
44
|
+
// Reject lexical `..` segments in the raw path BEFORE the OS-level resolver
|
|
45
|
+
// collapses them. POSIX `path.normalize` will turn `/a/b/../c` into `/a/c`,
|
|
46
|
+
// silently dropping the `..` — and that is exactly the symlink/junction
|
|
47
|
+
// escape we are guarding against. Test the raw string form.
|
|
48
|
+
const rawSegments = recordPath.split(/[\\/]/);
|
|
49
|
+
if (rawSegments.includes('..')) {
|
|
50
|
+
throw invalidPathError(recordPath, 'must not contain .. segments');
|
|
51
|
+
}
|
|
52
|
+
const expected = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
|
|
53
|
+
const rel = relative(expected, recordPath);
|
|
54
|
+
if (rel.startsWith('..') || isAbsolute(rel)) {
|
|
55
|
+
throw invalidPathError(recordPath, 'must be under .peaks/_sub_agents/');
|
|
56
|
+
}
|
|
57
|
+
let realRecord;
|
|
58
|
+
let realRoot;
|
|
59
|
+
try {
|
|
60
|
+
// For existing files, realpathSync resolves symlinks/junctions.
|
|
61
|
+
// For new files (the common case for `dispatch` CLI writing the
|
|
62
|
+
// record for the first time), we realpath the parent + check the
|
|
63
|
+
// leaf's basename shape.
|
|
64
|
+
const parent = dirname(recordPath);
|
|
65
|
+
const realParent = realpathSync(parent);
|
|
66
|
+
realRecord = resolve(realParent, recordPath.slice(parent.length + 1));
|
|
67
|
+
realRoot = realpathSync(projectRoot);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
// Realpath can fail if the parent does not exist yet (CLI is about
|
|
71
|
+
// to create it). Fall back to lexical comparison against the
|
|
72
|
+
// canonical projectRoot — the write will then create the file,
|
|
73
|
+
// and any symlink in the parent will be caught on the next read.
|
|
74
|
+
const fallback = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
|
|
75
|
+
const rel2 = relative(fallback, recordPath);
|
|
76
|
+
if (rel2.startsWith('..') || isAbsolute(rel2)) {
|
|
77
|
+
throw invalidPathError(recordPath, 'must be under .peaks/_sub_agents/');
|
|
78
|
+
}
|
|
79
|
+
return recordPath;
|
|
80
|
+
}
|
|
81
|
+
const realRel = relative(realRoot, realRecord);
|
|
82
|
+
if (realRel.startsWith('..' + sep) || realRel === '..' || isAbsolute(realRel)) {
|
|
83
|
+
throw invalidPathError(recordPath, 'escapes project root via symlink');
|
|
84
|
+
}
|
|
85
|
+
return realRecord;
|
|
86
|
+
}
|
|
87
|
+
function sanitizeSegment(segment, label) {
|
|
88
|
+
if (typeof segment !== 'string' || segment.length === 0) {
|
|
89
|
+
throw new Error(`Invalid ${label}: empty`);
|
|
90
|
+
}
|
|
91
|
+
if (!/^[A-Za-z0-9._-]+$/.test(segment)) {
|
|
92
|
+
throw new Error(`Invalid ${label}: must match [A-Za-z0-9._-]+ (got ${JSON.stringify(segment)})`);
|
|
93
|
+
}
|
|
94
|
+
if (segment.includes('..')) {
|
|
95
|
+
throw new Error(`Invalid ${label}: must not contain ..`);
|
|
96
|
+
}
|
|
97
|
+
return segment;
|
|
98
|
+
}
|
|
99
|
+
function invalidPathError(path, reason) {
|
|
100
|
+
const err = new Error(`Unsafe dispatch record path (${reason}): ${path}`);
|
|
101
|
+
err.code = 'INVALID_RECORD_PATH';
|
|
102
|
+
err.path = path;
|
|
103
|
+
return err;
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getSessionDir(projectRoot: string, sessionId: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical session-directory resolver.
|
|
3
|
+
*
|
|
4
|
+
* As of slice 2026-06-05-peaks-runtime-layer the per-session workspace
|
|
5
|
+
* lives at `<root>/.peaks/_runtime/<sessionId>/` (NOT at the legacy
|
|
6
|
+
* `<root>/.peaks/<sessionId>/` location). All **write** paths MUST route
|
|
7
|
+
* through this helper. The legacy top-level path is preserved as a
|
|
8
|
+
* back-compat **read** fallback only (see
|
|
9
|
+
* `src/services/artifacts/request-artifact-service.ts:662` etc.).
|
|
10
|
+
*
|
|
11
|
+
* The corresponding test in
|
|
12
|
+
* `tests/unit/services/session/session-dir-canonical.test.ts` enforces
|
|
13
|
+
* two invariants:
|
|
14
|
+
*
|
|
15
|
+
* (a) `getSessionDir(root, sid)` returns `<root>/.peaks/_runtime/<sid>`.
|
|
16
|
+
* (b) A static scan of `src/` flags any direct join of `.peaks` +
|
|
17
|
+
* `sessionId` that does NOT route through this resolver. The
|
|
18
|
+
* back-compat **read** sites are excluded by explicit allow-list.
|
|
19
|
+
*
|
|
20
|
+
* @param projectRoot - Absolute path to the project root.
|
|
21
|
+
* @param sessionId - The session identifier (e.g. `2026-06-06-session-5b1095`).
|
|
22
|
+
* @returns Absolute path to the canonical session directory.
|
|
23
|
+
*/
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
export function getSessionDir(projectRoot, sessionId) {
|
|
26
|
+
return join(projectRoot, '.peaks', '_runtime', sessionId);
|
|
27
|
+
}
|
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
|
|
2
|
+
export { getSessionDir } from './getSessionDir.js';
|
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding } from './session-manager.js';
|
|
2
|
+
export { getSessionDir } from './getSessionDir.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CancelResult {
|
|
2
|
+
readonly cancelled: number;
|
|
3
|
+
readonly scanned: number;
|
|
4
|
+
readonly paths: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Walk `.peaks/_sub_agents/<sid>/`, mark every in-flight record as
|
|
8
|
+
* `cancelled + disposed`, write back atomically (tmp + rename), and
|
|
9
|
+
* return the count. Records that already have `completedAt` set are
|
|
10
|
+
* skipped (they were already finished by the time cancel fired).
|
|
11
|
+
*/
|
|
12
|
+
export declare function cancelInFlightDispatches(projectRoot: string, sessionId: string, options?: {
|
|
13
|
+
now?: () => Date;
|
|
14
|
+
}): CancelResult;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice #009 / G5 RL-9 — user cancel must dispose in-flight dispatch records.
|
|
3
|
+
*
|
|
4
|
+
* When Solo's main loop receives SIGINT (Ctrl-C) or the user runs
|
|
5
|
+
* `peaks workflow cancel --rid <rid>`, the loop MUST mark in-flight
|
|
6
|
+
* dispatch records as `outcome: "cancelled"` + `disposed: true` +
|
|
7
|
+
* `disposedAt: now()` BEFORE exiting.
|
|
8
|
+
*
|
|
9
|
+
* "In-flight" = a record with `createdAt` populated AND
|
|
10
|
+
* `completedAt: null`. Those are the ones that the LLM got a toolCall
|
|
11
|
+
* for but did not (yet) confirm completion.
|
|
12
|
+
*
|
|
13
|
+
* This module is the helper Solo calls. The wiring (signal listener,
|
|
14
|
+
* cancel command side-effect) is the Solo main loop's responsibility;
|
|
15
|
+
* this module is the pure data operation.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
const CANCELLED_OUTCOME = 'cancelled';
|
|
20
|
+
const CANCELLED_STATUS = 'cancelled';
|
|
21
|
+
/**
|
|
22
|
+
* Walk `.peaks/_sub_agents/<sid>/`, mark every in-flight record as
|
|
23
|
+
* `cancelled + disposed`, write back atomically (tmp + rename), and
|
|
24
|
+
* return the count. Records that already have `completedAt` set are
|
|
25
|
+
* skipped (they were already finished by the time cancel fired).
|
|
26
|
+
*/
|
|
27
|
+
export function cancelInFlightDispatches(projectRoot, sessionId, options = {}) {
|
|
28
|
+
const now = options.now ?? (() => new Date());
|
|
29
|
+
const dir = join(projectRoot, '.peaks', '_sub_agents', sessionId);
|
|
30
|
+
if (!existsSync(dir)) {
|
|
31
|
+
return { cancelled: 0, scanned: 0, paths: [] };
|
|
32
|
+
}
|
|
33
|
+
let scanned = 0;
|
|
34
|
+
const cancelledPaths = [];
|
|
35
|
+
for (const entry of readdirSync(dir)) {
|
|
36
|
+
if (!entry.startsWith('dispatch-') || !entry.endsWith('.json'))
|
|
37
|
+
continue;
|
|
38
|
+
const fullPath = join(dir, entry);
|
|
39
|
+
scanned += 1;
|
|
40
|
+
const record = readRecordOrNull(fullPath);
|
|
41
|
+
if (record === null)
|
|
42
|
+
continue;
|
|
43
|
+
if (record.completedAt !== null)
|
|
44
|
+
continue; // already finished
|
|
45
|
+
const cancelled = {
|
|
46
|
+
...record,
|
|
47
|
+
completedAt: now().toISOString(),
|
|
48
|
+
outcome: CANCELLED_OUTCOME,
|
|
49
|
+
status: CANCELLED_STATUS,
|
|
50
|
+
disposed: true,
|
|
51
|
+
disposedAt: now().toISOString()
|
|
52
|
+
};
|
|
53
|
+
writeFileSync(fullPath, JSON.stringify(cancelled, null, 2) + '\n', 'utf8');
|
|
54
|
+
cancelledPaths.push(fullPath);
|
|
55
|
+
}
|
|
56
|
+
return { cancelled: cancelledPaths.length, scanned, paths: cancelledPaths };
|
|
57
|
+
}
|
|
58
|
+
function readRecordOrNull(path) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = readFileSync(path, 'utf8');
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
63
|
+
return null;
|
|
64
|
+
const obj = parsed;
|
|
65
|
+
if (typeof obj.createdAt !== 'string')
|
|
66
|
+
return null;
|
|
67
|
+
if (typeof obj.completedAt !== 'string' && obj.completedAt !== null)
|
|
68
|
+
return null;
|
|
69
|
+
if (typeof obj.role !== 'string')
|
|
70
|
+
return null;
|
|
71
|
+
return obj;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export type ResumeKind = 'fresh' | 'complete' | 'resume' | 'in-flight';
|
|
2
|
+
/**
|
|
3
|
+
* Step at which the slice should resume. The values are stable strings
|
|
4
|
+
* (not raw step numbers) so log output is greppable and a future
|
|
5
|
+
* workflow reshuffle can keep the names but remap the steps.
|
|
6
|
+
*/
|
|
7
|
+
export type ResumePoint = 'rd-planning' | 'rd-review-fanout' | 'rd-perf-baseline' | 'qa-test-cases' | 'qa-execution' | 'qa-validation' | 'txt-handoff';
|
|
8
|
+
/**
|
|
9
|
+
* Mid-implementation RD/QA states. The skill body treats these as
|
|
10
|
+
* "ask the user to confirm the in-flight gate" — there is no safe
|
|
11
|
+
* auto-resume for a slice the LLM is currently driving.
|
|
12
|
+
*/
|
|
13
|
+
export type InFlightState = 'spec-locked' | 'implemented' | 'running' | 'blocked';
|
|
14
|
+
export type ResumeClassification = {
|
|
15
|
+
kind: ResumeKind;
|
|
16
|
+
/** Set when `kind === 'resume'`; the step that should be re-entered. */
|
|
17
|
+
point: ResumePoint | null;
|
|
18
|
+
/** Set when `kind === 'in-flight'`; the current non-terminal state. */
|
|
19
|
+
state: InFlightState | null;
|
|
20
|
+
/**
|
|
21
|
+
* Files that the SKILL.md spec says should already exist for the
|
|
22
|
+
* current gate but were not found. Informational — the LLM uses this
|
|
23
|
+
* to decide what to produce next. Includes legacy-missing files when
|
|
24
|
+
* the legacy path was read.
|
|
25
|
+
*/
|
|
26
|
+
missingArtifacts: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Inconsistencies between state and file presence (e.g. RD is
|
|
29
|
+
* `state: qa-handoff` but `rd/code-review.md` is absent). The
|
|
30
|
+
* classifier still emits a `resume:` verdict, but the warning is the
|
|
31
|
+
* audit trail.
|
|
32
|
+
*/
|
|
33
|
+
warnings: string[];
|
|
34
|
+
/** Number of RD/QA requests filtered as `user-requested-abandon`. */
|
|
35
|
+
abandonedRequestCount: number;
|
|
36
|
+
/**
|
|
37
|
+
* True when the canonical `.peaks/_runtime/<sid>/` path was absent
|
|
38
|
+
* and the classifier fell back to `.peaks/<sid>/`. Lets the SKILL.md
|
|
39
|
+
* surface a one-time migration reminder to the user.
|
|
40
|
+
*/
|
|
41
|
+
usedLegacyPath: boolean;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Classify a session's resume state. Pure function — does not write
|
|
45
|
+
* files, does not call any peaks CLI.
|
|
46
|
+
*
|
|
47
|
+
* @param sid The session id (e.g. `2026-06-06-session-22f08c`).
|
|
48
|
+
* @param peaksRoot The canonical peaks runtime root, i.e. the
|
|
49
|
+
* directory containing `<sid>/` subdirs. For the
|
|
50
|
+
* v1.3.2 layout this is `<repo>/.peaks/_runtime`.
|
|
51
|
+
* The legacy `<repo>/.peaks` layout is also accepted
|
|
52
|
+
* for one minor release (see `usedLegacyPath`).
|
|
53
|
+
*/
|
|
54
|
+
export declare function classifyResume(sid: string, peaksRoot: string): ResumeClassification;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Peaks-Cli Solo Step 0.7 — Resume-mode detector.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the classification table in
|
|
5
|
+
* `skills/peaks-solo/SKILL.md` "Peaks-Cli Step 0.7: Detect unfinished work
|
|
6
|
+
* and offer resume". The function is a pure read of session artifacts;
|
|
7
|
+
* it performs no side effects and is safe to call from hooks, scripts,
|
|
8
|
+
* and skills.
|
|
9
|
+
*
|
|
10
|
+
* Two classification sources are merged:
|
|
11
|
+
* 1. State-based (PRD / RD / QA request artifact `state:` field) →
|
|
12
|
+
* determines the *deepest completed gate*.
|
|
13
|
+
* 2. File-presence ("Other resume triggers" table) → if a required
|
|
14
|
+
* artifact is missing for a gate that state says is complete, the
|
|
15
|
+
* classifier emits a `resume:<earlier-point>` verdict AND a
|
|
16
|
+
* `warnings[]` entry flagging the inconsistency.
|
|
17
|
+
*
|
|
18
|
+
* Primary vs. abandoned filter: when multiple RD/QA request artifacts
|
|
19
|
+
* exist for the same session (the 8-slice governance pass leaves
|
|
20
|
+
* `deferred`/`abandoned` artifacts alongside the active one), the
|
|
21
|
+
* classifier filters out files whose `state: blocked` field is paired
|
|
22
|
+
* with a `user-requested-abandon` transition note. The remaining
|
|
23
|
+
* artifacts are sorted by filename (alphabetical) and the first is the
|
|
24
|
+
* primary.
|
|
25
|
+
*
|
|
26
|
+
* Legacy path fallback: prefers the canonical
|
|
27
|
+
* `.peaks/_runtime/<sid>/` layout introduced in slice
|
|
28
|
+
* `2026-06-05-peaks-runtime-layer`; falls back to the pre-migration
|
|
29
|
+
* `.peaks/<sid>/` for one minor release so older trees do not show as
|
|
30
|
+
* false "fresh". The `usedLegacyPath` field reports which path was
|
|
31
|
+
* read.
|
|
32
|
+
*/
|
|
33
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
34
|
+
import { join } from 'node:path';
|
|
35
|
+
const MID_IMPL_RD_STATES = new Set([
|
|
36
|
+
'spec-locked',
|
|
37
|
+
'implemented',
|
|
38
|
+
'running',
|
|
39
|
+
'blocked'
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Classify a session's resume state. Pure function — does not write
|
|
43
|
+
* files, does not call any peaks CLI.
|
|
44
|
+
*
|
|
45
|
+
* @param sid The session id (e.g. `2026-06-06-session-22f08c`).
|
|
46
|
+
* @param peaksRoot The canonical peaks runtime root, i.e. the
|
|
47
|
+
* directory containing `<sid>/` subdirs. For the
|
|
48
|
+
* v1.3.2 layout this is `<repo>/.peaks/_runtime`.
|
|
49
|
+
* The legacy `<repo>/.peaks` layout is also accepted
|
|
50
|
+
* for one minor release (see `usedLegacyPath`).
|
|
51
|
+
*/
|
|
52
|
+
export function classifyResume(sid, peaksRoot) {
|
|
53
|
+
const resolved = resolveSessionDir(sid, peaksRoot);
|
|
54
|
+
if (resolved === null) {
|
|
55
|
+
return {
|
|
56
|
+
kind: 'fresh',
|
|
57
|
+
point: null,
|
|
58
|
+
state: null,
|
|
59
|
+
missingArtifacts: [],
|
|
60
|
+
warnings: [],
|
|
61
|
+
abandonedRequestCount: 0,
|
|
62
|
+
usedLegacyPath: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const { sessionDir, usedLegacyPath } = resolved;
|
|
66
|
+
const prdStates = readRequestStates(sessionDir, 'prd');
|
|
67
|
+
const rdStatesRaw = readRequestStates(sessionDir, 'rd');
|
|
68
|
+
const qaStatesRaw = readRequestStates(sessionDir, 'qa');
|
|
69
|
+
// Filter abandoned (state=blocked + user-requested-abandon note).
|
|
70
|
+
const abandonedRd = rdStatesRaw.filter((s) => s.abandoned);
|
|
71
|
+
const abandonedQa = qaStatesRaw.filter((s) => s.abandoned);
|
|
72
|
+
const abandonedCount = abandonedRd.length + abandonedQa.length;
|
|
73
|
+
// Primary selection filter: when there are MULTIPLE RD/QA requests,
|
|
74
|
+
// the abandoned ones are excluded from the candidate set so the
|
|
75
|
+
// classifier surfaces the active slice, not the audit-only trail.
|
|
76
|
+
// When there is only ONE request, the abandoned flag is informational
|
|
77
|
+
// only — a single blocked RD with an abandoned note is still the
|
|
78
|
+
// primary, because the user might want to unblock and continue.
|
|
79
|
+
const rdStates = rdStatesRaw.length > 1 ? rdStatesRaw.filter((s) => !s.abandoned) : rdStatesRaw;
|
|
80
|
+
const qaStates = qaStatesRaw.length > 1 ? qaStatesRaw.filter((s) => !s.abandoned) : qaStatesRaw;
|
|
81
|
+
const primaryPrd = pickPrimary(prdStates);
|
|
82
|
+
const primaryRd = pickPrimary(rdStates);
|
|
83
|
+
const primaryQa = pickPrimary(qaStates);
|
|
84
|
+
// Phase 1: TXT handoff present → workflow complete. Always wins.
|
|
85
|
+
if (existsSync(join(sessionDir, 'txt', 'handoff.md'))) {
|
|
86
|
+
return {
|
|
87
|
+
kind: 'complete',
|
|
88
|
+
point: null,
|
|
89
|
+
state: null,
|
|
90
|
+
missingArtifacts: [],
|
|
91
|
+
warnings: [],
|
|
92
|
+
abandonedRequestCount: abandonedCount,
|
|
93
|
+
usedLegacyPath
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// Phase 2: every RD/QA request is abandoned (filtered out) AND
|
|
97
|
+
// there were multiple to begin with. The slice is effectively
|
|
98
|
+
// dead — return fresh so the user can start over without
|
|
99
|
+
// re-attaching to the abandoned work.
|
|
100
|
+
if (primaryRd === null &&
|
|
101
|
+
primaryQa === null &&
|
|
102
|
+
abandonedCount > 0 &&
|
|
103
|
+
(rdStatesRaw.length > 1 || qaStatesRaw.length > 1)) {
|
|
104
|
+
return {
|
|
105
|
+
kind: 'fresh',
|
|
106
|
+
point: null,
|
|
107
|
+
state: null,
|
|
108
|
+
missingArtifacts: [],
|
|
109
|
+
warnings: [],
|
|
110
|
+
abandonedRequestCount: abandonedCount,
|
|
111
|
+
usedLegacyPath
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// Phase 3: mid-implementation RD states (spec-locked / implemented /
|
|
115
|
+
// running / blocked). Wins over the PRD-handed-off branch because
|
|
116
|
+
// the slice IS in flight — the LLM should not be told to "re-run
|
|
117
|
+
// the swarm" when an RD artifact is already mid-edit.
|
|
118
|
+
if (primaryRd !== null && MID_IMPL_RD_STATES.has(primaryRd.state)) {
|
|
119
|
+
return {
|
|
120
|
+
kind: 'in-flight',
|
|
121
|
+
point: null,
|
|
122
|
+
state: primaryRd.state,
|
|
123
|
+
missingArtifacts: [],
|
|
124
|
+
warnings: [],
|
|
125
|
+
abandonedRequestCount: abandonedCount,
|
|
126
|
+
usedLegacyPath
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Phase 4: terminal gates (QA verdict-issued / RD qa-handoff / PRD
|
|
130
|
+
// handed-off, with file-presence overrides).
|
|
131
|
+
const terminal = classifyTerminalGates(sessionDir, {
|
|
132
|
+
primaryPrd,
|
|
133
|
+
primaryRd,
|
|
134
|
+
primaryQa,
|
|
135
|
+
usedLegacyPath,
|
|
136
|
+
abandonedCount
|
|
137
|
+
});
|
|
138
|
+
if (terminal !== null)
|
|
139
|
+
return terminal;
|
|
140
|
+
// Phase 5: PRD exists with a non-handed-off state — treat as
|
|
141
|
+
// in-flight:spec-locked placeholder. The user can confirm whether
|
|
142
|
+
// they want to advance the PRD or start fresh.
|
|
143
|
+
if (primaryPrd !== null && primaryPrd.state.length > 0) {
|
|
144
|
+
return {
|
|
145
|
+
kind: 'in-flight',
|
|
146
|
+
point: null,
|
|
147
|
+
state: 'spec-locked',
|
|
148
|
+
missingArtifacts: [],
|
|
149
|
+
warnings: [],
|
|
150
|
+
abandonedRequestCount: abandonedCount,
|
|
151
|
+
usedLegacyPath
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
kind: 'fresh',
|
|
156
|
+
point: null,
|
|
157
|
+
state: null,
|
|
158
|
+
missingArtifacts: [],
|
|
159
|
+
warnings: [],
|
|
160
|
+
abandonedRequestCount: abandonedCount,
|
|
161
|
+
usedLegacyPath
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function classifyTerminalGates(sessionDir, ctx) {
|
|
165
|
+
// QA verdict-issued → deepest gate is D. If the test-report is
|
|
166
|
+
// missing the state is inconsistent; fall back to qa-execution.
|
|
167
|
+
if (ctx.primaryQa !== null && ctx.primaryQa.state === 'verdict-issued') {
|
|
168
|
+
const reportPath = join(sessionDir, 'qa', 'test-reports', ctx.primaryQa.filename);
|
|
169
|
+
if (!existsSync(reportPath)) {
|
|
170
|
+
return {
|
|
171
|
+
kind: 'resume',
|
|
172
|
+
point: 'qa-execution',
|
|
173
|
+
state: null,
|
|
174
|
+
missingArtifacts: [`qa/test-reports/${ctx.primaryQa.filename}`],
|
|
175
|
+
warnings: [
|
|
176
|
+
'inconsistent: qa verdict-issued but no qa/test-reports/<rid>.md; CLI gate should have blocked the transition'
|
|
177
|
+
],
|
|
178
|
+
abandonedRequestCount: ctx.abandonedCount,
|
|
179
|
+
usedLegacyPath: ctx.usedLegacyPath
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
kind: 'resume',
|
|
184
|
+
point: 'txt-handoff',
|
|
185
|
+
state: null,
|
|
186
|
+
missingArtifacts: ['txt/handoff.md'],
|
|
187
|
+
warnings: [],
|
|
188
|
+
abandonedRequestCount: ctx.abandonedCount,
|
|
189
|
+
usedLegacyPath: ctx.usedLegacyPath
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// RD qa-handoff → deepest gate is C. If the review artifacts are
|
|
193
|
+
// missing the state is inconsistent; fall back to rd-review-fanout.
|
|
194
|
+
if (ctx.primaryRd !== null && ctx.primaryRd.state === 'qa-handoff') {
|
|
195
|
+
const codeReviewPath = join(sessionDir, 'rd', 'code-review.md');
|
|
196
|
+
const securityReviewPath = join(sessionDir, 'rd', 'security-review.md');
|
|
197
|
+
const missing = [];
|
|
198
|
+
if (!existsSync(codeReviewPath))
|
|
199
|
+
missing.push('rd/code-review.md');
|
|
200
|
+
if (!existsSync(securityReviewPath))
|
|
201
|
+
missing.push('rd/security-review.md');
|
|
202
|
+
if (missing.length > 0) {
|
|
203
|
+
return {
|
|
204
|
+
kind: 'resume',
|
|
205
|
+
point: 'rd-review-fanout',
|
|
206
|
+
state: null,
|
|
207
|
+
missingArtifacts: missing,
|
|
208
|
+
warnings: [
|
|
209
|
+
'inconsistent: rd qa-handoff but review artifacts missing; CLI gate should have blocked the transition'
|
|
210
|
+
],
|
|
211
|
+
abandonedRequestCount: ctx.abandonedCount,
|
|
212
|
+
usedLegacyPath: ctx.usedLegacyPath
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
kind: 'resume',
|
|
217
|
+
point: 'qa-validation',
|
|
218
|
+
state: null,
|
|
219
|
+
missingArtifacts: ctx.primaryQa === null
|
|
220
|
+
? [`qa/test-cases/${ctx.primaryRd.filename}`]
|
|
221
|
+
: [],
|
|
222
|
+
warnings: [],
|
|
223
|
+
abandonedRequestCount: ctx.abandonedCount,
|
|
224
|
+
usedLegacyPath: ctx.usedLegacyPath
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// PRD handed-off → deepest gate is B. Walk "Other resume triggers"
|
|
228
|
+
// in priority order: tech-doc > qa/test-cases > in-flight (swarm
|
|
229
|
+
// converged, RD impl not yet started).
|
|
230
|
+
if (ctx.primaryPrd !== null && ctx.primaryPrd.state === 'handed-off') {
|
|
231
|
+
const missing = [];
|
|
232
|
+
if (!existsSync(join(sessionDir, 'rd', 'tech-doc.md'))) {
|
|
233
|
+
missing.push('rd/tech-doc.md');
|
|
234
|
+
}
|
|
235
|
+
// QA test-cases path: use the RD rid if present, else fall back
|
|
236
|
+
// to the PRD rid. The rid is shared across roles.
|
|
237
|
+
const qaCasesRid = ctx.primaryRd !== null
|
|
238
|
+
? ctx.primaryRd.filename
|
|
239
|
+
: ctx.primaryPrd !== null
|
|
240
|
+
? ctx.primaryPrd.filename
|
|
241
|
+
: null;
|
|
242
|
+
if (qaCasesRid !== null) {
|
|
243
|
+
const qaCasesPath = join(sessionDir, 'qa', 'test-cases', qaCasesRid);
|
|
244
|
+
if (!existsSync(qaCasesPath)) {
|
|
245
|
+
missing.push(`qa/test-cases/${qaCasesRid}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (missing.includes('rd/tech-doc.md')) {
|
|
249
|
+
return {
|
|
250
|
+
kind: 'resume',
|
|
251
|
+
point: 'rd-planning',
|
|
252
|
+
state: null,
|
|
253
|
+
missingArtifacts: missing,
|
|
254
|
+
warnings: [],
|
|
255
|
+
abandonedRequestCount: ctx.abandonedCount,
|
|
256
|
+
usedLegacyPath: ctx.usedLegacyPath
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (missing.some((m) => m.startsWith('qa/test-cases/'))) {
|
|
260
|
+
return {
|
|
261
|
+
kind: 'resume',
|
|
262
|
+
point: 'qa-test-cases',
|
|
263
|
+
state: null,
|
|
264
|
+
missingArtifacts: missing,
|
|
265
|
+
warnings: [],
|
|
266
|
+
abandonedRequestCount: ctx.abandonedCount,
|
|
267
|
+
usedLegacyPath: ctx.usedLegacyPath
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// All post-PRD artifacts present. Either the RD is mid-impl
|
|
271
|
+
// (handled upstream in Phase 3) or the swarm converged but the
|
|
272
|
+
// implementation has not yet started. Report the latter as
|
|
273
|
+
// in-flight:spec-locked so the user can confirm.
|
|
274
|
+
return {
|
|
275
|
+
kind: 'in-flight',
|
|
276
|
+
point: null,
|
|
277
|
+
state: 'spec-locked',
|
|
278
|
+
missingArtifacts: [],
|
|
279
|
+
warnings: [],
|
|
280
|
+
abandonedRequestCount: ctx.abandonedCount,
|
|
281
|
+
usedLegacyPath: ctx.usedLegacyPath
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Resolve the session directory. Prefers the canonical
|
|
288
|
+
* `.peaks/_runtime/<sid>/`; falls back to the legacy
|
|
289
|
+
* `.peaks/<sid>/` (one level up from the runtime root) for one
|
|
290
|
+
* minor release. Returns `null` when neither path exists.
|
|
291
|
+
*/
|
|
292
|
+
function resolveSessionDir(sid, peaksRoot) {
|
|
293
|
+
const canonical = join(peaksRoot, sid);
|
|
294
|
+
if (existsSync(canonical)) {
|
|
295
|
+
return { sessionDir: canonical, usedLegacyPath: false };
|
|
296
|
+
}
|
|
297
|
+
const legacy = join(peaksRoot, '..', sid);
|
|
298
|
+
if (existsSync(legacy)) {
|
|
299
|
+
return { sessionDir: legacy, usedLegacyPath: true };
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
function readRequestStates(sessionDir, role) {
|
|
304
|
+
const dir = join(sessionDir, role, 'requests');
|
|
305
|
+
if (!existsSync(dir))
|
|
306
|
+
return [];
|
|
307
|
+
return readdirSync(dir)
|
|
308
|
+
.filter((f) => typeof f === 'string' && f.endsWith('.md'))
|
|
309
|
+
.sort()
|
|
310
|
+
.map((filename) => {
|
|
311
|
+
const full = join(dir, filename);
|
|
312
|
+
const content = readFileSync(full, 'utf8');
|
|
313
|
+
return {
|
|
314
|
+
filename,
|
|
315
|
+
state: extractState(content),
|
|
316
|
+
abandoned: hasAbandonedTransitionNote(content)
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function extractState(content) {
|
|
321
|
+
const match = /^-\s*state:\s*(\S+)|^state:\s*(\S+)/m.exec(content);
|
|
322
|
+
if (match === null)
|
|
323
|
+
return '';
|
|
324
|
+
const captured = match[1] ?? match[2] ?? '';
|
|
325
|
+
return captured.trim();
|
|
326
|
+
}
|
|
327
|
+
function hasAbandonedTransitionNote(content) {
|
|
328
|
+
return /user-requested-abandon/.test(content);
|
|
329
|
+
}
|
|
330
|
+
function pickPrimary(states) {
|
|
331
|
+
if (states.length === 0)
|
|
332
|
+
return null;
|
|
333
|
+
return states[0] ?? null;
|
|
334
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G6 — skill-level heartbeat scheduler config.
|
|
3
|
+
*
|
|
4
|
+
* Slice 2026-06-07-sub-agent-dispatch-decouple (G6): the SKILL.md front
|
|
5
|
+
* matter for a Dispatcher (peaks-solo / peaks-rd / peaks-qa) can opt
|
|
6
|
+
* into a non-default heartbeat interval by including a line like:
|
|
7
|
+
*
|
|
8
|
+
* heartbeatIntervalSec: 15
|
|
9
|
+
*
|
|
10
|
+
* The default is 30 s (RL-13 empirical sweet spot). The poller
|
|
11
|
+
* cadence is fixed at 10 s (sub-agent 30 s / poller 10 s is the
|
|
12
|
+
* jitter-resistant offset).
|
|
13
|
+
*
|
|
14
|
+
* This module is a pure-key parser — it takes a SKILL.md body and
|
|
15
|
+
* returns the effective config. The Dispatcher's prompt template
|
|
16
|
+
* for sub-agents is then responsible for inlining the chosen value
|
|
17
|
+
* into the sub-agent prompt so that the LLM knows how often to
|
|
18
|
+
* call `peaks sub-agent heartbeat`.
|
|
19
|
+
*
|
|
20
|
+
* Note: the heartbeat *cadence* the LLM uses is enforced socially
|
|
21
|
+
* (via the prompt), not via any hook. R-1 / R-8 boundary — LLM
|
|
22
|
+
* behaviour is not observable. The user has been explicit about
|
|
23
|
+
* this: "心跳是 sub-agent 主动写, peaks CLI 不观测 LLM 行为".
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEFAULT_HEARTBEAT_INTERVAL_SEC = 30;
|
|
26
|
+
export declare const MIN_HEARTBEAT_INTERVAL_SEC = 5;
|
|
27
|
+
export declare const MAX_HEARTBEAT_INTERVAL_SEC = 600;
|
|
28
|
+
export type SkillHeartbeatConfig = {
|
|
29
|
+
readonly intervalSec: number;
|
|
30
|
+
/** Source of the chosen value (useful for debugging). */
|
|
31
|
+
readonly source: 'default' | 'skill-frontmatter';
|
|
32
|
+
};
|
|
33
|
+
/** Parse a SKILL.md body for a `heartbeatIntervalSec: <N>` line. */
|
|
34
|
+
export declare function parseHeartbeatConfig(skillBody: string): SkillHeartbeatConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Build the heartbeat-instruction paragraph to inline in a sub-agent
|
|
37
|
+
* prompt. The LLM reads this and adjusts its `peaks sub-agent
|
|
38
|
+
* heartbeat` cadence accordingly.
|
|
39
|
+
*/
|
|
40
|
+
export declare function heartbeatInstructionParagraph(config: SkillHeartbeatConfig): string;
|