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.
Files changed (115) hide show
  1. package/README.md +6 -2
  2. package/dist/src/cli/commands/core-artifact-commands.js +6 -3
  3. package/dist/src/cli/commands/gate-commands.js +28 -19
  4. package/dist/src/cli/commands/hook-handle.d.ts +17 -0
  5. package/dist/src/cli/commands/hook-handle.js +111 -0
  6. package/dist/src/cli/commands/hooks-commands.js +72 -21
  7. package/dist/src/cli/commands/progress-commands.js +9 -2
  8. package/dist/src/cli/commands/progress-start-spawn.js +30 -4
  9. package/dist/src/cli/commands/project-commands.js +8 -4
  10. package/dist/src/cli/commands/statusline-commands.js +75 -17
  11. package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
  12. package/dist/src/cli/commands/sub-agent-commands.js +488 -0
  13. package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
  14. package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
  15. package/dist/src/cli/commands/workflow-commands.js +2 -1
  16. package/dist/src/cli/commands/workspace-commands.js +3 -0
  17. package/dist/src/cli/program.js +9 -0
  18. package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
  19. package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
  20. package/dist/src/services/config/config-types.d.ts +1 -1
  21. package/dist/src/services/context/artifact-meta.d.ts +72 -0
  22. package/dist/src/services/context/artifact-meta.js +105 -0
  23. package/dist/src/services/context/context-guard.d.ts +49 -0
  24. package/dist/src/services/context/context-guard.js +91 -0
  25. package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
  26. package/dist/src/services/context/dispatch-context-guard.js +192 -0
  27. package/dist/src/services/context/headroom-client.d.ts +34 -0
  28. package/dist/src/services/context/headroom-client.js +117 -0
  29. package/dist/src/services/context/shared-channel.d.ts +92 -0
  30. package/dist/src/services/context/shared-channel.js +285 -0
  31. package/dist/src/services/context/threshold.d.ts +35 -0
  32. package/dist/src/services/context/threshold.js +76 -0
  33. package/dist/src/services/dashboard/project-dashboard-service.d.ts +23 -0
  34. package/dist/src/services/dashboard/project-dashboard-service.js +21 -0
  35. package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
  36. package/dist/src/services/dispatch/batch-counter.js +85 -0
  37. package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
  38. package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
  39. package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
  40. package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
  41. package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
  42. package/dist/src/services/dispatch/leak-detector.js +72 -0
  43. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
  44. package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
  45. package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
  46. package/dist/src/services/ide/adapters/claude-code-adapter.js +80 -0
  47. package/dist/src/services/ide/adapters/trae-adapter.d.ts +42 -0
  48. package/dist/src/services/ide/adapters/trae-adapter.js +98 -0
  49. package/dist/src/services/ide/hook-protocol.d.ts +47 -0
  50. package/dist/src/services/ide/hook-protocol.js +74 -0
  51. package/dist/src/services/ide/hook-translator.d.ts +72 -0
  52. package/dist/src/services/ide/hook-translator.js +128 -0
  53. package/dist/src/services/ide/ide-detector.d.ts +10 -0
  54. package/dist/src/services/ide/ide-detector.js +19 -0
  55. package/dist/src/services/ide/ide-registry.d.ts +14 -0
  56. package/dist/src/services/ide/ide-registry.js +45 -0
  57. package/dist/src/services/ide/ide-types.d.ts +180 -0
  58. package/dist/src/services/ide/ide-types.js +2 -0
  59. package/dist/src/services/ide/resource-profile.d.ts +52 -0
  60. package/dist/src/services/ide/resource-profile.js +33 -0
  61. package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
  62. package/dist/src/services/ide/shared/atomic-json.js +58 -0
  63. package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
  64. package/dist/src/services/ide/shared/safe-path.js +29 -0
  65. package/dist/src/services/memory/project-context-service.js +2 -1
  66. package/dist/src/services/memory/project-memory-service.js +4 -3
  67. package/dist/src/services/perf/perf-baseline-service.js +2 -1
  68. package/dist/src/services/progress/progress-service.d.ts +1 -1
  69. package/dist/src/services/progress/progress-service.js +18 -14
  70. package/dist/src/services/security/safe-settings-path.d.ts +12 -0
  71. package/dist/src/services/security/safe-settings-path.js +104 -0
  72. package/dist/src/services/session/getSessionDir.d.ts +1 -0
  73. package/dist/src/services/session/getSessionDir.js +27 -0
  74. package/dist/src/services/session/index.d.ts +1 -0
  75. package/dist/src/services/session/index.js +1 -0
  76. package/dist/src/services/signal/cancel-handler.d.ts +14 -0
  77. package/dist/src/services/signal/cancel-handler.js +76 -0
  78. package/dist/src/services/skill/resume-detector.d.ts +54 -0
  79. package/dist/src/services/skill/resume-detector.js +334 -0
  80. package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
  81. package/dist/src/services/skill/skill-scheduler.js +53 -0
  82. package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
  83. package/dist/src/services/skills/hooks-settings-service.js +190 -144
  84. package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
  85. package/dist/src/services/skills/statusline-settings-service.js +31 -34
  86. package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
  87. package/dist/src/services/slice/slice-archive-service.js +111 -0
  88. package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
  89. package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
  90. package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
  91. package/dist/src/services/solo/status-line-renderer.js +55 -0
  92. package/dist/src/services/standards/ide-aware-standards-service.d.ts +94 -0
  93. package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
  94. package/dist/src/services/standards/project-standards-service.d.ts +1 -2
  95. package/dist/src/services/workspace/reconcile-service.d.ts +36 -0
  96. package/dist/src/services/workspace/reconcile-service.js +107 -6
  97. package/dist/src/services/workspace/reconcile-types.d.ts +12 -0
  98. package/dist/src/shared/version.d.ts +1 -1
  99. package/dist/src/shared/version.js +1 -1
  100. package/package.json +2 -1
  101. package/scripts/install-skills.mjs +112 -2
  102. package/skills/peaks-ide/SKILL.md +159 -0
  103. package/skills/peaks-ide/references/audit-log-helper.md +52 -0
  104. package/skills/peaks-qa/SKILL.md +153 -55
  105. package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
  106. package/skills/peaks-rd/SKILL.md +134 -62
  107. package/skills/peaks-solo/SKILL.md +124 -37
  108. package/skills/peaks-solo/references/browser-workflow.md +22 -20
  109. package/skills/peaks-solo/references/context-governance.md +144 -0
  110. package/skills/peaks-solo/references/headroom-integration.md +107 -0
  111. package/skills/peaks-solo/references/runbook.md +3 -3
  112. package/skills/peaks-solo/references/sub-agent-dispatch.md +261 -0
  113. package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
  114. package/skills/peaks-txt/SKILL.md +17 -0
  115. package/skills/peaks-ui/SKILL.md +45 -10
@@ -1,11 +1,15 @@
1
- import { closeSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
1
+ import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { randomUUID } from 'node:crypto';
3
- import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
+ import { assertSafeSettingsFile, isInsidePath } from '../ide/shared/safe-path.js';
6
+ import { getAdapter } from '../ide/ide-registry.js';
5
7
  export const STATUSLINE_COMMAND = 'peaks statusline';
6
- function isInsidePath(childPath, parentPath) {
7
- const rel = relative(parentPath, childPath);
8
- return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
8
+ // Re-export the shared helper so existing consumers that imported
9
+ // `isInsidePath` from this module keep compiling.
10
+ export { isInsidePath };
11
+ function resolveIde(options) {
12
+ return options?.ide ?? 'claude-code';
9
13
  }
10
14
  function resolveSettingsRoot(scope, projectRoot) {
11
15
  if (scope === 'global')
@@ -15,25 +19,17 @@ function resolveSettingsRoot(scope, projectRoot) {
15
19
  }
16
20
  return resolve(projectRoot);
17
21
  }
18
- function resolveSettingsPath(scope, projectRoot) {
22
+ /**
23
+ * Resolve + safety-check the settings path for the given IDE and scope. The
24
+ * `dirName` and `settingsFileName` come from the registered adapter
25
+ * (`getAdapter(ide)`) so the hardcoded `.claude/settings.json` is gone —
26
+ * future adapters swap by changing the registry, not this file.
27
+ */
28
+ function resolveAndAssertSettingsPath(scope, ide, projectRoot) {
19
29
  const root = resolveSettingsRoot(scope, projectRoot);
20
- return join(root, '.claude', 'settings.json');
21
- }
22
- /** Reject symlinked .claude dir or settings file to prevent escape. */
23
- function assertSafeSettingsPath(scope, root, settingsPath) {
24
- const claudeDir = join(root, '.claude');
25
- if (existsSync(claudeDir) && lstatSync(claudeDir).isSymbolicLink()) {
26
- throw new Error('.claude directory must not be a symlink');
27
- }
28
- if (existsSync(settingsPath)) {
29
- if (lstatSync(settingsPath).isSymbolicLink()) {
30
- throw new Error('settings.json must not be a symlink');
31
- }
32
- const realRoot = realpathSync(root);
33
- if (!isInsidePath(realpathSync(settingsPath), realRoot)) {
34
- throw new Error(`settings.json must stay inside the ${scope} root`);
35
- }
36
- }
30
+ const adapter = getAdapter(ide);
31
+ const { settingsPath } = assertSafeSettingsFile(scope, root, adapter.settings.dirName, adapter.settings.settingsFileName);
32
+ return { root, settingsPath };
37
33
  }
38
34
  function readSettings(settingsPath) {
39
35
  if (!existsSync(settingsPath))
@@ -76,10 +72,9 @@ function buildPlan(scope, settingsPath, settings, exists) {
76
72
  desiredCommand: STATUSLINE_COMMAND
77
73
  };
78
74
  }
79
- export function planStatusLineInstall(scope, projectRoot) {
80
- const root = resolveSettingsRoot(scope, projectRoot);
81
- const settingsPath = resolveSettingsPath(scope, projectRoot);
82
- assertSafeSettingsPath(scope, root, settingsPath);
75
+ export function planStatusLineInstall(scope, projectRoot, options) {
76
+ const ide = resolveIde(options);
77
+ const { settingsPath } = resolveAndAssertSettingsPath(scope, ide, projectRoot);
83
78
  const exists = existsSync(settingsPath);
84
79
  const settings = readSettings(settingsPath);
85
80
  return buildPlan(scope, settingsPath, settings, exists);
@@ -109,9 +104,8 @@ function atomicWriteJson(settingsPath, settings) {
109
104
  }
110
105
  }
111
106
  export function applyStatusLineInstall(scope, projectRoot, options = {}) {
112
- const root = resolveSettingsRoot(scope, projectRoot);
113
- const settingsPath = resolveSettingsPath(scope, projectRoot);
114
- assertSafeSettingsPath(scope, root, settingsPath);
107
+ const ide = resolveIde(options);
108
+ const { settingsPath } = resolveAndAssertSettingsPath(scope, ide, projectRoot);
115
109
  const exists = existsSync(settingsPath);
116
110
  const settings = readSettings(settingsPath);
117
111
  const plan = buildPlan(scope, settingsPath, settings, exists);
@@ -126,10 +120,9 @@ export function applyStatusLineInstall(scope, projectRoot, options = {}) {
126
120
  atomicWriteJson(settingsPath, nextSettings);
127
121
  return { ...plan, applied: true };
128
122
  }
129
- export function removeStatusLineInstall(scope, projectRoot) {
130
- const root = resolveSettingsRoot(scope, projectRoot);
131
- const settingsPath = resolveSettingsPath(scope, projectRoot);
132
- assertSafeSettingsPath(scope, root, settingsPath);
123
+ export function removeStatusLineInstall(scope, projectRoot, options) {
124
+ const ide = resolveIde(options);
125
+ const { settingsPath } = resolveAndAssertSettingsPath(scope, ide, projectRoot);
133
126
  if (!existsSync(settingsPath)) {
134
127
  return { scope, settingsPath, removed: false };
135
128
  }
@@ -142,3 +135,7 @@ export function removeStatusLineInstall(scope, projectRoot) {
142
135
  atomicWriteJson(settingsPath, rest);
143
136
  return { scope, settingsPath, removed: true };
144
137
  }
138
+ // Suppress unused-import warning for `isAbsolute` if it becomes unused in
139
+ // future refactors. The pre-refactor file used it in the local isInsidePath;
140
+ // the shared helper owns that logic now.
141
+ void isAbsolute;
@@ -0,0 +1,20 @@
1
+ export declare const ARCHIVE_RETENTION_MS: number;
2
+ export interface ArchiveResult {
3
+ readonly archivedCompleted: number;
4
+ readonly archivedInFlight: number;
5
+ readonly gcDeleted: number;
6
+ }
7
+ /** Build the canonical archive dir for a given session + slice id. */
8
+ export declare function archiveDir(projectRoot: string, sessionId: string, sliceId: string): string;
9
+ /** Build the in-flight subdir (records not yet disposed). */
10
+ export declare function inFlightArchiveDir(projectRoot: string, sessionId: string, sliceId: string): string;
11
+ /**
12
+ * Archive the current `.peaks/_sub_agents/<sid>/` tree under
13
+ * `<archiveDir>` (per the sliceId), separating completed vs in-flight.
14
+ * Then run the 30-day GC over the archive dir.
15
+ */
16
+ export declare function archiveSubAgentRecords(projectRoot: string, options: {
17
+ sessionId: string;
18
+ sliceId: string;
19
+ now?: () => Date;
20
+ }): ArchiveResult;
@@ -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
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * IDE-aware wrapper for `peaks standards init` / `peaks standards update`.
3
+ *
4
+ * Slice #011-2026-06-07-ide-adapter-resource-profile: the original
5
+ * `executeProjectStandardsInit` / `executeProjectStandardsUpdate` always
6
+ * wrote to `CLAUDE.md` + `.claude/rules/**` regardless of which IDE
7
+ * the user was running. This wrapper dispatches on the IDE detected
8
+ * (or explicitly requested via `--ide`) and falls back to the legacy
9
+ * Claude Code path with a stderr warning when the detected IDE has
10
+ * no `standardsProfile` declared (Trae in slice 1.3.2).
11
+ *
12
+ * Two entry points:
13
+ *
14
+ * - `executeProjectStandardsInitIdeAware` — same signature as the
15
+ * underlying `executeProjectStandardsInit`, plus an optional
16
+ * `ideId` override that bypasses detection.
17
+ * - `executeProjectStandardsUpdateIdeAware` — same shape, for the
18
+ * `update` flow.
19
+ *
20
+ * Detection precedence:
21
+ * 1. Explicit `options.ideId` (CLI `--ide` flag)
22
+ * 2. `IdeRegistry.detect()` from cwd (or the `projectRoot` if given)
23
+ * 3. `null` (no IDE detected) → fall back to the legacy Claude Code path
24
+ *
25
+ * Fallback behavior: when the resolved IDE has no `standardsProfile`
26
+ * declared, the wrapper STILL calls the legacy Claude Code writer
27
+ * (so the user gets the files they would have gotten before slice #011)
28
+ * and emits a stderr warning with the IDE id and the fact that the
29
+ * adapter is UNVERIFIED for the standards profile. This keeps the
30
+ * "Trae is UNVERIFIED, ship a working file tree, surface the gap"
31
+ * contract intact.
32
+ */
33
+ import type { IdeId } from '../ide/ide-types.js';
34
+ import { detectAllResourceTargets, getStandardsProfile } from '../ide/resource-profile.js';
35
+ import { type ProjectStandardsInitOptions, type ProjectStandardsInitResult, type ProjectStandardsUpdateResult } from './project-standards-service.js';
36
+ export type { ProjectStandardsInitResult, ProjectStandardsUpdateResult };
37
+ export type ProjectStandardsIdeAwareOptions = ProjectStandardsInitOptions & {
38
+ /**
39
+ * Explicit IDE override. When set, bypasses `IdeRegistry.detect()`
40
+ * (cwd + env heuristics) and uses the provided IDE id directly.
41
+ * Mirrors the `peaks hooks install --ide <id>` pattern.
42
+ */
43
+ readonly ideId?: IdeId;
44
+ };
45
+ export type ProjectStandardsUpdateIdeAwareOptions = ProjectStandardsInitOptions & {
46
+ /** Explicit IDE override. See {@link ProjectStandardsIdeAwareOptions}. */
47
+ readonly ideId?: IdeId;
48
+ };
49
+ /**
50
+ * Resolve the active IDE id for a standards call. Order of precedence:
51
+ * 1. explicit `options.ideId`
52
+ * 2. `IdeRegistry.detect()` from `options.projectRoot`
53
+ * 3. `null` (no detected IDE — caller falls back to legacy)
54
+ */
55
+ export declare function resolveStandardsIdeId(options: {
56
+ readonly projectRoot: string;
57
+ readonly ideId?: IdeId;
58
+ }): IdeId | null;
59
+ /**
60
+ * Run `peaks standards init` with IDE-aware dispatch.
61
+ *
62
+ * When the resolved IDE has a `standardsProfile`, the call still
63
+ * delegates to the existing `executeProjectStandardsInit` (the
64
+ * profile maps to the Claude Code path; future per-IDE writers
65
+ * plug in here). When the IDE is unregistered for the standards
66
+ * profile, the call delegates to the legacy path + emits a stderr
67
+ * warning.
68
+ */
69
+ export declare function executeProjectStandardsInitIdeAware(options: ProjectStandardsIdeAwareOptions): ProjectStandardsInitResult;
70
+ /**
71
+ * Run `peaks standards update` with IDE-aware dispatch.
72
+ *
73
+ * Same dispatch rules as `executeProjectStandardsInitIdeAware`.
74
+ */
75
+ export declare function executeProjectStandardsUpdateIdeAware(options: ProjectStandardsUpdateIdeAwareOptions): ProjectStandardsUpdateResult;
76
+ /**
77
+ * Test seam + integration-test helper: returns the resolved IDE id
78
+ * for the call, plus the active standards profile. Exported for
79
+ * the integration test in `tests/unit/standards/ide-aware-standards-service.test.ts`
80
+ * to assert the dispatch decision without running the full write.
81
+ */
82
+ export declare function inspectStandardsDispatch(options: {
83
+ readonly projectRoot: string;
84
+ readonly ideId?: IdeId;
85
+ }): {
86
+ readonly ideId: IdeId | null;
87
+ readonly profile: ReturnType<typeof getStandardsProfile>;
88
+ };
89
+ /**
90
+ * Test seam: the resource-profile accessor exposes
91
+ * `detectAllResourceTargets` for callers that need to enumerate
92
+ * across all registered IDEs. Re-exported here for convenience.
93
+ */
94
+ export { detectAllResourceTargets };
@@ -0,0 +1,89 @@
1
+ import { detectAllResourceTargets, getStandardsProfile, } from '../ide/resource-profile.js';
2
+ import { executeProjectStandardsInit, executeProjectStandardsUpdate, } from './project-standards-service.js';
3
+ import { detectInstalledIde } from '../ide/ide-detector.js';
4
+ function warnUnregisteredIde(ideId, projectRoot) {
5
+ process.stderr.write(`peaks standards: IDE '${ideId}' has no standardsProfile declared; ` +
6
+ `falling back to the legacy Claude Code path (CLAUDE.md + .claude/rules/**) ` +
7
+ `for project '${projectRoot}'. This is a slice #011 follow-up gap; ` +
8
+ `see .peaks/memory/ide-adapter-resource-profile-framework.md.\n`);
9
+ }
10
+ function warnNoIdeDetected(projectRoot) {
11
+ process.stderr.write(`peaks standards: no IDE detected in '${projectRoot}'; ` +
12
+ `writing to the legacy Claude Code path (CLAUDE.md + .claude/rules/**). ` +
13
+ `Pass --ide <id> to bypass detection.\n`);
14
+ }
15
+ /**
16
+ * Resolve the active IDE id for a standards call. Order of precedence:
17
+ * 1. explicit `options.ideId`
18
+ * 2. `IdeRegistry.detect()` from `options.projectRoot`
19
+ * 3. `null` (no detected IDE — caller falls back to legacy)
20
+ */
21
+ export function resolveStandardsIdeId(options) {
22
+ if (options.ideId !== undefined) {
23
+ return options.ideId;
24
+ }
25
+ return detectInstalledIde(options.projectRoot);
26
+ }
27
+ /**
28
+ * Run `peaks standards init` with IDE-aware dispatch.
29
+ *
30
+ * When the resolved IDE has a `standardsProfile`, the call still
31
+ * delegates to the existing `executeProjectStandardsInit` (the
32
+ * profile maps to the Claude Code path; future per-IDE writers
33
+ * plug in here). When the IDE is unregistered for the standards
34
+ * profile, the call delegates to the legacy path + emits a stderr
35
+ * warning.
36
+ */
37
+ export function executeProjectStandardsInitIdeAware(options) {
38
+ const ideId = resolveStandardsIdeId(options);
39
+ if (ideId === null) {
40
+ warnNoIdeDetected(options.projectRoot);
41
+ return executeProjectStandardsInit(options);
42
+ }
43
+ const profile = getStandardsProfile(ideId);
44
+ if (profile === null) {
45
+ warnUnregisteredIde(ideId, options.projectRoot);
46
+ return executeProjectStandardsInit(options);
47
+ }
48
+ // Claude Code path: profile matches the legacy writer. Future per-IDE
49
+ // writers (markdown+frontmatter, multiple rule roots, etc.) plug in
50
+ // here by branching on `profile.format` / `profile.rulesDir`.
51
+ return executeProjectStandardsInit(options);
52
+ }
53
+ /**
54
+ * Run `peaks standards update` with IDE-aware dispatch.
55
+ *
56
+ * Same dispatch rules as `executeProjectStandardsInitIdeAware`.
57
+ */
58
+ export function executeProjectStandardsUpdateIdeAware(options) {
59
+ const ideId = resolveStandardsIdeId(options);
60
+ if (ideId === null) {
61
+ warnNoIdeDetected(options.projectRoot);
62
+ return executeProjectStandardsUpdate(options);
63
+ }
64
+ const profile = getStandardsProfile(ideId);
65
+ if (profile === null) {
66
+ warnUnregisteredIde(ideId, options.projectRoot);
67
+ return executeProjectStandardsUpdate(options);
68
+ }
69
+ return executeProjectStandardsUpdate(options);
70
+ }
71
+ /**
72
+ * Test seam + integration-test helper: returns the resolved IDE id
73
+ * for the call, plus the active standards profile. Exported for
74
+ * the integration test in `tests/unit/standards/ide-aware-standards-service.test.ts`
75
+ * to assert the dispatch decision without running the full write.
76
+ */
77
+ export function inspectStandardsDispatch(options) {
78
+ const ideId = resolveStandardsIdeId(options);
79
+ if (ideId === null) {
80
+ return { ideId: null, profile: null };
81
+ }
82
+ return { ideId, profile: getStandardsProfile(ideId) };
83
+ }
84
+ /**
85
+ * Test seam: the resource-profile accessor exposes
86
+ * `detectAllResourceTargets` for callers that need to enumerate
87
+ * across all registered IDEs. Re-exported here for convenience.
88
+ */
89
+ export { detectAllResourceTargets };
@@ -68,7 +68,7 @@ export type ProjectStandardsUpdateSummary = {
68
68
  readonly reviewSuggestions: string[];
69
69
  };
70
70
  };
71
- type ProjectStandardsInitOptions = {
71
+ export type ProjectStandardsInitOptions = {
72
72
  readonly projectRoot: string;
73
73
  readonly language?: string;
74
74
  readonly apply?: boolean;
@@ -79,4 +79,3 @@ export declare function executeProjectStandardsInit(options: ProjectStandardsIni
79
79
  export declare function executeProjectStandardsUpdate(options: ProjectStandardsInitOptions): ProjectStandardsUpdateResult;
80
80
  export declare function summarizeProjectStandardsInitResult(result: ProjectStandardsInitResult): ProjectStandardsInitSummary;
81
81
  export declare function summarizeProjectStandardsUpdateResult(result: ProjectStandardsUpdateResult): ProjectStandardsUpdateSummary;
82
- export {};