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,55 @@
1
+ /**
2
+ * `peaks sub-agent-dispatch-guard` — G9.5 / RL-30 strict hook-only atom.
3
+ *
4
+ * This is the **second-layer** gate (PreToolUse hook) for the G9 forced
5
+ * compression threshold. It re-validates the prompt size against the
6
+ * threshold table in `src/services/context/threshold.ts` and returns
7
+ * `{allow: true/false, reason, suggest}` JSON to the LLM platform.
8
+ *
9
+ * **NO `--force` flag is exposed at this layer** (RL-30 strict). The
10
+ * hook is the strictest layer in the G9 chain. If the CLI is bypassed
11
+ * (e.g. a user manually invokes the dispatch CLI with `--force` to
12
+ * override the 80% threshold), the hook catches it and returns
13
+ * `{allow: false}` regardless.
14
+ *
15
+ * This atom is **hidden from `peaks --help`** per dev-preference
16
+ * "skill-first / CLI-auxiliary" + PB-2 byte-stable. It is registered
17
+ * via the LLM platform's PreToolUse hook chain (e.g. Claude Code's
18
+ * `settings.json` `PreToolUse` array) and is not a user-facing command.
19
+ *
20
+ * The `peaks hooks install` command reads `IdeAdapter.promptSizeAware`
21
+ * to decide whether to register this hook for a given IDE.
22
+ */
23
+ import { Command } from 'commander';
24
+ import { type ContextGuardDecision } from '../../services/context/context-guard.js';
25
+ export declare const HOOK_GUARD_RESULT_TYPE: "peaks-hook-guard/v1";
26
+ export interface HookGuardResult {
27
+ readonly schema: typeof HOOK_GUARD_RESULT_TYPE;
28
+ readonly allow: boolean;
29
+ readonly code: ContextGuardDecision['code'];
30
+ readonly reason: string;
31
+ readonly suggest: string | null;
32
+ readonly tier: ContextGuardDecision['evaluation']['tier'];
33
+ readonly ratio: number;
34
+ readonly bytesUsed: number;
35
+ readonly capacityBytes: number;
36
+ readonly warnings: readonly string[];
37
+ }
38
+ /**
39
+ * Build the hook-guard result for a given prompt size. Pure function;
40
+ * no IO. The CLI atom (registered below) calls this and prints JSON.
41
+ *
42
+ * Even if the caller passes `force = true` in the input (it shouldn't —
43
+ * the hook CLI doesn't expose that flag), this function ignores it
44
+ * and treats the prompt as if no override were available. This is the
45
+ * RL-30 strict semantics.
46
+ */
47
+ export declare function evaluateHookGuard(promptSize: number): HookGuardResult;
48
+ /**
49
+ * Register the `peaks sub-agent-dispatch-guard` command. Intentionally
50
+ * NOT registered in the main `peaks --help` quickstart (dev-preference
51
+ * PB-2 byte-stable). The caller (the `peaks hooks install` flow) calls
52
+ * this directly via the imported function; the CLI registration in
53
+ * `src/cli/index.ts` uses a hidden command (no `description`, no help).
54
+ */
55
+ export declare function registerSubAgentDispatchGuard(program: Command): void;
@@ -0,0 +1,57 @@
1
+ import { evaluatePromptSize } from '../../services/context/context-guard.js';
2
+ export const HOOK_GUARD_RESULT_TYPE = 'peaks-hook-guard/v1';
3
+ /**
4
+ * Build the hook-guard result for a given prompt size. Pure function;
5
+ * no IO. The CLI atom (registered below) calls this and prints JSON.
6
+ *
7
+ * Even if the caller passes `force = true` in the input (it shouldn't —
8
+ * the hook CLI doesn't expose that flag), this function ignores it
9
+ * and treats the prompt as if no override were available. This is the
10
+ * RL-30 strict semantics.
11
+ */
12
+ export function evaluateHookGuard(promptSize) {
13
+ // Intentionally pass `force: false` always. The hook layer is strict.
14
+ const decision = evaluatePromptSize(promptSize, { force: false });
15
+ return {
16
+ schema: HOOK_GUARD_RESULT_TYPE,
17
+ allow: decision.allow,
18
+ code: decision.code,
19
+ reason: decision.allow
20
+ ? `prompt size ${promptSize} bytes within threshold (tier=${decision.evaluation.tier})`
21
+ : `prompt size ${promptSize} bytes exceeds threshold (tier=${decision.evaluation.tier}, ratio=${decision.evaluation.ratio.toFixed(3)})`,
22
+ suggest: decision.suggest,
23
+ tier: decision.evaluation.tier,
24
+ ratio: decision.evaluation.ratio,
25
+ bytesUsed: decision.evaluation.bytesUsed,
26
+ capacityBytes: decision.evaluation.capacityBytes,
27
+ warnings: decision.warnings
28
+ };
29
+ }
30
+ /**
31
+ * Register the `peaks sub-agent-dispatch-guard` command. Intentionally
32
+ * NOT registered in the main `peaks --help` quickstart (dev-preference
33
+ * PB-2 byte-stable). The caller (the `peaks hooks install` flow) calls
34
+ * this directly via the imported function; the CLI registration in
35
+ * `src/cli/index.ts` uses a hidden command (no `description`, no help).
36
+ */
37
+ export function registerSubAgentDispatchGuard(program) {
38
+ program
39
+ .command('sub-agent-dispatch-guard')
40
+ .description('INTERNAL: PreToolUse hook guard (G9.5 / RL-30 strict)')
41
+ .requiredOption('--prompt <text>', 'the prompt to validate (size in bytes is what gets checked)')
42
+ .option('--prompt-length <bytes>', 'DOGFOOD ONLY: synthesize a prompt of this size (overrides --prompt content for size only)')
43
+ .action((options) => {
44
+ let prompt = options.prompt;
45
+ if (typeof options.promptLength === 'string' && options.promptLength.length > 0) {
46
+ const len = Number.parseInt(options.promptLength, 10);
47
+ if (Number.isInteger(len) && len > 0) {
48
+ prompt = 'x'.repeat(len);
49
+ }
50
+ }
51
+ const promptSize = Buffer.byteLength(prompt, 'utf8');
52
+ const result = evaluateHookGuard(promptSize);
53
+ // Always exit 0 — the LLM platform reads `allow` from JSON.
54
+ // The decision is encoded in `allow` / `code`, not the exit code.
55
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
56
+ });
57
+ }
@@ -10,6 +10,7 @@ import { validateChangeIdOrThrow } from '../../shared/change-id.js';
10
10
  import { getEconomyAwareExecutionModelId } from '../../services/config/model-routing.js';
11
11
  import { getLocalArtifactPath } from '../../services/artifacts/workspace-service.js';
12
12
  import { getSessionId } from '../../services/session/session-manager.js';
13
+ import { getSessionDir } from '../../services/session/getSessionDir.js';
13
14
  import { findProjectRoot } from '../../services/config/config-safety.js';
14
15
  import { verifyPipeline } from '../../services/workflow/pipeline-verify-service.js';
15
16
  import { fail, ok } from '../../shared/result.js';
@@ -18,7 +19,7 @@ function getCurrentWorkspaceContext() {
18
19
  try {
19
20
  const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
20
21
  const sessionId = getSessionId(projectRoot);
21
- return sessionId ? { sessionId, sessionDir: `.peaks/${sessionId}` } : {};
22
+ return sessionId ? { sessionId, sessionDir: getSessionDir(projectRoot, sessionId) } : {};
22
23
  }
23
24
  catch {
24
25
  return {};
@@ -264,6 +264,9 @@ export function registerWorkspaceCommands(program, io) {
264
264
  if (result.systemCleaned.length > 0) {
265
265
  nextActions.push(`Removed ${result.systemCleaned.length} F3 system/ subdir(s).`);
266
266
  }
267
+ if (result.subAgentStateMigrated > 0) {
268
+ nextActions.push(`Migrated ${result.subAgentStateMigrated} legacy sub-agent state file(s) into .peaks/_sub_agents/.`);
269
+ }
267
270
  printResult(io, ok('workspace.reconcile', result, warnings, nextActions), options.json);
268
271
  if (result.errors.length > 0) {
269
272
  process.exitCode = 1;
@@ -17,7 +17,10 @@ import { registerScanCommands } from './commands/scan-commands.js';
17
17
  import { registerShadcnCommands } from './commands/shadcn-commands.js';
18
18
  import { registerSliceCommands } from './commands/slice-commands.js';
19
19
  import { registerSopCommands } from './commands/sop-commands.js';
20
+ import { registerSubAgentCommands } from './commands/sub-agent-commands.js';
21
+ import { registerSubAgentDispatchGuard } from './commands/sub-agent-dispatch-guard.js';
20
22
  import { registerGateCommands } from './commands/gate-commands.js';
23
+ import { registerHookHandleCommand } from './commands/hook-handle.js';
21
24
  import { registerHooksCommands } from './commands/hooks-commands.js';
22
25
  import { registerStatusLineCommands } from './commands/statusline-commands.js';
23
26
  import { registerUnderstandCommands } from './commands/understand-commands.js';
@@ -92,7 +95,13 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
92
95
  registerShadcnCommands(program, io);
93
96
  registerSliceCommands(program, io);
94
97
  registerSopCommands(program, io);
98
+ registerSubAgentCommands(program, io);
99
+ // Slice #010 G9.5: register the hook-only internal atom. Hidden from
100
+ // `peaks --help` (no description text); used by `peaks hooks install`
101
+ // to wire the PreToolUse hook chain.
102
+ registerSubAgentDispatchGuard(program);
95
103
  registerGateCommands(program, io);
104
+ registerHookHandleCommand(program, io);
96
105
  registerHooksCommands(program, io);
97
106
  registerStatusLineCommands(program, io);
98
107
  registerUnderstandCommands(program, io);
@@ -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;