peaks-loop 4.0.35 → 4.0.36
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 +16 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +57 -2
- package/dist/cli/commands/core/doctor-command.d.ts +8 -0
- package/dist/cli/commands/core/doctor-command.js +44 -2
- package/dist/cli/commands/core/memory-command.js +5 -1
- package/dist/cli/commands/dispatch-commands.js +15 -3
- package/dist/cli/commands/dispatch-from-dag.js +17 -0
- package/dist/cli/commands/memory-commands.d.ts +24 -0
- package/dist/cli/commands/memory-commands.js +77 -10
- package/dist/cli/commands/request-commands.d.ts +8 -0
- package/dist/cli/commands/request-commands.js +23 -2
- package/dist/cli/commands/sub-agent-commands.js +2 -0
- package/dist/cli/commands/wave-plan-commands.d.ts +24 -0
- package/dist/cli/commands/wave-plan-commands.js +93 -0
- package/dist/services/context/build-dispatch-system-prompt.d.ts +66 -9
- package/dist/services/context/build-dispatch-system-prompt.js +132 -17
- package/dist/services/context/context-audit.d.ts +100 -0
- package/dist/services/context/context-audit.js +322 -0
- package/dist/services/context/summary-view.d.ts +54 -0
- package/dist/services/context/summary-view.js +114 -0
- package/dist/services/dispatch/file-overlap-wave-planner.d.ts +70 -0
- package/dist/services/dispatch/file-overlap-wave-planner.js +119 -0
- package/dist/services/dispatch/session-capsule.d.ts +23 -0
- package/dist/services/dispatch/session-capsule.js +56 -0
- package/dist/services/dispatch/slice-dag.d.ts +9 -0
- package/dist/services/dispatch/slice-dag.js +9 -1
- package/dist/services/dispatch/test-tool-detection.d.ts +12 -1
- package/dist/services/dispatch/test-tool-detection.js +14 -13
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +10 -0
- package/dist/services/ide/adapters/claude-code-adapter.js +20 -1
- package/dist/services/ide/ide-types.d.ts +15 -0
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +5 -0
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +55 -5
- package/package.json +5 -5
- package/skills/bee/peaks-qa/SKILL.md +2 -0
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +12 -0
- package/skills/bee/peaks-rd/SKILL.md +2 -0
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +14 -0
- package/skills/bee/peaks-txt/SKILL.md +2 -0
- package/skills/bee/peaks-ui/SKILL.md +2 -0
- package/skills/peaks-code/SKILL.md +8 -0
- package/skills/peaks-code/references/context-governance.md +29 -0
- package/skills/peaks-doctor/SKILL.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.36 — 2026-09-10 (派发提示词瘦身 + 波次调度 + 编排器上下文审计)
|
|
4
|
+
|
|
5
|
+
**Highlights**:
|
|
6
|
+
|
|
7
|
+
1. **派发提示词瘦身(不牺牲规则)** — 每次派发的提示词里约 52% 是 CLI 注入的重复 boilerplate。本轮压缩 L1/生命周期/上下文探测段,并把 test-tool 块统一为一份(去掉 vitest/jest/mocha 举例,保留全部操作性规则,补上只存在于完整块里的 PB-5 例外规则)。7 个角色现在**字节完全一致**:rd/qa/qa-business/sc/prd **3897 → 3191 B(−18.1%)**。绑定规则 **21/21**、runner 规则 **5/5** 全角色断言。
|
|
8
|
+
- 契约指针实验**已回退**——它每次多 165 B 却不减任何内容,净亏。
|
|
9
|
+
|
|
10
|
+
2. **文件重叠感知的波次调度** — 新 planner 按切面文件集计算波次:同一波内文件两两不重叠,重叠的推迟到后续波并**注明被哪个文件挡住**;附加式接入 `--from-dag`(`firstLevelWaves`),不改原调度语义。
|
|
11
|
+
|
|
12
|
+
3. **编排器上下文审计 + 纪律** — 新增 `peaks code context-audit`:读会话 transcript 按 (工具, 命令) 汇总上下文消耗 Top-N。本仓库实测 **779 KB / 586 组**,且 Top-5 仅占 ~15%——**是长尾,不是单一元凶**。
|
|
13
|
+
- `--summary` 用于 memory reindex/list、doctor、request list:envelope **9.4–59 KB → 1.7–1.9 KB**(−79% ~ −97%),默认 `--json` 形状字节不变。
|
|
14
|
+
- SKILL.md 绑定规则:单次工具输出 >2 KB 禁止直接灌入编排器上下文。
|
|
15
|
+
- 子代理最终报告封顶 **≤40 行 / 2 KB**(5 个必填字段,细节写 artifact),已纳入 `BINDING_RULE_TOKENS`。
|
|
16
|
+
|
|
17
|
+
**验证**:build clean、tsc clean、全量 141 files / 1270 passed(1 skipped)。
|
|
18
|
+
|
|
3
19
|
## 4.0.35 — 2026-09-10 (记忆系统 overhaul — 写入合一 / 索引重建 / 按任务调取 / 漂移健康检查 / rotate)
|
|
4
20
|
|
|
5
21
|
**Highlights**:
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slice rid-024 — runtime probes: post-compact-detect / auto-compact /
|
|
3
|
-
* context-now / gate-step-08 / emit-handoff.
|
|
3
|
+
* context-now / context-audit / gate-step-08 / emit-handoff.
|
|
4
4
|
*
|
|
5
5
|
* Extracted from code-commands.ts (rid-024 split).
|
|
6
|
-
* Owns:
|
|
6
|
+
* Owns: 6 sub-commands that read or mutate runtime state.
|
|
7
7
|
* Owns the `readActiveSid` helper (only used by these runtime probes).
|
|
8
|
+
*
|
|
9
|
+
* Slice 2026-09-10-context-audit-and-discipline added `context-audit`
|
|
10
|
+
* (what fills the window, grouped by tool + short input key).
|
|
8
11
|
*/
|
|
9
12
|
import type { Command } from 'commander';
|
|
10
13
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slice rid-024 — runtime probes: post-compact-detect / auto-compact /
|
|
3
|
-
* context-now / gate-step-08 / emit-handoff.
|
|
3
|
+
* context-now / context-audit / gate-step-08 / emit-handoff.
|
|
4
4
|
*
|
|
5
5
|
* Extracted from code-commands.ts (rid-024 split).
|
|
6
|
-
* Owns:
|
|
6
|
+
* Owns: 6 sub-commands that read or mutate runtime state.
|
|
7
7
|
* Owns the `readActiveSid` helper (only used by these runtime probes).
|
|
8
|
+
*
|
|
9
|
+
* Slice 2026-09-10-context-audit-and-discipline added `context-audit`
|
|
10
|
+
* (what fills the window, grouped by tool + short input key).
|
|
8
11
|
*/
|
|
9
12
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
10
13
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
11
14
|
import { detectPostCompactResume, formatPostCompactResumeLogLine } from '../../services/code/post-compact-detector.js';
|
|
12
15
|
import { runAutoCompact } from '../../services/code/auto-compact-orchestrator.js';
|
|
16
|
+
import { auditContext } from '../../services/context/context-audit.js';
|
|
13
17
|
import { evaluateStep08, STEP_08_BACKUP_REGEX } from '../../services/code/step-08-gate.js';
|
|
14
18
|
import { evaluateEmitHandoff, JOB_NOT_INITIALIZED, JOB_REMAINING_BLOCKED } from '../../services/code/emit-handoff.js';
|
|
15
19
|
import { readJobShapeDecision, JobShapeDecisionError } from '../../services/code/job-shape-decision.js';
|
|
@@ -254,6 +258,57 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
254
258
|
process.exitCode = 1;
|
|
255
259
|
}
|
|
256
260
|
});
|
|
261
|
+
// Slice 2026-09-10-context-audit-and-discipline (Slice A): visibility into
|
|
262
|
+
// WHAT fills the orchestrator window. `context-now` returns a ratio; this
|
|
263
|
+
// returns the grouped byte breakdown of tool results from the live
|
|
264
|
+
// transcript. Read-only + fail-soft: an unavailable transcript reports
|
|
265
|
+
// `available:false` with a reason and NEVER sets a non-zero exit code.
|
|
266
|
+
addJsonOption(code
|
|
267
|
+
.command('context-audit')
|
|
268
|
+
.description('Slice 2026-09-10 Slice A: report what fills the current session\'s ' +
|
|
269
|
+
'context window, grouped by tool + short input key (command line / ' +
|
|
270
|
+
'path tail / pattern), sorted by bytes. Locates the CURRENT session\'s ' +
|
|
271
|
+
'IDE transcript through the active adapter\'s ' +
|
|
272
|
+
'`compact.resolveTranscriptPath` (vendor-neutral); emits total bytes, ' +
|
|
273
|
+
'entry count, and the top-N groups ' +
|
|
274
|
+
'(`{tool, key, bytes, pctOfTotal, count}`). Read-only and fail-soft — ' +
|
|
275
|
+
'a missing/oversized/corrupt transcript returns `available:false` ' +
|
|
276
|
+
'with a reason and never blocks. Never dumps tool result content.')
|
|
277
|
+
.requiredOption('--project <path>', 'target project root')
|
|
278
|
+
.option('--session-id <sid>', 'override session id (default: read from active presence)')
|
|
279
|
+
.option('--top <n>', 'number of top entries to emit (default 15, max 100)', (value) => Number(value))
|
|
280
|
+
.option('--transcript <path>', 'override the transcript jsonl path (test seam)')).action((opts) => {
|
|
281
|
+
try {
|
|
282
|
+
// `resolveOuterSessionId` checks the env signal FIRST, so the peaks
|
|
283
|
+
// session id is only a fallback lookup key — mirror context-now's
|
|
284
|
+
// 'unknown' default so an unbound presence still resolves via env.
|
|
285
|
+
const sessionId = opts.sessionId ?? readActiveSid(opts.project) ?? 'unknown';
|
|
286
|
+
const outerSessionId = resolveOuterSessionId(opts.project, sessionId);
|
|
287
|
+
const result = auditContext({
|
|
288
|
+
outerSessionId: outerSessionId ?? null,
|
|
289
|
+
...(opts.top !== undefined ? { topN: opts.top } : {}),
|
|
290
|
+
...(opts.transcript !== undefined ? { transcriptPath: opts.transcript } : {}),
|
|
291
|
+
});
|
|
292
|
+
// Fail-soft contract: unavailability is DATA, not an error. The exit
|
|
293
|
+
// code stays 0 so a `context-audit` call can never block a workflow.
|
|
294
|
+
const nextActions = result.available
|
|
295
|
+
? [`${result.entryCount} tool result(s) across ${result.groupCount} group(s); showing top ${result.entries.length}.`]
|
|
296
|
+
: [`context-audit unavailable: ${result.reason ?? 'unknown'} — continue without it (read-only probe).`];
|
|
297
|
+
printResult(io, ok('code.context-audit', { ...result }, [], nextActions), opts.json);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
printResult(io, ok('code.context-audit', {
|
|
301
|
+
available: false,
|
|
302
|
+
reason: `audit-failed: ${getErrorMessage(err)}`,
|
|
303
|
+
transcriptPath: null,
|
|
304
|
+
totalBytes: 0,
|
|
305
|
+
entryCount: 0,
|
|
306
|
+
groupCount: 0,
|
|
307
|
+
topN: 0,
|
|
308
|
+
entries: []
|
|
309
|
+
}, [], ['context-audit is a read-only probe; continue without it.']), opts.json);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
257
312
|
// v3.1.2 Step 0.8 — Mechanical PreToolUse gate.
|
|
258
313
|
// Wire-installed by `peaks workspace init` (extends the existing hook
|
|
259
314
|
// installer). Exit code is the load-bearing contract:
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { type ProgramIO } from '../../cli-helpers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
5
|
+
* the doctor envelope. The `checks` array (one object per check, each with a
|
|
6
|
+
* full message) and the stale-binding instance list collapse to counts +
|
|
7
|
+
* names-of-first-N; the summary counters and log section are scalars and are
|
|
8
|
+
* kept verbatim. ≤ 2 KB by construction.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildDoctorSummary(data: Record<string, unknown>): Record<string, unknown>;
|
|
3
11
|
export type BindingSource = 'canonical' | 'legacy';
|
|
4
12
|
export type DoctorLogsSection = {
|
|
5
13
|
logDir: string;
|
|
@@ -4,6 +4,7 @@ import { findProjectRoot } from '../../../services/config/config-safety.js';
|
|
|
4
4
|
import { loadSkillRegistry } from '../../../services/skills/skill-registry.js';
|
|
5
5
|
import { planStatusLineInstall } from '../../../services/skills/statusline-settings-service.js';
|
|
6
6
|
import { addJsonOption, printResult } from '../../cli-helpers.js';
|
|
7
|
+
import { boundedNames, fitSummaryToBytes } from '../../../services/context/summary-view.js';
|
|
7
8
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
8
9
|
// slice-3b Option C: the doctor subpackage owns the check pipeline but
|
|
9
10
|
// does NOT import cross-domain utils from the main package (avoids
|
|
@@ -38,6 +39,43 @@ function statusLineAlreadyInstalledForScope(scope, projectRoot) {
|
|
|
38
39
|
return false;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
44
|
+
* the doctor envelope. The `checks` array (one object per check, each with a
|
|
45
|
+
* full message) and the stale-binding instance list collapse to counts +
|
|
46
|
+
* names-of-first-N; the summary counters and log section are scalars and are
|
|
47
|
+
* kept verbatim. ≤ 2 KB by construction.
|
|
48
|
+
*/
|
|
49
|
+
export function buildDoctorSummary(data) {
|
|
50
|
+
const checks = Array.isArray(data.checks) ? data.checks : [];
|
|
51
|
+
const label = (c) => {
|
|
52
|
+
const id = typeof c.id === 'string' ? c.id : 'unknown';
|
|
53
|
+
if (c.ok === true)
|
|
54
|
+
return `ok ${id}`;
|
|
55
|
+
return `${c.severity === 'warning' ? 'warn' : 'FAIL'} ${id}`;
|
|
56
|
+
};
|
|
57
|
+
const failed = checks.filter((c) => c.ok === false);
|
|
58
|
+
const stale = typeof data.staleBinding === 'object' && data.staleBinding !== null
|
|
59
|
+
? data.staleBinding
|
|
60
|
+
: null;
|
|
61
|
+
const view = {
|
|
62
|
+
view: 'summary',
|
|
63
|
+
summary: data.summary,
|
|
64
|
+
checks: boundedNames(checks.map(label)),
|
|
65
|
+
failed: boundedNames(failed.map((c) => `${typeof c.id === 'string' ? c.id : 'unknown'}: ${typeof c.message === 'string' ? c.message : ''}`)),
|
|
66
|
+
};
|
|
67
|
+
if (stale !== null) {
|
|
68
|
+
view.staleBinding = {
|
|
69
|
+
ttlMs: stale.ttlMs,
|
|
70
|
+
staleCount: stale.staleCount,
|
|
71
|
+
droppedCount: stale.droppedCount,
|
|
72
|
+
droppedSids: boundedNames(Array.isArray(stale.droppedSids) ? stale.droppedSids.map((s) => String(s)) : []),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (data.logs !== undefined)
|
|
76
|
+
view.logs = data.logs;
|
|
77
|
+
return fitSummaryToBytes(view);
|
|
78
|
+
}
|
|
41
79
|
function doctorStatusLineInstalledProbe() {
|
|
42
80
|
const projectRoot = findProjectRoot(process.cwd());
|
|
43
81
|
// Check both scopes: a user may have installed the statusLine globally, which
|
|
@@ -136,7 +174,8 @@ export function registerDoctorCommand(program, io) {
|
|
|
136
174
|
// scan) addressable for non-current projects. The doctor was
|
|
137
175
|
// hardcoded to findProjectRoot(process.cwd()) which is the
|
|
138
176
|
// wrong default for users inspecting a sibling project.
|
|
139
|
-
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
177
|
+
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
178
|
+
.option('--summary', 'JSON envelope only: emit check counts + names-of-first-N (≤ 2 KB) instead of the full checks/stale-binding arrays; the default envelope is unchanged')).action(async (options) => {
|
|
140
179
|
// v2.18.2 cycle 2 (Q2 arbitration): --rebuild-binding and
|
|
141
180
|
// --cleanup-stale BOTH mutate the binding file. Running them
|
|
142
181
|
// together is ambiguous (rebuild rewrites callerIds; cleanup
|
|
@@ -209,9 +248,12 @@ export function registerDoctorCommand(program, io) {
|
|
|
209
248
|
droppedCount: droppedStale.length,
|
|
210
249
|
droppedSids: droppedStale
|
|
211
250
|
};
|
|
212
|
-
const
|
|
251
|
+
const fullData = logsSection === null
|
|
213
252
|
? { ...report, staleBinding: staleBindingSection }
|
|
214
253
|
: { ...report, logs: logsSection, staleBinding: staleBindingSection };
|
|
254
|
+
// Slice B: `--summary` is opt-in and affects the JSON envelope only (the
|
|
255
|
+
// human-readable path below already prints one line per check).
|
|
256
|
+
const data = options.summary === true ? buildDoctorSummary(fullData) : fullData;
|
|
215
257
|
// Slice 2026-08-05-statusline-sid-only-marker-and-multi-binary-drift-guard
|
|
216
258
|
// repair cycle: `report.summary.ok` already factors in the
|
|
217
259
|
// severity-aware aggregation in `buildReport` (warnings do NOT
|
|
@@ -53,6 +53,7 @@ export function registerMemoryCommand(program, io) {
|
|
|
53
53
|
.option('--kind <kind>', `filter by memory kind (one of: ${KIND_HELP})`)
|
|
54
54
|
.option('--pick', 'spawn fzf for interactive multi-select (requires fzf >= 0.38); writes picked.json')
|
|
55
55
|
.option('--fzf-bin <path>', 'override fzf binary path (default: fzf on PATH)', 'fzf')
|
|
56
|
+
.option('--summary', 'emit counts + names-of-first-N only (≤ 2 KB) instead of the full entry array; the default envelope is unchanged')
|
|
56
57
|
.option('--project <path>', 'target project root (defaults to git root or cwd)')).action((options) => {
|
|
57
58
|
void import('../memory-commands.js').then(({ runMemoryList }) => {
|
|
58
59
|
void runMemoryList(io, {
|
|
@@ -60,6 +61,7 @@ export function registerMemoryCommand(program, io) {
|
|
|
60
61
|
...(options.pick === true ? { pick: true } : {}),
|
|
61
62
|
...(options.fzfBin ? { fzfBin: options.fzfBin } : {}),
|
|
62
63
|
...(options.project !== undefined ? { project: options.project } : {}),
|
|
64
|
+
...(options.summary === true ? { summary: true } : {}),
|
|
63
65
|
...(options.json !== undefined ? { json: options.json } : {}),
|
|
64
66
|
});
|
|
65
67
|
}).catch((error) => {
|
|
@@ -79,12 +81,14 @@ export function registerMemoryCommand(program, io) {
|
|
|
79
81
|
.description('Rebuild .peaks/memory/index.json from every memory file on disk and regenerate MEMORY.md; reports unclassified files and orphans both ways. Dry-run by default; pass --apply to write.')
|
|
80
82
|
.option('--project <path>', 'target project root (defaults to git root or cwd)')
|
|
81
83
|
.option('--dry-run', 'report drift without writing (default)')
|
|
82
|
-
.option('--apply', 'rebuild index.json and regenerate MEMORY.md')
|
|
84
|
+
.option('--apply', 'rebuild index.json and regenerate MEMORY.md')
|
|
85
|
+
.option('--summary', 'emit drift counts + names-of-first-N only (≤ 2 KB) instead of the full arrays; the default envelope is unchanged')).action((options) => {
|
|
83
86
|
void import('../memory-commands.js').then(({ runMemoryReindex }) => {
|
|
84
87
|
void runMemoryReindex(io, {
|
|
85
88
|
...(options.project !== undefined ? { project: options.project } : {}),
|
|
86
89
|
...(options.dryRun === true ? { dryRun: true } : {}),
|
|
87
90
|
...(options.apply === true ? { apply: true } : {}),
|
|
91
|
+
...(options.summary === true ? { summary: true } : {}),
|
|
88
92
|
...(options.json !== undefined ? { json: options.json } : {}),
|
|
89
93
|
});
|
|
90
94
|
}).catch((error) => {
|
|
@@ -35,9 +35,10 @@ import { DEFAULT_PREFERENCES } from '../../services/preferences/preferences-type
|
|
|
35
35
|
import { writeLogEntry } from '../../services/log/logger.js';
|
|
36
36
|
import { PROMPT_LIMIT_BYTES, RECOMMENDED_ROLES, validateRole } from './sub-agent-shared.js';
|
|
37
37
|
import { runDispatchFromDag } from './dispatch-from-dag.js';
|
|
38
|
-
import { TEST_TOOL_DETECTION_BLOCK
|
|
38
|
+
import { TEST_TOOL_DETECTION_BLOCK } from '../../services/dispatch/test-tool-detection.js';
|
|
39
39
|
import { MemoryPreflightService, deriveMemoryQuery } from '../../services/context/memory-preflight-service.js';
|
|
40
40
|
import { buildDispatchSystemPrompt } from '../../services/context/build-dispatch-system-prompt.js';
|
|
41
|
+
import { readSessionCapsule } from '../../services/dispatch/session-capsule.js';
|
|
41
42
|
import { computeUiLibraryDispatchBlock } from '../../services/standards/ui-library-dispatch-block.js';
|
|
42
43
|
import { readFreshContextBlock } from '../../services/fresh-context/fresh-context-block.js';
|
|
43
44
|
import { createDispatchProvenanceToken, DISPATCH_PROVENANCE_ENV, writeDispatchProvenance, } from '../../services/worktree/dispatch-provenance.js';
|
|
@@ -443,10 +444,17 @@ export function registerDispatchCommand(parent, io) {
|
|
|
443
444
|
// "no probe available" hint instead of failing the
|
|
444
445
|
// dispatch.
|
|
445
446
|
}
|
|
447
|
+
// Slice 2026-09-10-dispatch-token-and-swarm §4: advisory background
|
|
448
|
+
// capsule published by the orchestrator via `peaks sub-agent share`.
|
|
449
|
+
// Absent capsule → no pointer and no precedence line (byte-identical
|
|
450
|
+
// legacy prompt).
|
|
451
|
+
const capsuleRef = readSessionCapsule({ projectRoot, sid, rid });
|
|
446
452
|
const memoryAugmentedBody = buildDispatchSystemPrompt({
|
|
447
453
|
taskTitle: role,
|
|
448
454
|
taskBody: options.prompt,
|
|
449
455
|
memoryBlock,
|
|
456
|
+
// 2026-09-10-dispatch-block-d (Option D): the composer owns the ONE
|
|
457
|
+
// unified Test Tool Detection injection for every role.
|
|
450
458
|
contextProbe,
|
|
451
459
|
// exactOptionalPropertyTypes: only set codegraphBlock when the rd
|
|
452
460
|
// preflight actually produced a value (null = attempted-unavailable,
|
|
@@ -459,7 +467,11 @@ export function registerDispatchCommand(parent, io) {
|
|
|
459
467
|
// Same optionality contract for the fresh-context block: set only
|
|
460
468
|
// when the rd/prd read ran (null = no block on disk → no block,
|
|
461
469
|
// undefined = non-rd/prd role → legacy prompt unchanged).
|
|
462
|
-
...(freshContextBlock !== undefined ? { freshContextBlock } : {})
|
|
470
|
+
...(freshContextBlock !== undefined ? { freshContextBlock } : {}),
|
|
471
|
+
// §4: advisory session capsule pointer + precedence line.
|
|
472
|
+
...(capsuleRef !== null
|
|
473
|
+
? { capsule: { batchId: capsuleRef.batchId, key: capsuleRef.key, bytes: capsuleRef.bytes } }
|
|
474
|
+
: {})
|
|
463
475
|
});
|
|
464
476
|
// Part 2.C: when --isolation worktree, prepend an isolation envelope
|
|
465
477
|
// block so the sub-agent sees the lease id + worktree path. The block
|
|
@@ -498,7 +510,7 @@ export function registerDispatchCommand(parent, io) {
|
|
|
498
510
|
`confirm the file exists. Anti-fake-green rule (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure §Lesson 1): ` +
|
|
499
511
|
`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`;
|
|
500
512
|
}
|
|
501
|
-
const effectivePrompt = `${
|
|
513
|
+
const effectivePrompt = `${memoryAugmentedBody}${isolationBlock}${mustLsFilesBlock}`;
|
|
502
514
|
const warnings = [...decision.warnings];
|
|
503
515
|
let toolCall;
|
|
504
516
|
try {
|
|
@@ -32,6 +32,7 @@ import { fail, ok } from 'peaks-loop-shared/result';
|
|
|
32
32
|
import { detectInstalledIde } from '../../services/ide/ide-detector.js';
|
|
33
33
|
import { getAdapter } from '../../services/ide/ide-registry.js';
|
|
34
34
|
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
35
|
+
import { planFileOverlapWaves } from '../../services/dispatch/file-overlap-wave-planner.js';
|
|
35
36
|
export async function runDispatchFromDag(role, options, asJson, io) {
|
|
36
37
|
if (!options.fromDag)
|
|
37
38
|
return;
|
|
@@ -135,6 +136,19 @@ export async function runDispatchFromDag(role, options, asJson, io) {
|
|
|
135
136
|
// end-to-end, but they are NOT surfaced in the CLI envelope (the LLM
|
|
136
137
|
// sees them only after re-invoking with fresh level-1 contracts).
|
|
137
138
|
const firstLevelIds = new Set(levelArr[0] ?? []);
|
|
139
|
+
// Slice 2026-09-10-dispatch-token-and-swarm §3: when EVERY first-level
|
|
140
|
+
// node declares `files`, refine the level into file-overlap waves so the
|
|
141
|
+
// LLM fans out without serializing on a shared file. Emitted additively
|
|
142
|
+
// in the envelope; the topological dispatch flow itself is unchanged.
|
|
143
|
+
// Nodes without `files` → `firstLevelWaves: null` (legacy envelope).
|
|
144
|
+
const firstLevelDescriptors = [...firstLevelIds].map((id) => ({
|
|
145
|
+
id,
|
|
146
|
+
files: dag.nodes.find((n) => n.id === id)?.files ?? []
|
|
147
|
+
}));
|
|
148
|
+
const firstLevelWaves = firstLevelDescriptors.length > 0
|
|
149
|
+
&& firstLevelDescriptors.every((d) => d.files.length > 0)
|
|
150
|
+
? planFileOverlapWaves(firstLevelDescriptors).waves
|
|
151
|
+
: null;
|
|
138
152
|
const emittedToolCalls = [];
|
|
139
153
|
const emittedSliceIds = [];
|
|
140
154
|
// CLI runner: emit the per-slice `buildToolCall` envelope, then return
|
|
@@ -240,6 +254,9 @@ export async function runDispatchFromDag(role, options, asJson, io) {
|
|
|
240
254
|
dispatchCount: emittedSliceIds.length,
|
|
241
255
|
levelsTotal: levelArr.length,
|
|
242
256
|
firstLevel: emittedSliceIds,
|
|
257
|
+
// §3: file-overlap wave plan for the first level (null when any
|
|
258
|
+
// first-level node omits `files`). Additive field.
|
|
259
|
+
firstLevelWaves,
|
|
243
260
|
toolCalls: emittedToolCalls,
|
|
244
261
|
existingContractCount: existingContracts.length,
|
|
245
262
|
expectedCompletionSeconds: 60,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type MemoryIndexEntry, type MemoryIndexSnapshot, type ProjectMemoryKind } from '../../services/memory/memory-search-service.js';
|
|
2
|
+
import { type MemoryReindexReport } from '../../services/memory/project-memory-service.js';
|
|
1
3
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
2
4
|
export interface MemorySearchCommandOptions {
|
|
3
5
|
query: string;
|
|
@@ -12,13 +14,35 @@ export interface MemoryListCommandOptions {
|
|
|
12
14
|
fzfBin?: string;
|
|
13
15
|
project?: string;
|
|
14
16
|
json?: boolean;
|
|
17
|
+
/** Slice B: emit counts + names-of-first-N instead of the full entry array. */
|
|
18
|
+
summary?: boolean;
|
|
15
19
|
}
|
|
16
20
|
export interface MemoryReindexCommandOptions {
|
|
17
21
|
project?: string;
|
|
18
22
|
dryRun?: boolean;
|
|
19
23
|
apply?: boolean;
|
|
20
24
|
json?: boolean;
|
|
25
|
+
/** Slice B: emit counts + names-of-first-N instead of the full arrays. */
|
|
26
|
+
summary?: boolean;
|
|
21
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
30
|
+
* `memory reindex`. The full report's arrays stay on disk / in the default
|
|
31
|
+
* envelope; this replaces them with `{count, names}` views (≤ 2 KB).
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildMemoryReindexSummary(report: MemoryReindexReport): Record<string, unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Slice B: bounded view of `memory list`. `count` is the true total; `names`
|
|
36
|
+
* carries `name (kind)` labels for the first N entries.
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildMemoryListSummary(data: {
|
|
39
|
+
snapshot: MemoryIndexSnapshot;
|
|
40
|
+
entries: readonly MemoryIndexEntry[];
|
|
41
|
+
kindFilter: ProjectMemoryKind | undefined;
|
|
42
|
+
pickedEntries: readonly MemoryIndexEntry[];
|
|
43
|
+
pickedOutputPath: string | null;
|
|
44
|
+
fzfVersion: string | null;
|
|
45
|
+
}): Record<string, unknown>;
|
|
22
46
|
export interface MemoryIngestCommandOptions {
|
|
23
47
|
project?: string;
|
|
24
48
|
sourceDir?: string;
|
|
@@ -2,6 +2,7 @@ import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
|
2
2
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
3
3
|
import { loadMemoryIndex, searchMemory } from '../../services/memory/memory-search-service.js';
|
|
4
4
|
import { executeMemoryReindex, VALID_PROJECT_MEMORY_KINDS } from '../../services/memory/project-memory-service.js';
|
|
5
|
+
import { boundedNames, fitSummaryToBytes } from '../../services/context/summary-view.js';
|
|
5
6
|
import { executeMemoryIngest } from '../../services/memory/memory-ingest-service.js';
|
|
6
7
|
import { executeMemoryRotate } from '../../services/memory/memory-rotate-service.js';
|
|
7
8
|
import { pickFromList } from '../../services/fuzzy-matching/fzf-pick-service.js';
|
|
@@ -9,6 +10,53 @@ import { fail, ok } from 'peaks-loop-shared/result';
|
|
|
9
10
|
import { getErrorMessage, printResult } from '../cli-helpers.js';
|
|
10
11
|
import { join } from 'node:path';
|
|
11
12
|
const VALID_KINDS = VALID_PROJECT_MEMORY_KINDS;
|
|
13
|
+
/**
|
|
14
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
15
|
+
* `memory reindex`. The full report's arrays stay on disk / in the default
|
|
16
|
+
* envelope; this replaces them with `{count, names}` views (≤ 2 KB).
|
|
17
|
+
*/
|
|
18
|
+
export function buildMemoryReindexSummary(report) {
|
|
19
|
+
const view = {
|
|
20
|
+
view: 'summary',
|
|
21
|
+
apply: report.apply,
|
|
22
|
+
projectRoot: report.projectRoot,
|
|
23
|
+
memoryDir: report.memoryDir,
|
|
24
|
+
indexPath: report.indexPath,
|
|
25
|
+
memoryMdPath: report.memoryMdPath,
|
|
26
|
+
scannedFiles: report.scannedFiles,
|
|
27
|
+
indexed: report.indexed,
|
|
28
|
+
indexedByKind: report.indexedByKind,
|
|
29
|
+
unclassified: boundedNames(report.unclassified.map((u) => `${u.name}${u.rawKind === null ? '' : ` (${u.rawKind})`}`)),
|
|
30
|
+
nameConflicts: boundedNames(report.nameConflicts.map((c) => c.name)),
|
|
31
|
+
orphanIndex: boundedNames(report.orphanIndex.map((o) => o.name)),
|
|
32
|
+
orphanDisk: boundedNames(report.orphanDisk.map((p) => p.split(/[\\/]/).pop() ?? p)),
|
|
33
|
+
memoryMd: report.memoryMd,
|
|
34
|
+
writtenFiles: boundedNames(report.writtenFiles.map((p) => p.split(/[\\/]/).pop() ?? p)),
|
|
35
|
+
};
|
|
36
|
+
return fitSummaryToBytes(view);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Slice B: bounded view of `memory list`. `count` is the true total; `names`
|
|
40
|
+
* carries `name (kind)` labels for the first N entries.
|
|
41
|
+
*/
|
|
42
|
+
export function buildMemoryListSummary(data) {
|
|
43
|
+
const label = (e) => `${e.name} (${e.kind})`;
|
|
44
|
+
const view = {
|
|
45
|
+
view: 'summary',
|
|
46
|
+
indexPath: data.snapshot.indexPath,
|
|
47
|
+
version: data.snapshot.version,
|
|
48
|
+
updatedAt: data.snapshot.updatedAt,
|
|
49
|
+
total: data.entries.length,
|
|
50
|
+
kindFilter: data.kindFilter ?? null,
|
|
51
|
+
entries: boundedNames(data.entries.map(label)),
|
|
52
|
+
};
|
|
53
|
+
if (data.pickedOutputPath !== null) {
|
|
54
|
+
view.picked = boundedNames(data.pickedEntries.map(label));
|
|
55
|
+
view.pickedOutputPath = data.pickedOutputPath;
|
|
56
|
+
view.fzfVersion = data.fzfVersion;
|
|
57
|
+
}
|
|
58
|
+
return fitSummaryToBytes(view);
|
|
59
|
+
}
|
|
12
60
|
function resolveMemoryProjectRoot(project) {
|
|
13
61
|
return project !== undefined
|
|
14
62
|
? resolveCanonicalProjectRoot(project)
|
|
@@ -63,15 +111,28 @@ export async function runMemoryList(io, options) {
|
|
|
63
111
|
if (entries.length === 0) {
|
|
64
112
|
nextActions.push('No entries match; run `peaks memory extract` to build the index from memory/*.md files.');
|
|
65
113
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
114
|
+
// Slice B: `--summary` swaps the full entry array for a bounded
|
|
115
|
+
// `{count, names}` view. The default (no flag) envelope is byte-identical
|
|
116
|
+
// to before — the flag is strictly opt-in.
|
|
117
|
+
const data = options.summary === true
|
|
118
|
+
? buildMemoryListSummary({
|
|
119
|
+
snapshot,
|
|
120
|
+
entries,
|
|
121
|
+
kindFilter,
|
|
122
|
+
pickedEntries,
|
|
123
|
+
pickedOutputPath,
|
|
124
|
+
fzfVersion,
|
|
125
|
+
})
|
|
126
|
+
: {
|
|
127
|
+
indexPath: snapshot.indexPath,
|
|
128
|
+
version: snapshot.version,
|
|
129
|
+
updatedAt: snapshot.updatedAt,
|
|
130
|
+
total: entries.length,
|
|
131
|
+
kindFilter: kindFilter ?? null,
|
|
132
|
+
entries,
|
|
133
|
+
...(options.pick === true ? { picked: pickedEntries, pickedOutputPath, fzfVersion } : {})
|
|
134
|
+
};
|
|
135
|
+
printResult(io, ok('memory.list', data, warnings, nextActions), options.json);
|
|
75
136
|
}
|
|
76
137
|
catch (error) {
|
|
77
138
|
const message = getErrorMessage(error);
|
|
@@ -149,7 +210,13 @@ export async function runMemoryReindex(io, options) {
|
|
|
149
210
|
if (report.nameConflicts.length > 0) {
|
|
150
211
|
nextActions.push(`${report.nameConflicts.length} name collision(s) across files; both entries are kept — rename one file to disambiguate.`);
|
|
151
212
|
}
|
|
152
|
-
|
|
213
|
+
// Slice B: `--summary` keeps the scalar drift counts + names-of-first-N;
|
|
214
|
+
// the full report (with every unclassified/orphan path) stays available
|
|
215
|
+
// by omitting the flag. Default shape is unchanged.
|
|
216
|
+
const data = options.summary === true
|
|
217
|
+
? buildMemoryReindexSummary(report)
|
|
218
|
+
: report;
|
|
219
|
+
printResult(io, ok('memory.reindex', data, [], nextActions), options.json);
|
|
153
220
|
}
|
|
154
221
|
catch (error) {
|
|
155
222
|
const message = getErrorMessage(error);
|
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { type RequestArtifactSummary } from '../../services/artifacts/request-artifact-service.js';
|
|
2
3
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
6
|
+
* `request list`. `count` is the true total; `names` carries
|
|
7
|
+
* `role/requestId (state)` labels for the first N entries. The full `items`
|
|
8
|
+
* array (with paths + timestamps) is one flag away — omit `--summary`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function buildRequestListSummary(items: readonly RequestArtifactSummary[]): Record<string, unknown>;
|
|
3
11
|
export declare function registerRequestCommands(program: Command, io: ProgramIO): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InvalidArgumentError } from 'commander';
|
|
2
2
|
import { createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact, PrerequisitesNotSatisfiedError, LintGateError, TypeSanityViolationError, FileSizeViolationError, VALID_REQUEST_TYPES } from '../../services/artifacts/request-artifact-service.js';
|
|
3
|
+
import { boundedNames, fitSummaryToBytes } from '../../services/context/summary-view.js';
|
|
3
4
|
import { applyPerArtifactFormat, inferArtifactName, parseRequestType, parseRole, parseStateForRole, resolveDefaultFormat, VALID_ROLES, } from './request-format-helpers.js';
|
|
4
5
|
import { ConfirmationRequiredError } from '../../services/mode/mode-enforcement.js';
|
|
5
6
|
import { recordBypass, isBypassLimitReached, MAX_BYPASSES_PER_SESSION } from '../../services/mode/bypass-tracker.js';
|
|
@@ -9,6 +10,20 @@ import { fail, ok } from 'peaks-loop-shared/result';
|
|
|
9
10
|
import { triggerBestPracticeScan } from '../../services/prd/best-practice-auto-trigger.js';
|
|
10
11
|
import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
|
|
11
12
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
13
|
+
/**
|
|
14
|
+
* Slice 2026-09-10-context-audit-and-discipline (Slice B): bounded view of
|
|
15
|
+
* `request list`. `count` is the true total; `names` carries
|
|
16
|
+
* `role/requestId (state)` labels for the first N entries. The full `items`
|
|
17
|
+
* array (with paths + timestamps) is one flag away — omit `--summary`.
|
|
18
|
+
*/
|
|
19
|
+
export function buildRequestListSummary(items) {
|
|
20
|
+
const view = {
|
|
21
|
+
view: 'summary',
|
|
22
|
+
count: items.length,
|
|
23
|
+
items: boundedNames(items.map((i) => `${i.role}/${i.requestId} (${i.state})`)),
|
|
24
|
+
};
|
|
25
|
+
return fitSummaryToBytes(view);
|
|
26
|
+
}
|
|
12
27
|
export function registerRequestCommands(program, io) {
|
|
13
28
|
const request = program.command('request').description('Manage per-request Peaks role artifacts (PRD / UI / RD / QA)');
|
|
14
29
|
addJsonOption(request
|
|
@@ -87,7 +102,8 @@ export function registerRequestCommands(program, io) {
|
|
|
87
102
|
.description('List per-request artifacts under a project workspace')
|
|
88
103
|
.requiredOption('--project <path>', 'target project root')
|
|
89
104
|
.option('--session-id <session>', 'limit to a specific session id')
|
|
90
|
-
.option('--role <role>', `limit to a single role (${VALID_ROLES.join(' | ')})`, parseRole)
|
|
105
|
+
.option('--role <role>', `limit to a single role (${VALID_ROLES.join(' | ')})`, parseRole)
|
|
106
|
+
.option('--summary', 'emit counts + names-of-first-N only (≤ 2 KB) instead of the full item array; the default envelope is unchanged')).action(async (options) => {
|
|
91
107
|
try {
|
|
92
108
|
const listOptions = { projectRoot: options.project };
|
|
93
109
|
if (options.sessionId !== undefined) {
|
|
@@ -97,7 +113,12 @@ export function registerRequestCommands(program, io) {
|
|
|
97
113
|
listOptions.role = options.role;
|
|
98
114
|
}
|
|
99
115
|
const items = await listRequestArtifacts(listOptions);
|
|
100
|
-
|
|
116
|
+
// Slice B: `--summary` is opt-in; the default `{count, items}` shape is
|
|
117
|
+
// byte-identical to before.
|
|
118
|
+
const data = options.summary === true
|
|
119
|
+
? buildRequestListSummary(items)
|
|
120
|
+
: { count: items.length, items };
|
|
121
|
+
printResult(io, ok('request.list', data), options.json);
|
|
101
122
|
}
|
|
102
123
|
catch (error) {
|
|
103
124
|
printResult(io, fail('request.list', 'REQUEST_LIST_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Check project path before retrying']), options.json);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerDispatchCommand } from './dispatch-commands.js';
|
|
2
2
|
import { registerHeartbeatCommand } from './heartbeat-commands.js';
|
|
3
3
|
import { registerShareCommand, registerSharedReadCommand, registerAwaitCommand, registerFinalizeCommand } from './share-commands.js';
|
|
4
|
+
import { registerWavePlanCommand } from './wave-plan-commands.js';
|
|
4
5
|
// Re-export `validateRole` for backward compat — the integration test
|
|
5
6
|
// suite and any external callers still import it from this entry file.
|
|
6
7
|
// The canonical implementation now lives in `sub-agent-shared.ts`.
|
|
@@ -17,4 +18,5 @@ export function registerSubAgentCommands(program, io) {
|
|
|
17
18
|
registerSharedReadCommand(subAgent, io);
|
|
18
19
|
registerAwaitCommand(subAgent, io);
|
|
19
20
|
registerFinalizeCommand(subAgent, io); // D21
|
|
21
|
+
registerWavePlanCommand(subAgent, io); // §3 file-overlap wave planner
|
|
20
22
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks sub-agent wave-plan` — slice 2026-09-10-dispatch-token-and-swarm §3.
|
|
3
|
+
*
|
|
4
|
+
* Nested under the existing `sub-agent` verb (no new top-level verb, per the
|
|
5
|
+
* project-level rule that users never learn a new CLI surface; the LLM runs
|
|
6
|
+
* this on their behalf).
|
|
7
|
+
*
|
|
8
|
+
* Input: slice descriptors `{ slices: [{ id, files: [...] }] }` from a JSON
|
|
9
|
+
* file (`--slices <file>`) or inline (`--slices-json '<json>'`).
|
|
10
|
+
*
|
|
11
|
+
* Output: a machine-readable wave plan where every wave's slices have
|
|
12
|
+
* pairwise-disjoint file sets, plus the per-slice deferral reason naming the
|
|
13
|
+
* colliding file. The orchestrator uses this to fan out a level in parallel
|
|
14
|
+
* WITHOUT serializing on a shared file — the deferred slices simply run in
|
|
15
|
+
* the next wave.
|
|
16
|
+
*/
|
|
17
|
+
import type { Command } from 'commander';
|
|
18
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
19
|
+
export interface WavePlanOptions {
|
|
20
|
+
slices?: string;
|
|
21
|
+
slicesJson?: string;
|
|
22
|
+
json?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function registerWavePlanCommand(parent: Command, io: ProgramIO): void;
|