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
@@ -0,0 +1,192 @@
1
+ /**
2
+ * G7.4 path safety + G8 path safety — R-2 symlink/junction guard for
3
+ * sub-agent artifact + shared channel paths.
4
+ *
5
+ * Slice #009's `assertSafeDispatchRecordPath` covers the dispatch record
6
+ * path. This module covers two adjacent paths under the same canonical
7
+ * root (`.peaks/_sub_agents/<sid>/`):
8
+ *
9
+ * - `artifacts/<rid>-<role>-<idx>.<ext>` (G7 write-artifact)
10
+ * - `shared/<batchId>.json` (G8 share / shared-read)
11
+ *
12
+ * Both reuse the same R-2 logic: reject `..` segments BEFORE the OS
13
+ * resolver collapses them (POSIX normalize silently drops `..`), reject
14
+ * absolute paths that escape the canonical root, resolve symlinks via
15
+ * `realpathSync` and re-check the resolved path.
16
+ *
17
+ * The canonical-root pattern is intentionally identical to the slice #009
18
+ * helper, so future audits can reuse one mental model: "all sub-agent
19
+ * state files must live under `.peaks/_sub_agents/<sid>/` and pass the
20
+ * same R-2 guard."
21
+ */
22
+ import { realpathSync } from 'node:fs';
23
+ import { dirname, isAbsolute, relative, resolve } from 'node:path';
24
+ const SUB_AGENTS_DIR = '_sub_agents';
25
+ const ARTIFACTS_SUBDIR = 'artifacts';
26
+ const SHARED_SUBDIR = 'shared';
27
+ const ARTIFACT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\.[A-Za-z0-9]+$/;
28
+ const BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
29
+ /** Build the canonical artifact path for a given session/rid/role/idx/ext. */
30
+ export function artifactPath(projectRoot, sid, rid, role, idx, ext = 'md') {
31
+ if (typeof role !== 'string' || role.length === 0) {
32
+ throw new Error('artifactPath: role must be non-empty');
33
+ }
34
+ if (!Number.isInteger(idx) || idx < 1) {
35
+ throw new Error(`artifactPath: idx must be positive integer (got ${idx})`);
36
+ }
37
+ const safeSid = sanitizeSegment(sid, 'sessionId');
38
+ const safeRid = sanitizeSegment(rid, 'requestId');
39
+ const safeRole = sanitizeSegment(role, 'role');
40
+ const safeExt = ext.replace(/[^A-Za-z0-9]/g, '');
41
+ if (safeExt.length === 0) {
42
+ throw new Error(`artifactPath: ext must be alphanumeric (got "${ext}")`);
43
+ }
44
+ return resolve(projectRoot, '.peaks', SUB_AGENTS_DIR, safeSid, ARTIFACTS_SUBDIR, `${safeRid}-${safeRole}-${String(idx).padStart(3, '0')}.${safeExt}`);
45
+ }
46
+ /** Build the canonical shared channel path. */
47
+ export function sharedChannelPath(projectRoot, sid, rid, batchId) {
48
+ if (typeof batchId !== 'string' || batchId.length === 0) {
49
+ throw new Error('sharedChannelPath: batchId must be non-empty');
50
+ }
51
+ const safeSid = sanitizeSegment(sid, 'sessionId');
52
+ const safeRid = sanitizeSegment(rid, 'requestId');
53
+ if (!BATCH_ID_PATTERN.test(batchId)) {
54
+ throw new Error(`sharedChannelPath: batchId must match ${BATCH_ID_PATTERN} (got "${batchId}")`);
55
+ }
56
+ return resolve(projectRoot, '.peaks', SUB_AGENTS_DIR, safeSid, SHARED_SUBDIR, `${safeRid}-${batchId}.json`);
57
+ }
58
+ /**
59
+ * Assert that `artifactPath` lives under
60
+ * `projectRoot/.peaks/_sub_agents/<sid>/artifacts/`. Rejects
61
+ * symlink/junction escapes and `..` segments.
62
+ *
63
+ * Throws an Error with `.code = 'INVALID_ARTIFACT_PATH'` on rejection.
64
+ */
65
+ export function assertSafeArtifactPath(artifactPathInput, projectRoot) {
66
+ if (!isAbsolute(artifactPathInput)) {
67
+ throw invalidPathError(artifactPathInput, 'must be absolute');
68
+ }
69
+ const rawSegments = artifactPathInput.split(/[\\/]/);
70
+ if (rawSegments.includes('..')) {
71
+ throw invalidPathError(artifactPathInput, 'must not contain .. segments');
72
+ }
73
+ const expected = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
74
+ const rel = relative(expected, artifactPathInput);
75
+ if (rel.startsWith('..') || isAbsolute(rel)) {
76
+ throw invalidPathError(artifactPathInput, 'must be under .peaks/_sub_agents/');
77
+ }
78
+ let realArtifact;
79
+ let realRoot;
80
+ try {
81
+ const parent = dirname(artifactPathInput);
82
+ const realParent = realpathSync(parent);
83
+ realArtifact = resolve(realParent, artifactPathInput.slice(parent.length + 1));
84
+ realRoot = realpathSync(projectRoot);
85
+ }
86
+ catch {
87
+ const fallback = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
88
+ const rel2 = relative(fallback, artifactPathInput);
89
+ if (rel2.startsWith('..') || isAbsolute(rel2)) {
90
+ throw invalidPathError(artifactPathInput, 'must be under .peaks/_sub_agents/');
91
+ }
92
+ return artifactPathInput;
93
+ }
94
+ const realRel = relative(realRoot, realArtifact);
95
+ if (realRel.startsWith('..') || isAbsolute(realRel)) {
96
+ throw invalidPathError(artifactPathInput, 'symlink/junction escapes project root');
97
+ }
98
+ const realExpected = resolve(realRoot, '.peaks', SUB_AGENTS_DIR);
99
+ const realRelExpected = relative(realExpected, realArtifact);
100
+ if (realRelExpected.startsWith('..') || isAbsolute(realRelExpected)) {
101
+ throw invalidPathError(artifactPathInput, 'must be under .peaks/_sub_agents/');
102
+ }
103
+ return realArtifact;
104
+ }
105
+ /**
106
+ * Assert that `channelPath` lives under
107
+ * `projectRoot/.peaks/_sub_agents/<sid>/shared/`. Same R-2 logic as
108
+ * `assertSafeArtifactPath` but with a different canonical subdir.
109
+ *
110
+ * Throws an Error with `.code = 'INVALID_SHARED_CHANNEL_PATH'` on rejection.
111
+ */
112
+ export function assertSafeSharedChannelPath(channelPathInput, projectRoot) {
113
+ if (!isAbsolute(channelPathInput)) {
114
+ throw invalidPathErrorShared(channelPathInput, 'must be absolute');
115
+ }
116
+ const rawSegments = channelPathInput.split(/[\\/]/);
117
+ if (rawSegments.includes('..')) {
118
+ throw invalidPathErrorShared(channelPathInput, 'must not contain .. segments');
119
+ }
120
+ const expected = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
121
+ const rel = relative(expected, channelPathInput);
122
+ if (rel.startsWith('..') || isAbsolute(rel)) {
123
+ throw invalidPathErrorShared(channelPathInput, 'must be under .peaks/_sub_agents/shared/');
124
+ }
125
+ let realChannel;
126
+ let realRoot;
127
+ try {
128
+ const parent = dirname(channelPathInput);
129
+ const realParent = realpathSync(parent);
130
+ realChannel = resolve(realParent, channelPathInput.slice(parent.length + 1));
131
+ realRoot = realpathSync(projectRoot);
132
+ }
133
+ catch {
134
+ const fallback = resolve(projectRoot, '.peaks', SUB_AGENTS_DIR);
135
+ const rel2 = relative(fallback, channelPathInput);
136
+ if (rel2.startsWith('..') || isAbsolute(rel2)) {
137
+ throw invalidPathErrorShared(channelPathInput, 'must be under .peaks/_sub_agents/shared/');
138
+ }
139
+ return channelPathInput;
140
+ }
141
+ const realRel = relative(realRoot, realChannel);
142
+ if (realRel.startsWith('..') || isAbsolute(realRel)) {
143
+ throw invalidPathErrorShared(channelPathInput, 'symlink/junction escapes project root');
144
+ }
145
+ const realExpected = resolve(realRoot, '.peaks', SUB_AGENTS_DIR);
146
+ const realRelExpected = relative(realExpected, realChannel);
147
+ if (realRelExpected.startsWith('..') || isAbsolute(realRelExpected)) {
148
+ throw invalidPathErrorShared(channelPathInput, 'must be under .peaks/_sub_agents/shared/');
149
+ }
150
+ return realChannel;
151
+ }
152
+ /**
153
+ * Soft-warn check on the artifact file name pattern. Returns null if
154
+ * the name matches `<rid>-<role>-<idx>.<ext>`, otherwise returns a
155
+ * warning string. Does NOT reject (the path is still in the canonical
156
+ * dir; the warning is for human/audit readability per G7.4.c).
157
+ */
158
+ export function checkArtifactNameConvention(artifactPathInput) {
159
+ const base = artifactPathInput.split(/[\\/]/).pop() ?? '';
160
+ if (ARTIFACT_NAME_PATTERN.test(base)) {
161
+ return null;
162
+ }
163
+ return `Artifact file name does not match <rid>-<role>-<idx>.<ext> convention: ${base}`;
164
+ }
165
+ function sanitizeSegment(value, field) {
166
+ if (typeof value !== 'string' || value.length === 0) {
167
+ throw new Error(`${field} must be non-empty`);
168
+ }
169
+ if (value.length > 256) {
170
+ throw new Error(`${field} must be ≤ 256 chars (got ${value.length})`);
171
+ }
172
+ for (let i = 0; i < value.length; i += 1) {
173
+ const code = value.charCodeAt(i);
174
+ if (code <= 0x20 || code === 0x7F) {
175
+ throw new Error(`${field} must not contain whitespace or control characters`);
176
+ }
177
+ }
178
+ if (value.includes('..') || value.includes('/') || value.includes('\\')) {
179
+ throw new Error(`${field} must not contain '..' or path separators`);
180
+ }
181
+ return value;
182
+ }
183
+ function invalidPathError(path, reason) {
184
+ const e = new Error(`Invalid artifact path: ${reason} (path: ${path})`);
185
+ e.code = 'INVALID_ARTIFACT_PATH';
186
+ return e;
187
+ }
188
+ function invalidPathErrorShared(path, reason) {
189
+ const e = new Error(`Invalid shared channel path: ${reason} (path: ${path})`);
190
+ e.code = 'INVALID_SHARED_CHANNEL_PATH';
191
+ return e;
192
+ }
@@ -0,0 +1,34 @@
1
+ export type HeadroomMode = 'balanced' | 'aggressive' | 'conservative';
2
+ export interface HeadroomResult {
3
+ readonly compressed: boolean;
4
+ readonly originalSize: number;
5
+ readonly compressedSize: number;
6
+ readonly compressionRatio: number;
7
+ readonly mode: HeadroomMode;
8
+ /** `'HEADROOM_UNAVAILABLE'` on fallback; `null` on success. */
9
+ readonly warning: string | null;
10
+ /** Compressed prompt body. `null` if no compression happened. */
11
+ readonly compressedPrompt: string | null;
12
+ /** Tokens saved (from the SDK). 0 on fallback. */
13
+ readonly tokensSaved: number;
14
+ }
15
+ /**
16
+ * Compress a prompt via headroom-ai. The `fallback: true` option is
17
+ * non-negotiable: if the proxy daemon is unavailable, the SDK returns
18
+ * `result.compressed = false` and the original messages; we surface
19
+ * that as `HEADROOM_UNAVAILABLE` warning + G7 metadata-only fallback.
20
+ */
21
+ export declare function compressPrompt(prompt: string, mode?: HeadroomMode): Promise<HeadroomResult>;
22
+ /**
23
+ * Bridge interface: when `--use-headroom` is set, share entries written
24
+ * via `peaks sub-agent share` MAY also flow through headroom's
25
+ * `SharedContext`. Slice #010 implements a peak-internal shared channel
26
+ * (see `shared-channel.ts`); the headroom-side `SharedContext` is a
27
+ * separate concept that future slices can layer on. For now this
28
+ * function is a stub that returns the peak-internal channel ID, which
29
+ * is enough to demonstrate the bridge contract.
30
+ */
31
+ export declare function buildSharedContextBridge(batchId: string): {
32
+ peakChannelId: string;
33
+ headroomContextId: string;
34
+ };
@@ -0,0 +1,117 @@
1
+ const DEFAULT_TIMEOUT_MS = 30_000;
2
+ const DEFAULT_MODEL = 'claude-sonnet-4-5-20250929';
3
+ // Approximate 1 token = 4 bytes for English text. This is a rough
4
+ // heuristic; the SDK does its own tokenization internally.
5
+ const BYTES_PER_TOKEN = 4;
6
+ /**
7
+ * Compress a prompt via headroom-ai. The `fallback: true` option is
8
+ * non-negotiable: if the proxy daemon is unavailable, the SDK returns
9
+ * `result.compressed = false` and the original messages; we surface
10
+ * that as `HEADROOM_UNAVAILABLE` warning + G7 metadata-only fallback.
11
+ */
12
+ export async function compressPrompt(prompt, mode = 'balanced') {
13
+ const originalSize = Buffer.byteLength(prompt, 'utf8');
14
+ let compressFn = null;
15
+ try {
16
+ const mod = await import('headroom-ai');
17
+ if (typeof mod.compress === 'function') {
18
+ compressFn = mod.compress;
19
+ }
20
+ }
21
+ catch {
22
+ return fallback(originalSize, mode);
23
+ }
24
+ if (compressFn === null) {
25
+ return fallback(originalSize, mode);
26
+ }
27
+ const messages = [
28
+ { role: 'user', content: prompt }
29
+ ];
30
+ const opts = {
31
+ model: DEFAULT_MODEL,
32
+ timeout: DEFAULT_TIMEOUT_MS,
33
+ fallback: true, // CRITICAL: return original messages if proxy is down
34
+ retries: 1
35
+ };
36
+ if (mode === 'aggressive') {
37
+ opts.tokenBudget = Math.max(1, Math.floor(originalSize * 0.20 / BYTES_PER_TOKEN));
38
+ }
39
+ else if (mode === 'conservative') {
40
+ opts.tokenBudget = Math.max(1, Math.floor(originalSize * 0.70 / BYTES_PER_TOKEN));
41
+ }
42
+ else {
43
+ // balanced: target ~60% reduction
44
+ opts.tokenBudget = Math.max(1, Math.floor(originalSize * 0.40 / BYTES_PER_TOKEN));
45
+ }
46
+ let result;
47
+ try {
48
+ result = await compressFn(messages, opts);
49
+ }
50
+ catch {
51
+ return fallback(originalSize, mode);
52
+ }
53
+ if (result.compressed === false) {
54
+ return {
55
+ compressed: false,
56
+ originalSize,
57
+ compressedSize: originalSize,
58
+ compressionRatio: 1.0,
59
+ mode,
60
+ warning: 'HEADROOM_UNAVAILABLE',
61
+ compressedPrompt: null,
62
+ tokensSaved: 0
63
+ };
64
+ }
65
+ const compressedContent = extractContent(result.messages);
66
+ if (compressedContent === null) {
67
+ return fallback(originalSize, mode);
68
+ }
69
+ const compressedSize = Buffer.byteLength(compressedContent, 'utf8');
70
+ return {
71
+ compressed: true,
72
+ originalSize,
73
+ compressedSize,
74
+ compressionRatio: compressedSize / originalSize,
75
+ mode,
76
+ warning: null,
77
+ compressedPrompt: compressedContent,
78
+ tokensSaved: result.tokensSaved ?? 0
79
+ };
80
+ }
81
+ function fallback(originalSize, mode) {
82
+ return {
83
+ compressed: false,
84
+ originalSize,
85
+ compressedSize: originalSize,
86
+ compressionRatio: 1.0,
87
+ mode,
88
+ warning: 'HEADROOM_UNAVAILABLE',
89
+ compressedPrompt: null,
90
+ tokensSaved: 0
91
+ };
92
+ }
93
+ function extractContent(messages) {
94
+ if (!Array.isArray(messages) || messages.length === 0) {
95
+ return null;
96
+ }
97
+ const last = messages[messages.length - 1];
98
+ if (typeof last.content === 'string') {
99
+ return last.content;
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Bridge interface: when `--use-headroom` is set, share entries written
105
+ * via `peaks sub-agent share` MAY also flow through headroom's
106
+ * `SharedContext`. Slice #010 implements a peak-internal shared channel
107
+ * (see `shared-channel.ts`); the headroom-side `SharedContext` is a
108
+ * separate concept that future slices can layer on. For now this
109
+ * function is a stub that returns the peak-internal channel ID, which
110
+ * is enough to demonstrate the bridge contract.
111
+ */
112
+ export function buildSharedContextBridge(batchId) {
113
+ return {
114
+ peakChannelId: batchId,
115
+ headroomContextId: `headroom-ctx-${batchId}`
116
+ };
117
+ }
@@ -0,0 +1,92 @@
1
+ export interface SharedChannelEntry {
2
+ readonly at: string;
3
+ readonly from: string;
4
+ readonly key: string;
5
+ readonly value: Readonly<Record<string, unknown>>;
6
+ readonly valueSize: number;
7
+ }
8
+ export interface SharedChannel {
9
+ readonly batchId: string;
10
+ readonly createdAt: string;
11
+ readonly updatedAt: string;
12
+ readonly entries: Readonly<Record<string, SharedChannelEntry>>;
13
+ }
14
+ export declare const SHARED_CHANNEL_MAX_VALUE_BYTES: number;
15
+ export declare const SHARED_CHANNEL_SOFT_VALUE_WARN = 1024;
16
+ export declare const SHARED_CHANNEL_MAX_FILE_BYTES: number;
17
+ export declare const SHARED_CHANNEL_TTL_DAYS = 30;
18
+ export type WriteSharedEntryResult = {
19
+ readonly ok: true;
20
+ readonly entry: SharedChannelEntry;
21
+ readonly channelSize: number;
22
+ readonly lastWriteWins: boolean;
23
+ readonly softWarning: boolean;
24
+ } | {
25
+ readonly ok: false;
26
+ readonly code: 'VALUE_TOO_LARGE' | 'INVALID_BATCH_ID' | 'WRITE_ERROR';
27
+ readonly message: string;
28
+ };
29
+ /**
30
+ * Write a shared entry to the per-batch channel file. Atomic-write
31
+ * (tmp + rename). Returns the new entry + channel size + last-write-wins
32
+ * flag (true if the key already existed; the new value overwrites).
33
+ *
34
+ * RL-25 size limit: `value ≥ 64KB` is hard-rejected with `VALUE_TOO_LARGE`.
35
+ * `value > 1KB and < 64KB` is a soft warning (returned in the result).
36
+ */
37
+ export declare function writeSharedEntry(opts: {
38
+ projectRoot: string;
39
+ sid: string;
40
+ rid: string;
41
+ batchId: string;
42
+ key: string;
43
+ from: string;
44
+ value: Record<string, unknown>;
45
+ }): WriteSharedEntryResult;
46
+ /**
47
+ * Read the shared channel for a batch. Returns the channel with all
48
+ * matching entries. Filters: `--since` (ISO8601) and `--key` (glob
49
+ * pattern; simple `*` wildcard, no regex).
50
+ *
51
+ * If the channel file does not exist, returns an empty channel
52
+ * (this is the dispatcher's "fresh batch" view).
53
+ */
54
+ export declare function readSharedChannel(opts: {
55
+ projectRoot: string;
56
+ sid: string;
57
+ rid: string;
58
+ batchId: string;
59
+ since?: string;
60
+ keyPattern?: string;
61
+ }): SharedChannel;
62
+ /**
63
+ * Garbage-collect a single channel. Returns true if the file was
64
+ * deleted, false if it did not exist.
65
+ */
66
+ export declare function gcChannel(opts: {
67
+ projectRoot: string;
68
+ sid: string;
69
+ rid: string;
70
+ batchId: string;
71
+ }): boolean;
72
+ /**
73
+ * Check if a channel file is older than `SHARED_CHANNEL_TTL_DAYS` days
74
+ * and should be GC'd as an orphan. Returns true if file is missing
75
+ * (already GC'd) or older than TTL.
76
+ */
77
+ export declare function isOrphanChannel(opts: {
78
+ projectRoot: string;
79
+ sid: string;
80
+ rid: string;
81
+ batchId: string;
82
+ now?: Date;
83
+ }): boolean;
84
+ /**
85
+ * Compile a simple key pattern with `*` wildcards to a matcher. Only
86
+ * `*` is special; everything else is a literal. `*` matches zero or
87
+ * more characters. Examples:
88
+ * "rd.*" matches "rd.completed", "rd.found-blocker"
89
+ * "*.completed" matches "rd.completed", "qa.completed"
90
+ * "*" matches everything
91
+ */
92
+ export declare function compileKeyPattern(pattern: string): (key: string) => boolean;