peaks-loop 4.0.24 → 4.0.26
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/CHANGELOG.md +29 -0
- package/dist/cli/commands/codegraph-commands.d.ts +10 -0
- package/dist/cli/commands/codegraph-commands.js +18 -4
- package/dist/cli/commands/core/memory-command.js +1 -3
- package/dist/cli/commands/dispatch-commands.js +11 -67
- package/dist/cli/commands/memory-commands.d.ts +0 -2
- package/dist/cli/commands/memory-commands.js +4 -7
- package/dist/cli/commands/preferences-commands.js +0 -1
- package/dist/cli/commands/qa-commands.js +5 -5
- package/dist/cli/commands/sub-agent-shared.d.ts +0 -4
- package/dist/cli/commands/sub-agent-shared.js +0 -14
- package/dist/cli/commands/workflow-commands.js +5 -5
- package/dist/services/context/build-dispatch-system-prompt.d.ts +4 -4
- package/dist/services/context/build-dispatch-system-prompt.js +6 -6
- package/dist/services/context/context-guard.d.ts +2 -4
- package/dist/services/context/context-guard.js +4 -6
- package/dist/services/context/{headroom-fetcher.d.ts → doc-cache-fetcher.d.ts} +2 -2
- package/dist/services/context/{headroom-fetcher.js → doc-cache-fetcher.js} +4 -3
- package/dist/services/context/memory-preflight-service.js +6 -17
- package/dist/services/context/threshold.d.ts +0 -3
- package/dist/services/context/threshold.js +0 -3
- package/dist/services/dispatch/batch-counter.js +1 -1
- package/dist/services/dispatch/leak-detector.js +1 -1
- package/dist/services/fuzzy-matching/fzf-pick-service.d.ts +1 -1
- package/dist/services/fuzzy-matching/fzf-pick-service.js +1 -1
- package/dist/services/memory/llm-reranker.d.ts +4 -5
- package/dist/services/memory/llm-reranker.js +6 -8
- package/dist/services/memory/memory-search-service.d.ts +0 -32
- package/dist/services/memory/memory-search-service.js +0 -37
- package/dist/services/preferences/preferences-service.d.ts +5 -8
- package/dist/services/preferences/preferences-service.js +0 -30
- package/dist/services/preferences/preferences-types.d.ts +0 -22
- package/dist/services/preferences/preferences-types.js +0 -12
- package/dist/services/retrospective/retrospective-search-service.d.ts +0 -18
- package/dist/services/retrospective/retrospective-search-service.js +0 -32
- package/dist/services/slice/slice-benchmark-service.d.ts +1 -1
- package/dist/services/slice/slice-benchmark-service.js +1 -1
- package/dist/services/slice/slice-pick-service.d.ts +1 -1
- package/dist/services/slice/slice-pick-service.js +1 -1
- package/package.json +5 -6
- package/skills/bee/peaks-qa/references/qa-context-governance.md +1 -1
- package/skills/bee/peaks-rd/SKILL.md +9 -2
- package/skills/bee/peaks-rd/references/frontend-acl-mapper.md +29 -0
- package/skills/bee/peaks-rd/references/rd-context-governance.md +2 -2
- package/skills/bee/peaks-txt/SKILL.md +1 -1
- package/skills/bee/peaks-ui/SKILL.md +1 -1
- package/skills/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/codegraph-orchestration.md +7 -1
- package/skills/peaks-code/references/context-governance.md +7 -34
- package/skills/peaks-code/references/envelope-contract.md +2 -7
- package/skills/peaks-code/references/sub-agent-dispatch.md +2 -4
- package/dist/services/context/headroom-client.d.ts +0 -34
- package/dist/services/context/headroom-client.js +0 -117
- package/dist/services/context/headroom-prefs.d.ts +0 -46
- package/dist/services/context/headroom-prefs.js +0 -34
- package/skills/peaks-code/references/headroom-integration.md +0 -107
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.26 — 2026-09-01 (headroom removal + prompt-cache prefix alignment)
|
|
4
|
+
|
|
5
|
+
**2 atomic commits from session 2026-09-01-session-fdd7aa**:
|
|
6
|
+
|
|
7
|
+
- `3d8c911a` refactor(context): remove headroom-ai half-baked integration
|
|
8
|
+
- `2086074b` perf(dispatch): stabilize sub-agent prompt prefix for prompt-cache alignment
|
|
9
|
+
|
|
10
|
+
**Highlights**:
|
|
11
|
+
|
|
12
|
+
1. **移除 headroom-ai 半成品集成** — headroom-ai 的 TS SDK 已接但 Python proxy 后端(N-7)从未集成,`--use-headroom` / `--compress-results` 是静默 no-op。删除依赖 + headroom-client/prefs + G7.7 opt-in 路径 + HeadroomPreferences;保留 G7 metadata-only + auto-compact + prompt-cache 对齐作为省 token 主杠杆。通用 DocFetcher 保留为 doc-cache-fetcher.ts。42 文件,+100/−909。
|
|
13
|
+
|
|
14
|
+
2. **dispatch 前缀稳定化(prompt-cache 对齐)** — 把 LIFECYCLE_RULES 从 prompt 尾部挪到 L1 worktree 块之后,让所有稳定 boilerplate 连续排在最前,最大化 Anthropic prompt-cache 前缀复用(缓存读价约 90% 折扣)。变量块(context % / project memory / task)留在最后。
|
|
15
|
+
|
|
16
|
+
## 4.0.25 — 2026-09-01 (codegraph hint fix + post-slice re-index + frontend ACL)
|
|
17
|
+
|
|
18
|
+
**1 atomic commit from session 2026-09-01-session-fdd7aa** (user feedback batch):
|
|
19
|
+
|
|
20
|
+
- `70184e1f` feat(peaks-code): apply feedback batch — codegraph hint fix + post-slice re-index + frontend ACL + headroom sediment
|
|
21
|
+
|
|
22
|
+
**Highlights**:
|
|
23
|
+
|
|
24
|
+
1. **codegraph 提示泄漏修复** — `peaks codegraph <subcommand>` 透传上游 stdout/stderr 前,新增 `rewriteBareCodegraphHints` 纯函数,把上游裸 `codegraph <sub>` 提示改写为 `peaks codegraph <sub>`(上游 binary 是嵌套传递依赖、不在 PATH,裸命令提示会误导 LLM 敲 "command not found")。8 个 vitest 用例:quoted/backtick hint、7 子命令全量、幂等、`@colbymchenry/codegraph` 包名不误伤。
|
|
25
|
+
|
|
26
|
+
2. **codegraph post-slice 增量刷新** — peaks-code 编排规范:每个 slice/task 完成后主动 `peaks codegraph index`,让下一 slice 的 affected/query/context 基于最新代码图,而不是过期的 pre-slice 图。
|
|
27
|
+
|
|
28
|
+
3. **前端防腐层 ACL 标准** — peaks-rd 新增硬约束:前端内部模型不被外部/API 形状污染;DTO ↔ ViewModel 映射(外部字段 → 内部字段);Mapper 聚合单文件(如 `mappers/user.mapper.ts`),不散落各页面。QA / code-review 校验。
|
|
29
|
+
|
|
30
|
+
4. **headroom-ai 半成品 finding sediment** — 记录 headroom proxy 后端(N-7)未接、当前 0 token 节省;省 token 主杠杆是 G7 metadata-only + auto-compact + cache 对齐。
|
|
31
|
+
|
|
3
32
|
## 4.0.24 — 2026-08-12 (zod v4 + Context7 + Step 2.5 best-practice scan)
|
|
4
33
|
|
|
5
34
|
**6 atomic commits from session 2026-08-12-session-4aaf2b** (5 functional + 1 sediment):
|
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Rewrites bare upstream `codegraph <subcommand>` hints to the peaks-loop
|
|
5
|
+
* equivalent (`peaks codegraph <subcommand>`). The upstream binary is a
|
|
6
|
+
* nested transitive dependency and is NOT on PATH, so an LLM that follows
|
|
7
|
+
* a bare hint like `Run "codegraph init" to initialize` would hit
|
|
8
|
+
* "command not found". Already-prefixed `peaks codegraph ...` hints and
|
|
9
|
+
* other `codegraph` references (e.g. `@colbymchenry/codegraph`) are left
|
|
10
|
+
* untouched.
|
|
11
|
+
*/
|
|
12
|
+
export declare function rewriteBareCodegraphHints(text: string): string;
|
|
3
13
|
export declare function registerCodegraphCommands(program: Command, io: ProgramIO): void;
|
|
@@ -24,6 +24,18 @@ function printCodegraphFailure(io, command, error, asJson, exitCode = 1) {
|
|
|
24
24
|
printResult(io, fail(command, 'CODEGRAPH_COMMAND_FAILED', redactSensitiveErrorMessage(getErrorMessage(error)), {}, ['Check the codegraph command options and project path before retrying']), asJson);
|
|
25
25
|
process.exitCode = exitCode;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Rewrites bare upstream `codegraph <subcommand>` hints to the peaks-loop
|
|
29
|
+
* equivalent (`peaks codegraph <subcommand>`). The upstream binary is a
|
|
30
|
+
* nested transitive dependency and is NOT on PATH, so an LLM that follows
|
|
31
|
+
* a bare hint like `Run "codegraph init" to initialize` would hit
|
|
32
|
+
* "command not found". Already-prefixed `peaks codegraph ...` hints and
|
|
33
|
+
* other `codegraph` references (e.g. `@colbymchenry/codegraph`) are left
|
|
34
|
+
* untouched.
|
|
35
|
+
*/
|
|
36
|
+
export function rewriteBareCodegraphHints(text) {
|
|
37
|
+
return text.replace(/(?<![\w-])(?<!peaks\s)codegraph(?=\s+(?:status|init|index|query|files|context|affected)\b)/g, 'peaks codegraph');
|
|
38
|
+
}
|
|
27
39
|
async function runCodegraphCommand(io, command, options, asJson) {
|
|
28
40
|
try {
|
|
29
41
|
const invocation = createCodegraphInvocation(options);
|
|
@@ -33,11 +45,13 @@ async function runCodegraphCommand(io, command, options, asJson) {
|
|
|
33
45
|
return;
|
|
34
46
|
}
|
|
35
47
|
const didFail = result.exitCode !== null && result.exitCode !== 0;
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
const stdout = rewriteBareCodegraphHints(result.stdout);
|
|
49
|
+
const stderr = rewriteBareCodegraphHints(result.stderr);
|
|
50
|
+
if (stdout.length > 0) {
|
|
51
|
+
io.stdout((didFail ? redactSensitiveErrorMessage(stdout) : stdout).trimEnd());
|
|
38
52
|
}
|
|
39
|
-
if (
|
|
40
|
-
io.stderr((didFail ? redactSensitiveErrorMessage(
|
|
53
|
+
if (stderr.length > 0) {
|
|
54
|
+
io.stderr((didFail ? redactSensitiveErrorMessage(stderr) : stderr).trimEnd());
|
|
41
55
|
}
|
|
42
56
|
if (didFail) {
|
|
43
57
|
process.exitCode = result.exitCode;
|
|
@@ -74,10 +74,9 @@ export function registerMemoryCommand(program, io) {
|
|
|
74
74
|
});
|
|
75
75
|
addJsonOption(memory
|
|
76
76
|
.command('search <query>')
|
|
77
|
-
.description('Fuzzy-search the memory index (deterministic, local, zero-token). Default --limit 6.
|
|
77
|
+
.description('Fuzzy-search the memory index (deterministic, local, zero-token). Default --limit 6.')
|
|
78
78
|
.option('--kind <kind>', 'filter by memory kind (one of: project, rule, decision, reference, feedback, convention, module, lesson)')
|
|
79
79
|
.option('--limit <n>', 'maximum number of matches to return', (value) => Number(value))
|
|
80
|
-
.option('--compress-results', 'compress joined match text via headroom-ai (uses preferences.headroom.perTouchpoint.memorySearch mode)')
|
|
81
80
|
.option('--project <path>', 'target project root (defaults to git root or cwd)')).action((query, options) => {
|
|
82
81
|
// Lazy import avoids a top-of-file import cycle (memory-commands.ts
|
|
83
82
|
// imports services that the rest of this file may also touch).
|
|
@@ -86,7 +85,6 @@ export function registerMemoryCommand(program, io) {
|
|
|
86
85
|
query,
|
|
87
86
|
...(options.kind !== undefined ? { kind: options.kind } : {}),
|
|
88
87
|
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
89
|
-
...(options.compressResults === true ? { compressResults: true } : {}),
|
|
90
88
|
...(options.project !== undefined ? { project: options.project } : {}),
|
|
91
89
|
...(options.json !== undefined ? { json: options.json } : {}),
|
|
92
90
|
});
|
|
@@ -29,13 +29,11 @@ import { evaluatePromptSize } from '../../services/context/context-guard.js';
|
|
|
29
29
|
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
30
30
|
import { buildArtifactMeta, buildContextImpact } from '../../services/context/artifact-meta.js';
|
|
31
31
|
import { assertSafeArtifactPath } from 'peaks-loop-shared-channel';
|
|
32
|
-
import { compressPrompt } from '../../services/context/headroom-client.js';
|
|
33
|
-
import { resolveHeadroomOptions } from '../../services/context/headroom-prefs.js';
|
|
34
32
|
import { playwrightProfilePaths } from '../../services/worktree/playwright-profile.js';
|
|
35
33
|
import { loadPreferences } from '../../services/preferences/preferences-service.js';
|
|
36
34
|
import { DEFAULT_PREFERENCES } from '../../services/preferences/preferences-types.js';
|
|
37
35
|
import { writeLogEntry } from '../../services/log/logger.js';
|
|
38
|
-
import {
|
|
36
|
+
import { PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, validateRole } from './sub-agent-shared.js';
|
|
39
37
|
import { runDispatchFromDag } from './dispatch-from-dag.js';
|
|
40
38
|
import { TEST_TOOL_DETECTION_BLOCK, formatTestToolDetection } from '../../services/dispatch/test-tool-detection.js';
|
|
41
39
|
import { MemoryPreflightService } from '../../services/context/memory-preflight-service.js';
|
|
@@ -46,7 +44,7 @@ export function registerDispatchCommand(parent, io) {
|
|
|
46
44
|
.command('dispatch')
|
|
47
45
|
.description('Build an IDE-specific tool-call descriptor for a sub-agent dispatch. ' +
|
|
48
46
|
'Dry-run by design; the LLM executes the returned toolCall in its own ' +
|
|
49
|
-
'environment. Flags: --write-artifact (G7),
|
|
47
|
+
'environment. Flags: --write-artifact (G7), ' +
|
|
50
48
|
'--force (G9 CLI 兜底). ' +
|
|
51
49
|
'See skills/peaks-code/references/sub-agent-dispatch.md for the ' +
|
|
52
50
|
'orchestrator contract.')
|
|
@@ -63,8 +61,6 @@ export function registerDispatchCommand(parent, io) {
|
|
|
63
61
|
.option('--project <path>', 'target project root (defaults to cwd)')
|
|
64
62
|
.option('--batch-id <uuid>', 'batch id for the dispatch (default: auto-generated UUID)')
|
|
65
63
|
.option('--write-artifact <path>', 'G7: register an artifact file at <path>; CLI computes sha256 + size + writes ArtifactMeta to the dispatch record')
|
|
66
|
-
.option('--use-headroom', 'G7.7/G9: compress the prompt via headroom-ai before dispatch (opt-in; falls back to G7 metadata-only if headroom unavailable)')
|
|
67
|
-
.option('--headroom-mode <mode>', `G7.7: headroom mode (${HEADROOM_MODES.join(' | ')}); default balanced`)
|
|
68
64
|
.option('--force', 'G9: override the 80% hard reject threshold at CLI (NOT allowed at hook layer per RL-30 strict)')
|
|
69
65
|
.option('--from-dag <file>', '2.7.0 slice-dag-dispatcher MVP: read a SliceDag JSON file, dispatch one sub-agent per node in topological order; --batch-id overrides the auto-generated batch id (mutually exclusive with <role>)')
|
|
70
66
|
.option('--isolation <mode>', 'slice 2026-07-29-worktree-l2-extended Part 2.C: isolation mode for the sub-agent. Accepts "worktree" (Part 2.C + Part 12 L2 surface), "container" (Part 8 contract + Part 12 L4 docker runtime), or "vm" (Part 25 contract; the VM runtime is a follow-up rid and fail-fasts with ISOLATION_VM_NOT_YET_IMPLEMENTED). Auto-spawns a lease + injects PEAKS_<MODE>_LEASE_ID into the dispatch envelope so the sub-agent can write to the isolated surface without a separate auth grant.')
|
|
@@ -351,35 +347,14 @@ export function registerDispatchCommand(parent, io) {
|
|
|
351
347
|
return;
|
|
352
348
|
}
|
|
353
349
|
}
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
// loadPreferences can throw on schema mismatch; we fall back to defaults to avoid
|
|
357
|
-
// breaking the dispatch on a stale preferences.json file.
|
|
350
|
+
// loadPreferences can throw on schema mismatch; we fall back to defaults
|
|
351
|
+
// to avoid breaking the dispatch on a stale preferences.json file.
|
|
358
352
|
let projectPrefs = DEFAULT_PREFERENCES;
|
|
359
|
-
let headroomPrefs = DEFAULT_PREFERENCES.headroom;
|
|
360
353
|
try {
|
|
361
354
|
projectPrefs = loadPreferences(projectRoot);
|
|
362
|
-
headroomPrefs = projectPrefs.headroom;
|
|
363
355
|
}
|
|
364
356
|
catch { // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
|
|
365
|
-
// Keep default preferences
|
|
366
|
-
// if they want to override the fallback.
|
|
367
|
-
}
|
|
368
|
-
const headroomResolved = resolveHeadroomOptions(headroomPrefs, {
|
|
369
|
-
useHeadroom: options.useHeadroom === true,
|
|
370
|
-
...(options.headroomMode !== undefined ? { headroomMode: options.headroomMode } : {})
|
|
371
|
-
});
|
|
372
|
-
if (headroomResolved.blocked !== null) {
|
|
373
|
-
printResult(io, fail('sub-agent.dispatch', headroomResolved.blocked, `headroom integration is disabled in preferences (headroom.enabled=false); pass --headroom-mode and update preferences first, or run without --use-headroom`, {
|
|
374
|
-
role,
|
|
375
|
-
toolCall: null,
|
|
376
|
-
dispatchRecordPath: null
|
|
377
|
-
}, [
|
|
378
|
-
'Edit .peaks/preferences.json: set headroom.enabled = true (per-touchpoint mode is headroom.perTouchpoint.subAgentDispatch).',
|
|
379
|
-
'Or re-run without --use-headroom to dispatch without compression.'
|
|
380
|
-
]), asJson);
|
|
381
|
-
process.exitCode = 1;
|
|
382
|
-
return;
|
|
357
|
+
// Keep default preferences.
|
|
383
358
|
}
|
|
384
359
|
const ide = detectInstalledIde(projectRoot) ?? 'claude-code';
|
|
385
360
|
const adapter = getAdapter(ide);
|
|
@@ -390,12 +365,9 @@ export function registerDispatchCommand(parent, io) {
|
|
|
390
365
|
process.exitCode = 1;
|
|
391
366
|
return;
|
|
392
367
|
}
|
|
393
|
-
// G7.7 headroom compress (opt-in). If headroom fails or is unavailable,
|
|
394
|
-
// fall back to the original prompt + emit warning.
|
|
395
368
|
// Slice 2026-07-22-orchestrator-memory-preflight (Task 5): prepend the
|
|
396
369
|
// memory preflight block (or silently skip when unavailable) via the
|
|
397
|
-
// pure-function builder
|
|
398
|
-
// compressor sees the augmented payload.
|
|
370
|
+
// pure-function builder.
|
|
399
371
|
const preflightService = new MemoryPreflightService(projectRoot, projectPrefs);
|
|
400
372
|
const memoryBlock = await preflightService.fetchBlock(role);
|
|
401
373
|
// Slice 2026-07-29-context-evaluation-accuracy: capture the
|
|
@@ -428,9 +400,8 @@ export function registerDispatchCommand(parent, io) {
|
|
|
428
400
|
contextProbe
|
|
429
401
|
});
|
|
430
402
|
// Part 2.C: when --isolation worktree, prepend an isolation envelope
|
|
431
|
-
// block so the sub-agent sees the lease id + worktree path
|
|
432
|
-
//
|
|
433
|
-
// compressor is expected to keep it. We deliberately do NOT set
|
|
403
|
+
// block so the sub-agent sees the lease id + worktree path. The block
|
|
404
|
+
// is short (a few lines). We deliberately do NOT set
|
|
434
405
|
// process.env.PEAKS_WORKTREE_LEASE_ID here — sub-agents are spawned
|
|
435
406
|
// by the LLM in its own environment, not as children of this CLI;
|
|
436
407
|
// the lease id travels through the dispatch record + prompt body.
|
|
@@ -465,20 +436,8 @@ export function registerDispatchCommand(parent, io) {
|
|
|
465
436
|
`confirm the file exists. Anti-fake-green rule (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure §Lesson 1): ` +
|
|
466
437
|
`if the file does not exist, your verdict MUST be \`status: "blocked"\` with reason "must_ls_files_failed". Do NOT silently skip this step.\n`;
|
|
467
438
|
}
|
|
468
|
-
|
|
469
|
-
let headroomCompressed = false;
|
|
470
|
-
let headroomResult = null;
|
|
439
|
+
const effectivePrompt = `${formatTestToolDetection()}\n\n${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
|
|
471
440
|
const warnings = [...decision.warnings];
|
|
472
|
-
if (headroomResolved.mode !== null) {
|
|
473
|
-
headroomResult = await compressPrompt(effectivePrompt, headroomResolved.mode);
|
|
474
|
-
if (headroomResult.warning !== null) {
|
|
475
|
-
warnings.push(headroomResult.warning);
|
|
476
|
-
}
|
|
477
|
-
if (headroomResult.compressed && headroomResult.compressedPrompt !== null) {
|
|
478
|
-
effectivePrompt = headroomResult.compressedPrompt;
|
|
479
|
-
headroomCompressed = true;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
441
|
let toolCall;
|
|
483
442
|
try {
|
|
484
443
|
toolCall = adapter.subAgentDispatcher.buildToolCall({ role, prompt: effectivePrompt, requestId: rid, sessionId: sid });
|
|
@@ -554,8 +513,7 @@ export function registerDispatchCommand(parent, io) {
|
|
|
554
513
|
detail: {
|
|
555
514
|
requestId: rid,
|
|
556
515
|
ide: adapter.subAgentDispatcher.label,
|
|
557
|
-
promptBytes: effectivePrompt.length
|
|
558
|
-
headroomCompressed
|
|
516
|
+
promptBytes: effectivePrompt.length
|
|
559
517
|
}
|
|
560
518
|
}, { projectRoot });
|
|
561
519
|
}
|
|
@@ -628,9 +586,6 @@ export function registerDispatchCommand(parent, io) {
|
|
|
628
586
|
if (counter.warning) {
|
|
629
587
|
nextActions.push(`Batch is over the RL-1 limit (${BATCH_LIMIT}); consider splitting into multiple batches.`);
|
|
630
588
|
}
|
|
631
|
-
if (headroomResult && headroomResult.warning === 'HEADROOM_UNAVAILABLE') {
|
|
632
|
-
nextActions.push('Headroom daemon unavailable; dispatched with G7 metadata-only fallback.');
|
|
633
|
-
}
|
|
634
589
|
const expectedCompletionSeconds = 45;
|
|
635
590
|
const artifactsPublicPaths = typeof options.writeArtifact === 'string' && options.writeArtifact.length > 0
|
|
636
591
|
? [options.writeArtifact]
|
|
@@ -650,7 +605,7 @@ export function registerDispatchCommand(parent, io) {
|
|
|
650
605
|
// disk (gitignored under .peaks/_sub_agents/) keeps the prompt
|
|
651
606
|
// for the sub-agent to read; CLI stdout stays metadata-only.
|
|
652
607
|
// Surface promptSize + originalPromptSize so the LLM-side
|
|
653
|
-
// runner can
|
|
608
|
+
// runner can reason about the size delta without seeing the
|
|
654
609
|
// content.
|
|
655
610
|
originalPromptSize: options.prompt.length,
|
|
656
611
|
promptSize: effectivePrompt.length,
|
|
@@ -658,16 +613,6 @@ export function registerDispatchCommand(parent, io) {
|
|
|
658
613
|
dispatchRecordPath,
|
|
659
614
|
batchId,
|
|
660
615
|
dispatchedInBatch: counter.count,
|
|
661
|
-
headroomCompressed,
|
|
662
|
-
headroomResult: headroomResult
|
|
663
|
-
? {
|
|
664
|
-
mode: headroomResult.mode,
|
|
665
|
-
compressed: headroomResult.compressed,
|
|
666
|
-
compressionRatio: headroomResult.compressionRatio,
|
|
667
|
-
tokensSaved: headroomResult.tokensSaved,
|
|
668
|
-
warning: headroomResult.warning
|
|
669
|
-
}
|
|
670
|
-
: null,
|
|
671
616
|
forcedAt: decision.forcedAt,
|
|
672
617
|
contextImpact,
|
|
673
618
|
artifactMetas: artifactMeta ? [artifactMeta] : [],
|
|
@@ -706,7 +651,6 @@ export function registerDispatchCommand(parent, io) {
|
|
|
706
651
|
role,
|
|
707
652
|
batchId,
|
|
708
653
|
dispatchedInBatch: counter.count,
|
|
709
|
-
headroomCompressed,
|
|
710
654
|
forcedAt: decision.forcedAt
|
|
711
655
|
}
|
|
712
656
|
});
|
|
@@ -5,8 +5,6 @@ export interface MemorySearchCommandOptions {
|
|
|
5
5
|
limit?: number;
|
|
6
6
|
project?: string;
|
|
7
7
|
json?: boolean;
|
|
8
|
-
/** When true, call headroom-ai to compress joined match text for LLM-side prompt assembly. */
|
|
9
|
-
compressResults?: boolean;
|
|
10
8
|
}
|
|
11
9
|
export interface MemoryListCommandOptions {
|
|
12
10
|
kind?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
2
2
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
3
|
-
import { loadMemoryIndex,
|
|
3
|
+
import { loadMemoryIndex, searchMemory } from '../../services/memory/memory-search-service.js';
|
|
4
4
|
import { pickFromList } from '../../services/fuzzy-matching/fzf-pick-service.js';
|
|
5
5
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
6
6
|
import { getErrorMessage, printResult } from '../cli-helpers.js';
|
|
@@ -97,19 +97,16 @@ export async function runMemorySearch(io, options) {
|
|
|
97
97
|
? options.kind
|
|
98
98
|
: undefined;
|
|
99
99
|
try {
|
|
100
|
-
const
|
|
100
|
+
const matches = searchMemory({
|
|
101
101
|
query: options.query,
|
|
102
102
|
projectRoot,
|
|
103
103
|
...(options.limit !== undefined ? { limit: options.limit } : {}),
|
|
104
104
|
...(kindFilter !== undefined ? { kind: kindFilter } : {}),
|
|
105
|
-
}, {
|
|
106
|
-
...(options.compressResults === true ? { compressResults: true } : {})
|
|
107
105
|
});
|
|
108
106
|
printResult(io, ok('memory.search', {
|
|
109
107
|
query: options.query,
|
|
110
|
-
total:
|
|
111
|
-
matches
|
|
112
|
-
...(out.compressedResults !== null ? { compressedResults: out.compressedResults } : {}),
|
|
108
|
+
total: matches.length,
|
|
109
|
+
matches,
|
|
113
110
|
warnings: [],
|
|
114
111
|
}, []), options.json);
|
|
115
112
|
}
|
|
@@ -36,13 +36,13 @@ import { BROWSER_REUSE_HINT } from '../../services/qa/browser-reuse-hint.js';
|
|
|
36
36
|
// Plan 1 / Task 9 — auto-build peaks-context before peaks-qa runs.
|
|
37
37
|
import { buildContext } from '../../services/context/context-builder.js';
|
|
38
38
|
// Plan 1 / Task 10 — production fetcher (replaces mockFetcher).
|
|
39
|
-
import {
|
|
39
|
+
import { createDocCacheFetcher } from '../../services/context/doc-cache-fetcher.js';
|
|
40
40
|
// Plan 2 / Task 8 — consume MUT.sig from peaks-mut into verdict envelope.
|
|
41
41
|
import { loadMutReport, mutReportPath } from 'peaks-loop-mut';
|
|
42
|
-
function
|
|
43
|
-
return
|
|
42
|
+
function buildDocFetcher(sid) {
|
|
43
|
+
return createDocCacheFetcher({
|
|
44
44
|
cacheDir: `.peaks/_runtime/${sid}/doc-cache`,
|
|
45
|
-
// remoteFetcher wired in a future slice
|
|
45
|
+
// remoteFetcher wired in a future slice.
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
async function ensureContextForQa(goal, project, sid) {
|
|
@@ -55,7 +55,7 @@ async function ensureContextForQa(goal, project, sid) {
|
|
|
55
55
|
depsMode: 'locked',
|
|
56
56
|
docBudgetTokens: 8000,
|
|
57
57
|
out,
|
|
58
|
-
fetcher:
|
|
58
|
+
fetcher: buildDocFetcher(sid),
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
61
|
catch (error) {
|
|
@@ -12,12 +12,10 @@
|
|
|
12
12
|
* Everything else is internal to the `peaks sub-agent` group.
|
|
13
13
|
*/
|
|
14
14
|
import type { SubAgentBatchResult } from '../../services/dispatch/sub-agent-dispatcher.js';
|
|
15
|
-
import type { HeadroomMode } from '../../services/context/headroom-client.js';
|
|
16
15
|
import type { HeartbeatStatus } from '../../services/dispatch/dispatch-record-writer.js';
|
|
17
16
|
export { probeShell, type ShellProbeReport, type ShellProbeOptions } from '../../services/env/shell-probe.js';
|
|
18
17
|
export declare const RECOMMENDED_ROLES = "rd | qa | ui | txt | qa-business | qa-perf | qa-security | qa-business-<*> | general-purpose";
|
|
19
18
|
export declare const HEARTBEAT_STATUSES: readonly HeartbeatStatus[];
|
|
20
|
-
export declare const HEADROOM_MODES: readonly HeadroomMode[];
|
|
21
19
|
export declare const PROMPT_LIMIT_BYTES: number;
|
|
22
20
|
export type DispatchOptions = {
|
|
23
21
|
prompt?: string;
|
|
@@ -27,8 +25,6 @@ export type DispatchOptions = {
|
|
|
27
25
|
project?: string;
|
|
28
26
|
batchId?: string;
|
|
29
27
|
writeArtifact?: string;
|
|
30
|
-
useHeadroom?: boolean;
|
|
31
|
-
headroomMode?: string;
|
|
32
28
|
force?: boolean;
|
|
33
29
|
fromDag?: string;
|
|
34
30
|
/**
|
|
@@ -25,7 +25,6 @@ export const HEARTBEAT_STATUSES = [
|
|
|
25
25
|
'never-started',
|
|
26
26
|
'unreadable'
|
|
27
27
|
];
|
|
28
|
-
export const HEADROOM_MODES = ['balanced', 'aggressive', 'conservative'];
|
|
29
28
|
export const PROMPT_LIMIT_BYTES = 256 * 1024;
|
|
30
29
|
/**
|
|
31
30
|
* Validate a role string. Returns `null` when valid, otherwise the
|
|
@@ -75,16 +74,3 @@ export function summarizeBatchResults(results) {
|
|
|
75
74
|
}
|
|
76
75
|
return { total: results.length, done, failed, cancelled, timeout };
|
|
77
76
|
}
|
|
78
|
-
// Note: `isHeadroomMode` and `RegisterSubCommand` used to live here as
|
|
79
|
-
// duplicate exports. Removed in slice 2026-06-23-audit-p0-cleanup:
|
|
80
|
-
// - `isHeadroomMode` is exported by `src/services/context/headroom-prefs.ts`
|
|
81
|
-
// and that is the canonical source — dispatch consumer imports from
|
|
82
|
-
// there directly.
|
|
83
|
-
// - `RegisterSubCommand` was never used as a type anywhere; the entry
|
|
84
|
-
// point (`sub-agent-commands.ts`) calls each register function with
|
|
85
|
-
// `(program, io)` directly.
|
|
86
|
-
// - `deriveProjectRoot` (audit-p0-reaudit) was removed in slice
|
|
87
|
-
// 2026-06-23-audit-3rd: it trusted the record path's `.peaks` segment,
|
|
88
|
-
// letting a caller point `--record` at any project's record tree. The
|
|
89
|
-
// heartbeat command now trusts `--project` (or `process.cwd()`) and
|
|
90
|
-
// leaves the relative() backstop to the R-2 guard.
|
|
@@ -36,11 +36,11 @@ import { registerWorkflowLifecycleCommand } from './workflow-lifecycle-commands.
|
|
|
36
36
|
// Plan 1 / Task 9 — auto-build peaks-context before peaks-rd runs.
|
|
37
37
|
import { buildContext } from '../../services/context/context-builder.js';
|
|
38
38
|
// Plan 1 / Task 10 — production fetcher (replaces mockFetcher).
|
|
39
|
-
import {
|
|
40
|
-
function
|
|
41
|
-
return
|
|
39
|
+
import { createDocCacheFetcher } from '../../services/context/doc-cache-fetcher.js';
|
|
40
|
+
function buildDocFetcher(sid) {
|
|
41
|
+
return createDocCacheFetcher({
|
|
42
42
|
cacheDir: `.peaks/_runtime/${sid}/doc-cache`,
|
|
43
|
-
// remoteFetcher wired in a future slice
|
|
43
|
+
// remoteFetcher wired in a future slice.
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
async function ensureContextForRd(goal, project, sid) {
|
|
@@ -53,7 +53,7 @@ async function ensureContextForRd(goal, project, sid) {
|
|
|
53
53
|
depsMode: 'locked',
|
|
54
54
|
docBudgetTokens: 8000,
|
|
55
55
|
out,
|
|
56
|
-
fetcher:
|
|
56
|
+
fetcher: buildDocFetcher(sid),
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
catch (error) {
|
|
@@ -72,10 +72,10 @@ export declare const L1_WORKTREE_GOVERNANCE_BLOCK = "## Superpowers chain refusa
|
|
|
72
72
|
* index, race the worktree release, and can corrupt the
|
|
73
73
|
* caller's working branch.
|
|
74
74
|
*
|
|
75
|
-
* These rules are
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* These rules are placed immediately after the L1 worktree governance
|
|
76
|
+
* block so every stable boilerplate block is contiguous at the prompt
|
|
77
|
+
* start, maximizing Anthropic prompt-cache prefix reuse (stable-first
|
|
78
|
+
* ordering).
|
|
79
79
|
*/
|
|
80
80
|
export declare const LIFECYCLE_RULES = "## Sub-agent lifecycle rules (locked 2026-08-01)\n\n- If you start a long-lived local service (vite dev, mock API, docker container, etc.), register it with `peaks sub-agent shutdown register --pid <pid> --name <label>` before you exit. The parent session will best-effort-kill it before merge-back.\n- Do NOT run E2E. The parent session runs Playwright verification once after merge-back (Task 10). Your E2E work is duplicate effort.\n- Do NOT call `git merge`, `git pull`, `git rebase`, or `peaks worktree release`. The parent session owns the merge-back step.\n";
|
|
81
81
|
/**
|
|
@@ -62,10 +62,10 @@ If the upstream superpowers chain suggests raw \`git worktree add\`:
|
|
|
62
62
|
* index, race the worktree release, and can corrupt the
|
|
63
63
|
* caller's working branch.
|
|
64
64
|
*
|
|
65
|
-
* These rules are
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
65
|
+
* These rules are placed immediately after the L1 worktree governance
|
|
66
|
+
* block so every stable boilerplate block is contiguous at the prompt
|
|
67
|
+
* start, maximizing Anthropic prompt-cache prefix reuse (stable-first
|
|
68
|
+
* ordering).
|
|
69
69
|
*/
|
|
70
70
|
export const LIFECYCLE_RULES = `## Sub-agent lifecycle rules (locked 2026-08-01)
|
|
71
71
|
|
|
@@ -97,9 +97,9 @@ export function buildDispatchSystemPrompt(input) {
|
|
|
97
97
|
const { taskBody, memoryBlock, contextProbe } = input;
|
|
98
98
|
const contextBlock = renderContextBlock(contextProbe ?? null);
|
|
99
99
|
if (memoryBlock.available === true && typeof memoryBlock.block === 'string') {
|
|
100
|
-
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${contextBlock}${memoryBlock.block}\n## Task\n${taskBody}
|
|
100
|
+
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${memoryBlock.block}\n## Task\n${taskBody}`;
|
|
101
101
|
}
|
|
102
|
-
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${
|
|
102
|
+
return `${L1_WORKTREE_GOVERNANCE_BLOCK}\n${LIFECYCLE_RULES}\n${contextBlock}${taskBody}`;
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
105
|
* Slice 2026-07-29-context-evaluation-accuracy: emit a
|
|
@@ -8,13 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Decision codes:
|
|
10
10
|
* - `OK` — under 50%
|
|
11
|
-
* - `CONTEXT_SOFT_WARN` — 50-75%,
|
|
12
|
-
* - `CONTEXT_NEAR_LIMIT` — 75-80%,
|
|
11
|
+
* - `CONTEXT_SOFT_WARN` — 50-75%, prompt is large
|
|
12
|
+
* - `CONTEXT_NEAR_LIMIT` — 75-80%, prompt is near the limit
|
|
13
13
|
* - `PROMPT_TOO_LARGE` — 80-90%, hard reject (allow = false)
|
|
14
14
|
* - `PROMPT_EMERGENCY` — ≥ 90%, hard reject + emergency
|
|
15
15
|
* - `FORCED_OVER_THRESHOLD` — user passed --force at CLI; allow = true
|
|
16
|
-
*
|
|
17
|
-
* See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`.
|
|
18
16
|
*/
|
|
19
17
|
import { tierToCode, type ThresholdEvaluation } from './threshold.js';
|
|
20
18
|
export type ContextGuardCode = 'OK' | 'CONTEXT_SOFT_WARN' | 'CONTEXT_NEAR_LIMIT' | 'PROMPT_TOO_LARGE' | 'PROMPT_EMERGENCY' | 'FORCED_OVER_THRESHOLD';
|
|
@@ -8,17 +8,15 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Decision codes:
|
|
10
10
|
* - `OK` — under 50%
|
|
11
|
-
* - `CONTEXT_SOFT_WARN` — 50-75%,
|
|
12
|
-
* - `CONTEXT_NEAR_LIMIT` — 75-80%,
|
|
11
|
+
* - `CONTEXT_SOFT_WARN` — 50-75%, prompt is large
|
|
12
|
+
* - `CONTEXT_NEAR_LIMIT` — 75-80%, prompt is near the limit
|
|
13
13
|
* - `PROMPT_TOO_LARGE` — 80-90%, hard reject (allow = false)
|
|
14
14
|
* - `PROMPT_EMERGENCY` — ≥ 90%, hard reject + emergency
|
|
15
15
|
* - `FORCED_OVER_THRESHOLD` — user passed --force at CLI; allow = true
|
|
16
|
-
*
|
|
17
|
-
* See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`.
|
|
18
16
|
*/
|
|
19
17
|
import { CONTEXT_CAPACITY_DEFAULT_BYTES, evaluateThresholdTier, tierToCode } from './threshold.js';
|
|
20
|
-
const NEAR_LIMIT_SUGGEST = '
|
|
21
|
-
const SOFT_WARN_SUGGEST = '
|
|
18
|
+
const NEAR_LIMIT_SUGGEST = 'Prompt is near the context limit; trim the prompt or split into multiple dispatches.';
|
|
19
|
+
const SOFT_WARN_SUGGEST = 'Prompt is large; trim or split into multiple dispatches to stay within context budget.';
|
|
22
20
|
const HARD_REJECT_SUGGEST = 'Trim prompt to < 80% of context capacity. Pass --force at CLI to override (NOT allowed at hook layer).';
|
|
23
21
|
const EMERGENCY_SUGGEST = 'Prompt exceeds 90% of context. Trim aggressively or split into multiple dispatches.';
|
|
24
22
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DocFetcher } from './doc-retriever.js';
|
|
2
|
-
export interface
|
|
2
|
+
export interface DocCacheFetcherOptions {
|
|
3
3
|
readonly cacheDir: string;
|
|
4
4
|
readonly remoteFetcher?: DocFetcher;
|
|
5
5
|
}
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function createDocCacheFetcher(opts: DocCacheFetcherOptions): DocFetcher;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Production DocFetcher.
|
|
3
|
-
*
|
|
2
|
+
* Production DocFetcher. Local cache (per-session, per-dep markdown)
|
|
3
|
+
* preferred when version matches; otherwise delegates to an injected
|
|
4
|
+
* remote fetcher.
|
|
4
5
|
*
|
|
5
6
|
* Hard constraint H2 (locked version): never returns a doc whose version
|
|
6
7
|
* differs from the requested locked version.
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
*/
|
|
13
14
|
import { readFile } from 'node:fs/promises';
|
|
14
15
|
import { join } from 'node:path';
|
|
15
|
-
export function
|
|
16
|
+
export function createDocCacheFetcher(opts) {
|
|
16
17
|
return async (dep, version) => {
|
|
17
18
|
const cachePath = join(opts.cacheDir, `${dep}@${version}.md`);
|
|
18
19
|
try {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - MemoryIndexReader (Task 3) — reads .peaks/memory/index.json + layer=A filter
|
|
9
9
|
* - MemoryLruCache (Task 2) — constructed for capacity parity, but NOT used
|
|
10
10
|
* for memo content (see deviation note below)
|
|
11
|
-
* -
|
|
11
|
+
* - truncateToCap — byte-cap truncation of the composed block
|
|
12
12
|
*
|
|
13
13
|
* Deviation from brief — controller-accepted (pre-task 4):
|
|
14
14
|
* The brief mandated use of `MemoryLruCache` for memo content caching. The
|
|
@@ -20,25 +20,14 @@
|
|
|
20
20
|
* `MemoryLruCache` class remains as a separate, reusable LRU primitive
|
|
21
21
|
* (Task 2) and is not consumed by this service.
|
|
22
22
|
*/
|
|
23
|
-
import { compressPrompt } from './headroom-client.js';
|
|
24
23
|
import { MemoryIndexReader } from './memory-index-reader.js';
|
|
25
24
|
import { resolveMemoryPreflightConfig, } from './memory-preflight-config.js';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const result = await compressPrompt(text, mode);
|
|
29
|
-
if (result.warning !== null || result.compressedPrompt === null) {
|
|
30
|
-
return { text, truncated: false };
|
|
31
|
-
}
|
|
32
|
-
const compressed = result.compressedPrompt;
|
|
33
|
-
if (Buffer.byteLength(compressed, 'utf8') > capBytes) {
|
|
34
|
-
const sliced = compressed.slice(0, Math.max(0, capBytes - 64)) + '\n…[truncated]';
|
|
35
|
-
return { text: sliced, truncated: true };
|
|
36
|
-
}
|
|
37
|
-
return { text: compressed, truncated: false };
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
25
|
+
function truncateToCap(text, capBytes) {
|
|
26
|
+
if (Buffer.byteLength(text, 'utf8') <= capBytes) {
|
|
40
27
|
return { text, truncated: false };
|
|
41
28
|
}
|
|
29
|
+
const sliced = text.slice(0, Math.max(0, capBytes - 64)) + '\n…[truncated]';
|
|
30
|
+
return { text: sliced, truncated: true };
|
|
42
31
|
}
|
|
43
32
|
export class MemoryPreflightService {
|
|
44
33
|
reader;
|
|
@@ -85,7 +74,7 @@ export class MemoryPreflightService {
|
|
|
85
74
|
const header = '## Project memory relevant to this task\n';
|
|
86
75
|
const composed = `${header}${listLines}${tail}`;
|
|
87
76
|
const capBytes = Math.max(64, this.config.maxTokens * 4);
|
|
88
|
-
const { text, truncated } =
|
|
77
|
+
const { text, truncated } = truncateToCap(composed, capBytes);
|
|
89
78
|
const droppedCount = truncated ? selected.length - countItemsInBlock(text) : 0;
|
|
90
79
|
return {
|
|
91
80
|
available: true,
|
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
* 256K default context capacity is a conservative proxy. The LLM's real
|
|
5
5
|
* capacity is its private business (R-1 / R-8 / R-10 / R-13 boundary
|
|
6
6
|
* inherited from slice #009); we use prompt size as the gate signal.
|
|
7
|
-
*
|
|
8
|
-
* See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`
|
|
9
|
-
* for the full G9 rule (RL-27..RL-32, AC-50..AC-65).
|
|
10
7
|
*/
|
|
11
8
|
export declare const CONTEXT_CAPACITY_DEFAULT_BYTES: number;
|
|
12
9
|
export declare const THRESHOLD_SOFT_WARN_RATIO = 0.5;
|
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
* 256K default context capacity is a conservative proxy. The LLM's real
|
|
5
5
|
* capacity is its private business (R-1 / R-8 / R-10 / R-13 boundary
|
|
6
6
|
* inherited from slice #009); we use prompt size as the gate signal.
|
|
7
|
-
*
|
|
8
|
-
* See: `.peaks/memory/sub-agent-headroom-forced-compression-gate.md`
|
|
9
|
-
* for the full G9 rule (RL-27..RL-32, AC-50..AC-65).
|
|
10
7
|
*/
|
|
11
8
|
export const CONTEXT_CAPACITY_DEFAULT_BYTES = 256 * 1024; // 256K
|
|
12
9
|
export const THRESHOLD_SOFT_WARN_RATIO = 0.5; // 50%
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Above 6, the LLM / human is encouraged to split into multiple
|
|
6
6
|
* batches with an explicit reducer step in between. peaks-code's
|
|
7
7
|
* swarm phase dispatches 3; peaks-rd's 4-way fan-out dispatches 4;
|
|
8
|
-
* peaks-qa's 3-way fan-out dispatches 3. The 6 limit leaves
|
|
8
|
+
* peaks-qa's 3-way fan-out dispatches 3. The 6 limit leaves margin
|
|
9
9
|
* for "qa-business-api" / "qa-business-frontend" / "qa-business-regression"
|
|
10
10
|
* subdivisions (3-way + 3-way = 6) without crossing the line.
|
|
11
11
|
*
|