peaks-cli 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/src/cli/commands/gate-commands.js +28 -19
- package/dist/src/cli/commands/hook-handle.d.ts +17 -0
- package/dist/src/cli/commands/hook-handle.js +111 -0
- package/dist/src/cli/commands/hooks-commands.js +72 -21
- package/dist/src/cli/commands/progress-commands.js +9 -2
- package/dist/src/cli/commands/progress-start-spawn.js +30 -4
- package/dist/src/cli/commands/statusline-commands.js +75 -17
- package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
- package/dist/src/cli/commands/sub-agent-commands.js +488 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
- package/dist/src/cli/commands/workspace-commands.js +3 -0
- package/dist/src/cli/program.js +9 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
- package/dist/src/services/config/config-types.d.ts +1 -1
- package/dist/src/services/context/artifact-meta.d.ts +72 -0
- package/dist/src/services/context/artifact-meta.js +105 -0
- package/dist/src/services/context/context-guard.d.ts +49 -0
- package/dist/src/services/context/context-guard.js +91 -0
- package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
- package/dist/src/services/context/dispatch-context-guard.js +192 -0
- package/dist/src/services/context/headroom-client.d.ts +34 -0
- package/dist/src/services/context/headroom-client.js +117 -0
- package/dist/src/services/context/shared-channel.d.ts +92 -0
- package/dist/src/services/context/shared-channel.js +285 -0
- package/dist/src/services/context/threshold.d.ts +35 -0
- package/dist/src/services/context/threshold.js +76 -0
- package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
- package/dist/src/services/dispatch/batch-counter.js +85 -0
- package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
- package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
- package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
- package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
- package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
- package/dist/src/services/dispatch/leak-detector.js +72 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
- package/dist/src/services/ide/hook-protocol.d.ts +44 -0
- package/dist/src/services/ide/hook-protocol.js +71 -0
- package/dist/src/services/ide/hook-translator.d.ts +72 -0
- package/dist/src/services/ide/hook-translator.js +128 -0
- package/dist/src/services/ide/ide-detector.d.ts +10 -0
- package/dist/src/services/ide/ide-detector.js +19 -0
- package/dist/src/services/ide/ide-registry.d.ts +14 -0
- package/dist/src/services/ide/ide-registry.js +45 -0
- package/dist/src/services/ide/ide-types.d.ts +120 -0
- package/dist/src/services/ide/ide-types.js +2 -0
- package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
- package/dist/src/services/ide/shared/atomic-json.js +58 -0
- package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
- package/dist/src/services/ide/shared/safe-path.js +29 -0
- package/dist/src/services/progress/progress-service.d.ts +1 -1
- package/dist/src/services/progress/progress-service.js +18 -14
- package/dist/src/services/security/safe-settings-path.d.ts +12 -0
- package/dist/src/services/security/safe-settings-path.js +104 -0
- package/dist/src/services/signal/cancel-handler.d.ts +14 -0
- package/dist/src/services/signal/cancel-handler.js +76 -0
- package/dist/src/services/skill/resume-detector.d.ts +54 -0
- package/dist/src/services/skill/resume-detector.js +334 -0
- package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
- package/dist/src/services/skill/skill-scheduler.js +53 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
- package/dist/src/services/skills/hooks-settings-service.js +190 -144
- package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
- package/dist/src/services/skills/statusline-settings-service.js +31 -34
- package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
- package/dist/src/services/slice/slice-archive-service.js +111 -0
- package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
- package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
- package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
- package/dist/src/services/solo/status-line-renderer.js +55 -0
- package/dist/src/services/workspace/reconcile-service.d.ts +36 -0
- package/dist/src/services/workspace/reconcile-service.js +107 -6
- package/dist/src/services/workspace/reconcile-types.d.ts +12 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-qa/SKILL.md +57 -1
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +50 -8
- package/skills/peaks-solo/SKILL.md +77 -20
- package/skills/peaks-solo/references/context-governance.md +144 -0
- package/skills/peaks-solo/references/headroom-integration.md +107 -0
- package/skills/peaks-solo/references/runbook.md +3 -3
- package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +17 -0
- package/skills/peaks-ui/SKILL.md +27 -1
|
@@ -0,0 +1,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>/
|
|
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;
|