peaks-cli 1.3.2 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +6 -2
  2. package/dist/src/cli/commands/gate-commands.js +28 -19
  3. package/dist/src/cli/commands/hook-handle.d.ts +17 -0
  4. package/dist/src/cli/commands/hook-handle.js +111 -0
  5. package/dist/src/cli/commands/hooks-commands.js +72 -21
  6. package/dist/src/cli/commands/progress-commands.js +9 -2
  7. package/dist/src/cli/commands/progress-start-spawn.js +30 -4
  8. package/dist/src/cli/commands/statusline-commands.js +75 -17
  9. package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
  10. package/dist/src/cli/commands/sub-agent-commands.js +488 -0
  11. package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
  12. package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
  13. package/dist/src/cli/commands/workspace-commands.js +3 -0
  14. package/dist/src/cli/program.js +9 -0
  15. package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
  16. package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
  17. package/dist/src/services/config/config-types.d.ts +1 -1
  18. package/dist/src/services/context/artifact-meta.d.ts +72 -0
  19. package/dist/src/services/context/artifact-meta.js +105 -0
  20. package/dist/src/services/context/context-guard.d.ts +49 -0
  21. package/dist/src/services/context/context-guard.js +91 -0
  22. package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
  23. package/dist/src/services/context/dispatch-context-guard.js +192 -0
  24. package/dist/src/services/context/headroom-client.d.ts +34 -0
  25. package/dist/src/services/context/headroom-client.js +117 -0
  26. package/dist/src/services/context/shared-channel.d.ts +92 -0
  27. package/dist/src/services/context/shared-channel.js +285 -0
  28. package/dist/src/services/context/threshold.d.ts +35 -0
  29. package/dist/src/services/context/threshold.js +76 -0
  30. package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
  31. package/dist/src/services/dispatch/batch-counter.js +85 -0
  32. package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
  33. package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
  34. package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
  35. package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
  36. package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
  37. package/dist/src/services/dispatch/leak-detector.js +72 -0
  38. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
  39. package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
  40. package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
  41. package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
  42. package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
  43. package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
  44. package/dist/src/services/ide/hook-protocol.d.ts +44 -0
  45. package/dist/src/services/ide/hook-protocol.js +71 -0
  46. package/dist/src/services/ide/hook-translator.d.ts +72 -0
  47. package/dist/src/services/ide/hook-translator.js +128 -0
  48. package/dist/src/services/ide/ide-detector.d.ts +10 -0
  49. package/dist/src/services/ide/ide-detector.js +19 -0
  50. package/dist/src/services/ide/ide-registry.d.ts +14 -0
  51. package/dist/src/services/ide/ide-registry.js +45 -0
  52. package/dist/src/services/ide/ide-types.d.ts +120 -0
  53. package/dist/src/services/ide/ide-types.js +2 -0
  54. package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
  55. package/dist/src/services/ide/shared/atomic-json.js +58 -0
  56. package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
  57. package/dist/src/services/ide/shared/safe-path.js +29 -0
  58. package/dist/src/services/progress/progress-service.d.ts +1 -1
  59. package/dist/src/services/progress/progress-service.js +18 -14
  60. package/dist/src/services/security/safe-settings-path.d.ts +12 -0
  61. package/dist/src/services/security/safe-settings-path.js +104 -0
  62. package/dist/src/services/signal/cancel-handler.d.ts +14 -0
  63. package/dist/src/services/signal/cancel-handler.js +76 -0
  64. package/dist/src/services/skill/resume-detector.d.ts +54 -0
  65. package/dist/src/services/skill/resume-detector.js +334 -0
  66. package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
  67. package/dist/src/services/skill/skill-scheduler.js +53 -0
  68. package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
  69. package/dist/src/services/skills/hooks-settings-service.js +190 -144
  70. package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
  71. package/dist/src/services/skills/statusline-settings-service.js +31 -34
  72. package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
  73. package/dist/src/services/slice/slice-archive-service.js +111 -0
  74. package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
  75. package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
  76. package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
  77. package/dist/src/services/solo/status-line-renderer.js +55 -0
  78. package/dist/src/services/workspace/reconcile-service.d.ts +36 -0
  79. package/dist/src/services/workspace/reconcile-service.js +107 -6
  80. package/dist/src/services/workspace/reconcile-types.d.ts +12 -0
  81. package/dist/src/shared/version.d.ts +1 -1
  82. package/dist/src/shared/version.js +1 -1
  83. package/package.json +2 -1
  84. package/skills/peaks-ide/SKILL.md +159 -0
  85. package/skills/peaks-qa/SKILL.md +57 -1
  86. package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
  87. package/skills/peaks-rd/SKILL.md +50 -8
  88. package/skills/peaks-solo/SKILL.md +77 -20
  89. package/skills/peaks-solo/references/context-governance.md +144 -0
  90. package/skills/peaks-solo/references/headroom-integration.md +107 -0
  91. package/skills/peaks-solo/references/runbook.md +3 -3
  92. package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
  93. package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
  94. package/skills/peaks-txt/SKILL.md +17 -0
  95. package/skills/peaks-ui/SKILL.md +27 -1
@@ -0,0 +1,28 @@
1
+ import { type HookGuardResult } from '../cli/commands/sub-agent-dispatch-guard.js';
2
+ /**
3
+ * Read the prompt size from the LLM platform's hook stdin. Different
4
+ * LLMs send different payload shapes; we accept the most common:
5
+ * - Claude Code: `{"tool_name": "Bash", "tool_input": {"command": "..."}}`
6
+ * - Trae: `{"tool_name": "terminal", "tool_input": {"command": "..."}}`
7
+ *
8
+ * The hook reads the `command` (or `prompt`) field and computes the
9
+ * byte length. If neither field is present, returns 0 (always passes).
10
+ */
11
+ export declare function readPromptSizeFromHookStdin(stdin: unknown): number;
12
+ /**
13
+ * Execute the hook guard via spawnSync. Returns the parsed result or
14
+ * a fallback (allow: true) on subprocess failure.
15
+ *
16
+ * Prefer the in-process `evaluateHookGuard` (no subprocess) when the
17
+ * hook is called from a TypeScript context. Use `runHookGuardSubprocess`
18
+ * only when the hook needs to be invoked from a non-TypeScript caller
19
+ * (e.g. a shell script that wraps the peaks CLI).
20
+ */
21
+ export declare function runHookGuardSubprocess(prompt: string): HookGuardResult;
22
+ /**
23
+ * Main entry point for the hook. Reads the LLM platform's stdin,
24
+ * computes the prompt size, and returns the guard result. Used by
25
+ * the LLM platform's hook JSON to decide whether to allow the tool
26
+ * call.
27
+ */
28
+ export declare function runHookGuard(stdin: unknown): HookGuardResult;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * G9.5 PreToolUse hook execution body.
3
+ *
4
+ * Wraps `peaks sub-agent-dispatch-guard` for LLM platform integration.
5
+ * The hook reads the prompt size from the LLM platform's hook stdin
6
+ * (Claude Code / Trae / etc.), invokes the guard CLI, and returns
7
+ * `{allow: true/false, reason, suggest}` JSON to the LLM platform.
8
+ *
9
+ * The hook is registered via `peaks hooks install` in the LLM
10
+ * platform's `settings.json` `PreToolUse` array. Only IDEs with
11
+ * `IdeAdapter.promptSizeAware: true` get the hook installed.
12
+ *
13
+ * The hook layer is the **strictest** layer in the G9 chain (RL-30).
14
+ * The CLI 兜底 layer (`peaks sub-agent dispatch --force`) can override
15
+ * the 80% threshold; the hook layer CANNOT.
16
+ */
17
+ import { spawnSync } from 'node:child_process';
18
+ import { evaluateHookGuard } from '../cli/commands/sub-agent-dispatch-guard.js';
19
+ /**
20
+ * Read the prompt size from the LLM platform's hook stdin. Different
21
+ * LLMs send different payload shapes; we accept the most common:
22
+ * - Claude Code: `{"tool_name": "Bash", "tool_input": {"command": "..."}}`
23
+ * - Trae: `{"tool_name": "terminal", "tool_input": {"command": "..."}}`
24
+ *
25
+ * The hook reads the `command` (or `prompt`) field and computes the
26
+ * byte length. If neither field is present, returns 0 (always passes).
27
+ */
28
+ export function readPromptSizeFromHookStdin(stdin) {
29
+ if (stdin === null || typeof stdin !== 'object') {
30
+ return 0;
31
+ }
32
+ const obj = stdin;
33
+ const toolInput = obj.tool_input;
34
+ if (toolInput === null || typeof toolInput !== 'object') {
35
+ return 0;
36
+ }
37
+ const ti = toolInput;
38
+ const candidates = ['command', 'prompt', 'text', 'input'];
39
+ for (const key of candidates) {
40
+ const v = ti[key];
41
+ if (typeof v === 'string') {
42
+ return Buffer.byteLength(v, 'utf8');
43
+ }
44
+ }
45
+ return 0;
46
+ }
47
+ /**
48
+ * Execute the hook guard via spawnSync. Returns the parsed result or
49
+ * a fallback (allow: true) on subprocess failure.
50
+ *
51
+ * Prefer the in-process `evaluateHookGuard` (no subprocess) when the
52
+ * hook is called from a TypeScript context. Use `runHookGuardSubprocess`
53
+ * only when the hook needs to be invoked from a non-TypeScript caller
54
+ * (e.g. a shell script that wraps the peaks CLI).
55
+ */
56
+ export function runHookGuardSubprocess(prompt) {
57
+ const result = spawnSync('node', [
58
+ process.argv[1] ?? 'peaks',
59
+ 'sub-agent-dispatch-guard',
60
+ '--prompt', prompt,
61
+ '--json'
62
+ ], { encoding: 'utf8' });
63
+ if (result.status !== 0) {
64
+ // Fallback: allow (don't block the dispatch on a guard subprocess failure).
65
+ return {
66
+ schema: 'peaks-hook-guard/v1',
67
+ allow: true,
68
+ code: 'OK',
69
+ reason: `guard subprocess failed (status ${result.status}); falling through`,
70
+ suggest: null,
71
+ tier: 'ok',
72
+ ratio: 0,
73
+ bytesUsed: 0,
74
+ capacityBytes: 0,
75
+ warnings: ['HOOK_GUARD_SUBPROCESS_FAILED']
76
+ };
77
+ }
78
+ try {
79
+ return JSON.parse(result.stdout);
80
+ }
81
+ catch {
82
+ return {
83
+ schema: 'peaks-hook-guard/v1',
84
+ allow: true,
85
+ code: 'OK',
86
+ reason: 'guard subprocess produced unparseable JSON; falling through',
87
+ suggest: null,
88
+ tier: 'ok',
89
+ ratio: 0,
90
+ bytesUsed: 0,
91
+ capacityBytes: 0,
92
+ warnings: ['HOOK_GUARD_SUBPROCESS_INVALID_JSON']
93
+ };
94
+ }
95
+ }
96
+ /**
97
+ * Main entry point for the hook. Reads the LLM platform's stdin,
98
+ * computes the prompt size, and returns the guard result. Used by
99
+ * the LLM platform's hook JSON to decide whether to allow the tool
100
+ * call.
101
+ */
102
+ export function runHookGuard(stdin) {
103
+ const promptSize = readPromptSizeFromHookStdin(stdin);
104
+ return evaluateHookGuard(promptSize);
105
+ }
@@ -61,7 +61,7 @@ export type PeaksConfig = {
61
61
  /**
62
62
  * Sub-agent progress surfacing knobs. The `peaks progress watch`
63
63
  * CLI (intended to be run in a separate terminal tab while the
64
- * LLM is working) reads `.peaks/<sid>/system/subagent-progress.json`
64
+ * LLM is working) reads `.peaks/_sub_agents/<sid>/subagent-progress.json`
65
65
  * and renders elapsed / spinner / sub-step in real time. The
66
66
  * `enabled` flag is a kill-switch for users who find the watch
67
67
  * distracting; the `heartbeatIntervalMs` lets power users tune
@@ -0,0 +1,72 @@
1
+ export type ArtifactStatus = 'created' | 'finalized' | 'partial' | 'failed';
2
+ export interface ArtifactMeta {
3
+ readonly path: string;
4
+ readonly size: number;
5
+ readonly sha256: string;
6
+ readonly status: ArtifactStatus;
7
+ /** Mandatory literal `false`. The type system rejects `true`. */
8
+ readonly contentInlined: false;
9
+ /** 1-2 sentence description, ≤ 200 chars. Allowed in main context. */
10
+ readonly summary: string | null;
11
+ /** ISO8601. */
12
+ readonly writtenAt: string;
13
+ /** Request id this artifact belongs to. */
14
+ readonly rid: string;
15
+ /** Sub-agent role string. */
16
+ readonly role: string;
17
+ /** Sequence number when same role is dispatched multiple times. */
18
+ readonly idx: number;
19
+ }
20
+ export interface ContextImpact {
21
+ readonly promptSize: number;
22
+ readonly artifactSizes: readonly number[];
23
+ readonly batchTotalSize: number;
24
+ /** `high` if `batchTotalSize > 4MB` OR any `artifactSize > 1MB`. */
25
+ readonly contextWarning: 'normal' | 'high' | 'critical';
26
+ }
27
+ /**
28
+ * Compute the sha256 hex digest of a file. Throws if the file does not
29
+ * exist or is not readable. Caller is expected to handle ENOENT as
30
+ * `code: 'ARTIFACT_NOT_FOUND'` and treat 0-byte files as `status: 'failed'`.
31
+ */
32
+ export declare function computeSha256(filePath: string): string;
33
+ /**
34
+ * Build an `ArtifactMeta` from on-disk file. Computes size + sha256.
35
+ *
36
+ * `status` semantics:
37
+ * - `'created'` — file exists, non-empty, sha256 succeeded
38
+ * - `'failed'` — file is 0 bytes (R-2 / G7.4.e: do not silently succeed)
39
+ * - `'partial'` — caller-provided (e.g. sub-agent reports unfinished work)
40
+ * - `'finalized'` — caller-provided (e.g. sub-agent reports complete)
41
+ */
42
+ export declare function buildArtifactMeta(opts: {
43
+ path: string;
44
+ rid: string;
45
+ role: string;
46
+ idx: number;
47
+ summary: string | null;
48
+ status?: ArtifactStatus;
49
+ /** Override sha256 / size (e.g. when caller already computed them). */
50
+ precomputed?: {
51
+ size: number;
52
+ sha256: string;
53
+ };
54
+ /** Override the writtenAt timestamp. */
55
+ writtenAt?: string;
56
+ }): ArtifactMeta;
57
+ /**
58
+ * Build a `ContextImpact` from a prompt size + artifact sizes.
59
+ * Computes `contextWarning` per the G7.3 rule:
60
+ * - `'critical'` if any artifact > ARTIFACT_MAX_SIZE_BYTES
61
+ * - `'high'` if total > BATCH_TOTAL_HIGH_BYTES (4MB)
62
+ * - `'normal'` otherwise
63
+ */
64
+ export declare function buildContextImpact(opts: {
65
+ promptSize: number;
66
+ artifactSizes: readonly number[];
67
+ }): ContextImpact;
68
+ export declare const ARTIFACT_LIMITS: {
69
+ readonly ARTIFACT_MAX_SIZE_BYTES: number;
70
+ readonly BATCH_TOTAL_HIGH_BYTES: number;
71
+ readonly ARTIFACT_SUMMARY_MAX_CHARS: 200;
72
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * G7 — sub-agent context minimal-occupation (RL-17..RL-22, AC-38..AC-43).
3
+ *
4
+ * `ArtifactMeta` is what the dispatch record stores per sub-agent artifact
5
+ * instead of the full content. The `contentInlined: false` literal is the
6
+ * API contract: the type system rejects `true`, so main LLM context can
7
+ * never accidentally be flooded with inlined artifact bodies.
8
+ *
9
+ * The artifact's content lives on disk at `path`; the meta is ~200 chars
10
+ * (path + size + sha256 + status + summary), so 3 sub-agents × ~200 chars
11
+ * = ~600 chars net context increase per batch instead of 3MB+.
12
+ *
13
+ * Path convention (G7.4.c):
14
+ * `.peaks/_sub_agents/<sid>/artifacts/<rid>-<role>-<idx>.<ext>`
15
+ *
16
+ * See: `.peaks/memory/sub-agent-context-minimal-occupation.md` for the
17
+ * full G7 rule.
18
+ */
19
+ import { createHash } from 'node:crypto';
20
+ import { readFileSync, statSync } from 'node:fs';
21
+ const ARTIFACT_MAX_SIZE_BYTES = 1024 * 1024; // 1MB
22
+ const BATCH_TOTAL_HIGH_BYTES = 4 * 1024 * 1024; // 4MB
23
+ /**
24
+ * Compute the sha256 hex digest of a file. Throws if the file does not
25
+ * exist or is not readable. Caller is expected to handle ENOENT as
26
+ * `code: 'ARTIFACT_NOT_FOUND'` and treat 0-byte files as `status: 'failed'`.
27
+ */
28
+ export function computeSha256(filePath) {
29
+ const content = readFileSync(filePath);
30
+ return createHash('sha256').update(content).digest('hex');
31
+ }
32
+ /**
33
+ * Build an `ArtifactMeta` from on-disk file. Computes size + sha256.
34
+ *
35
+ * `status` semantics:
36
+ * - `'created'` — file exists, non-empty, sha256 succeeded
37
+ * - `'failed'` — file is 0 bytes (R-2 / G7.4.e: do not silently succeed)
38
+ * - `'partial'` — caller-provided (e.g. sub-agent reports unfinished work)
39
+ * - `'finalized'` — caller-provided (e.g. sub-agent reports complete)
40
+ */
41
+ export function buildArtifactMeta(opts) {
42
+ let size;
43
+ let sha256;
44
+ let status = opts.status ?? 'created';
45
+ if (opts.precomputed) {
46
+ size = opts.precomputed.size;
47
+ sha256 = opts.precomputed.sha256;
48
+ }
49
+ else {
50
+ const stat = statSync(opts.path);
51
+ size = stat.size;
52
+ if (size === 0) {
53
+ // 0-byte artifact: cannot compute meaningful sha256; mark as failed.
54
+ sha256 = '0'.repeat(64);
55
+ status = 'failed';
56
+ }
57
+ else {
58
+ sha256 = computeSha256(opts.path);
59
+ }
60
+ }
61
+ const summary = opts.summary;
62
+ if (summary !== null && summary.length > 200) {
63
+ throw new Error(`ArtifactMeta summary must be ≤ 200 chars (got ${summary.length})`);
64
+ }
65
+ return {
66
+ path: opts.path,
67
+ size,
68
+ sha256,
69
+ status,
70
+ contentInlined: false,
71
+ summary,
72
+ writtenAt: opts.writtenAt ?? new Date().toISOString(),
73
+ rid: opts.rid,
74
+ role: opts.role,
75
+ idx: opts.idx
76
+ };
77
+ }
78
+ /**
79
+ * Build a `ContextImpact` from a prompt size + artifact sizes.
80
+ * Computes `contextWarning` per the G7.3 rule:
81
+ * - `'critical'` if any artifact > ARTIFACT_MAX_SIZE_BYTES
82
+ * - `'high'` if total > BATCH_TOTAL_HIGH_BYTES (4MB)
83
+ * - `'normal'` otherwise
84
+ */
85
+ export function buildContextImpact(opts) {
86
+ const batchTotalSize = opts.promptSize + opts.artifactSizes.reduce((a, b) => a + b, 0);
87
+ let contextWarning = 'normal';
88
+ if (opts.artifactSizes.some((s) => s > ARTIFACT_MAX_SIZE_BYTES)) {
89
+ contextWarning = 'critical';
90
+ }
91
+ else if (batchTotalSize > BATCH_TOTAL_HIGH_BYTES) {
92
+ contextWarning = 'high';
93
+ }
94
+ return {
95
+ promptSize: opts.promptSize,
96
+ artifactSizes: opts.artifactSizes,
97
+ batchTotalSize,
98
+ contextWarning
99
+ };
100
+ }
101
+ export const ARTIFACT_LIMITS = {
102
+ ARTIFACT_MAX_SIZE_BYTES,
103
+ BATCH_TOTAL_HIGH_BYTES,
104
+ ARTIFACT_SUMMARY_MAX_CHARS: 200
105
+ };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * G9 — forced compression gate (RL-27..RL-32).
3
+ *
4
+ * The CLI 兜底 layer. Validates prompt size against the threshold
5
+ * table in `threshold.ts` and returns a decision. The PreToolUse hook
6
+ * layer (`peaks sub-agent-dispatch-guard`) re-runs the same logic
7
+ * without `--force` (RL-30 strict).
8
+ *
9
+ * Decision codes:
10
+ * - `OK` — under 50%
11
+ * - `CONTEXT_SOFT_WARN` — 50-75%, suggest --use-headroom
12
+ * - `CONTEXT_NEAR_LIMIT` — 75-80%, mandatory --use-headroom suggestion
13
+ * - `PROMPT_TOO_LARGE` — 80-90%, hard reject (allow = false)
14
+ * - `PROMPT_EMERGENCY` — ≥ 90%, hard reject + emergency
15
+ * - `FORCED_OVER_THRESHOLD` — user passed --force at CLI; allow = true
16
+ *
17
+ * See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`.
18
+ */
19
+ import { tierToCode, type ThresholdEvaluation } from './threshold.js';
20
+ export type ContextGuardCode = 'OK' | 'CONTEXT_SOFT_WARN' | 'CONTEXT_NEAR_LIMIT' | 'PROMPT_TOO_LARGE' | 'PROMPT_EMERGENCY' | 'FORCED_OVER_THRESHOLD';
21
+ export interface ContextGuardDecision {
22
+ readonly allow: boolean;
23
+ readonly code: ContextGuardCode;
24
+ readonly warnings: readonly string[];
25
+ readonly suggest: string | null;
26
+ readonly evaluation: ThresholdEvaluation;
27
+ /** ISO8601 timestamp when --force override was applied. null otherwise. */
28
+ readonly forcedAt: string | null;
29
+ }
30
+ export interface ContextGuardOptions {
31
+ /** Pass `true` to allow override at the ≥ 80% tier. CLI-only; hook layer MUST NOT set this. */
32
+ readonly force?: boolean;
33
+ /** Override the default 256K context capacity (e.g. for tests). */
34
+ readonly capacityBytes?: number;
35
+ }
36
+ /**
37
+ * Evaluate a prompt size against the G9.3 threshold table.
38
+ *
39
+ * The `force` option is the **only** path that lets a ≥ 80% prompt
40
+ * through. The hook layer (PreToolUse) MUST NOT accept this option;
41
+ * it is enforced by the `peaks sub-agent-dispatch-guard` atom's
42
+ * command-line parser, which does not declare a `--force` flag.
43
+ */
44
+ export declare function evaluatePromptSize(promptSize: number, opts?: ContextGuardOptions): ContextGuardDecision;
45
+ /**
46
+ * Re-export of `tierToCode` for callers that want a stable mapping
47
+ * without depending on the threshold module directly.
48
+ */
49
+ export { tierToCode };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * G9 — forced compression gate (RL-27..RL-32).
3
+ *
4
+ * The CLI 兜底 layer. Validates prompt size against the threshold
5
+ * table in `threshold.ts` and returns a decision. The PreToolUse hook
6
+ * layer (`peaks sub-agent-dispatch-guard`) re-runs the same logic
7
+ * without `--force` (RL-30 strict).
8
+ *
9
+ * Decision codes:
10
+ * - `OK` — under 50%
11
+ * - `CONTEXT_SOFT_WARN` — 50-75%, suggest --use-headroom
12
+ * - `CONTEXT_NEAR_LIMIT` — 75-80%, mandatory --use-headroom suggestion
13
+ * - `PROMPT_TOO_LARGE` — 80-90%, hard reject (allow = false)
14
+ * - `PROMPT_EMERGENCY` — ≥ 90%, hard reject + emergency
15
+ * - `FORCED_OVER_THRESHOLD` — user passed --force at CLI; allow = true
16
+ *
17
+ * See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`.
18
+ */
19
+ import { CONTEXT_CAPACITY_DEFAULT_BYTES, evaluateThresholdTier, tierToCode } from './threshold.js';
20
+ const NEAR_LIMIT_SUGGEST = 'Consider --use-headroom to compress prompt.';
21
+ const SOFT_WARN_SUGGEST = 'Use --use-headroom to compress prompt proactively.';
22
+ const HARD_REJECT_SUGGEST = 'Trim prompt to < 80% of context capacity. Pass --force at CLI to override (NOT allowed at hook layer).';
23
+ const EMERGENCY_SUGGEST = 'Prompt exceeds 90% of context. Trim aggressively or split into multiple dispatches.';
24
+ /**
25
+ * Evaluate a prompt size against the G9.3 threshold table.
26
+ *
27
+ * The `force` option is the **only** path that lets a ≥ 80% prompt
28
+ * through. The hook layer (PreToolUse) MUST NOT accept this option;
29
+ * it is enforced by the `peaks sub-agent-dispatch-guard` atom's
30
+ * command-line parser, which does not declare a `--force` flag.
31
+ */
32
+ export function evaluatePromptSize(promptSize, opts = {}) {
33
+ const capacity = opts.capacityBytes ?? CONTEXT_CAPACITY_DEFAULT_BYTES;
34
+ const evaluation = evaluateThresholdTier(promptSize, capacity);
35
+ const tier = evaluation.tier;
36
+ let allow;
37
+ let code;
38
+ let suggest;
39
+ let forcedAt = null;
40
+ switch (tier) {
41
+ case 'ok':
42
+ allow = true;
43
+ code = 'OK';
44
+ suggest = null;
45
+ break;
46
+ case 'soft-warn':
47
+ allow = true;
48
+ code = 'CONTEXT_SOFT_WARN';
49
+ suggest = SOFT_WARN_SUGGEST;
50
+ break;
51
+ case 'near-limit':
52
+ allow = true;
53
+ code = 'CONTEXT_NEAR_LIMIT';
54
+ suggest = NEAR_LIMIT_SUGGEST;
55
+ break;
56
+ case 'hard-reject':
57
+ allow = false;
58
+ code = 'PROMPT_TOO_LARGE';
59
+ suggest = HARD_REJECT_SUGGEST;
60
+ break;
61
+ case 'emergency':
62
+ allow = false;
63
+ code = 'PROMPT_EMERGENCY';
64
+ suggest = EMERGENCY_SUGGEST;
65
+ break;
66
+ }
67
+ // --force override (CLI only; hook layer never reaches this branch)
68
+ if (!allow && opts.force === true) {
69
+ allow = true;
70
+ code = 'FORCED_OVER_THRESHOLD';
71
+ suggest = 'Override applied at CLI. Hook layer will still reject.';
72
+ forcedAt = new Date().toISOString();
73
+ }
74
+ const warnings = [...evaluation.warnings];
75
+ if (forcedAt !== null && !warnings.includes('FORCED_OVER_THRESHOLD')) {
76
+ warnings.push('FORCED_OVER_THRESHOLD');
77
+ }
78
+ return {
79
+ allow,
80
+ code,
81
+ warnings,
82
+ suggest,
83
+ evaluation,
84
+ forcedAt
85
+ };
86
+ }
87
+ /**
88
+ * Re-export of `tierToCode` for callers that want a stable mapping
89
+ * without depending on the threshold module directly.
90
+ */
91
+ export { tierToCode };
@@ -0,0 +1,27 @@
1
+ /** Build the canonical artifact path for a given session/rid/role/idx/ext. */
2
+ export declare function artifactPath(projectRoot: string, sid: string, rid: string, role: string, idx: number, ext?: string): string;
3
+ /** Build the canonical shared channel path. */
4
+ export declare function sharedChannelPath(projectRoot: string, sid: string, rid: string, batchId: string): string;
5
+ /**
6
+ * Assert that `artifactPath` lives under
7
+ * `projectRoot/.peaks/_sub_agents/<sid>/artifacts/`. Rejects
8
+ * symlink/junction escapes and `..` segments.
9
+ *
10
+ * Throws an Error with `.code = 'INVALID_ARTIFACT_PATH'` on rejection.
11
+ */
12
+ export declare function assertSafeArtifactPath(artifactPathInput: string, projectRoot: string): string;
13
+ /**
14
+ * Assert that `channelPath` lives under
15
+ * `projectRoot/.peaks/_sub_agents/<sid>/shared/`. Same R-2 logic as
16
+ * `assertSafeArtifactPath` but with a different canonical subdir.
17
+ *
18
+ * Throws an Error with `.code = 'INVALID_SHARED_CHANNEL_PATH'` on rejection.
19
+ */
20
+ export declare function assertSafeSharedChannelPath(channelPathInput: string, projectRoot: string): string;
21
+ /**
22
+ * Soft-warn check on the artifact file name pattern. Returns null if
23
+ * the name matches `<rid>-<role>-<idx>.<ext>`, otherwise returns a
24
+ * warning string. Does NOT reject (the path is still in the canonical
25
+ * dir; the warning is for human/audit readability per G7.4.c).
26
+ */
27
+ export declare function checkArtifactNameConvention(artifactPathInput: string): string | null;