peaks-loop 4.0.29 → 4.0.30
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 +14 -0
- package/dist/cli/commands/_register.js +4 -0
- package/dist/cli/commands/dispatch-commands.d.ts +5 -41
- package/dist/cli/commands/dispatch-commands.js +35 -185
- package/dist/cli/commands/evidence-commands.d.ts +12 -0
- package/dist/cli/commands/evidence-commands.js +43 -0
- package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
- package/dist/cli/commands/fresh-context-commands.js +24 -0
- package/dist/cli/commands/request-commands.js +2 -96
- package/dist/cli/commands/request-format-helpers.d.ts +19 -0
- package/dist/cli/commands/request-format-helpers.js +102 -0
- package/dist/cli/commands/worktree-auth-commands.js +2 -472
- package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
- package/dist/cli/commands/worktree-lease-commands.js +500 -0
- package/dist/services/artifacts/request-artifact-service.js +13 -3
- package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
- package/dist/services/code/auto-compact-lifecycle.js +235 -0
- package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
- package/dist/services/code/auto-compact-orchestrator.js +1 -222
- package/dist/services/context/build-dispatch-system-prompt.d.ts +26 -0
- package/dist/services/context/build-dispatch-system-prompt.js +38 -3
- package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
- package/dist/services/dispatch/dispatch-record-types.js +15 -0
- package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
- package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
- package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
- package/dist/services/dispatch/dispatch-record-writer.js +3 -245
- package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
- package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
- package/dist/services/dispatch/isolation-lease.d.ts +43 -0
- package/dist/services/dispatch/isolation-lease.js +129 -0
- package/dist/services/evidence/evidence-generator.d.ts +26 -0
- package/dist/services/evidence/evidence-generator.js +349 -0
- package/dist/services/fresh-context/config.d.ts +1 -0
- package/dist/services/fresh-context/config.js +13 -0
- package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
- package/dist/services/fresh-context/fresh-context-block.js +40 -0
- package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
- package/dist/services/fresh-context/trigger-scan.js +29 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
- package/dist/services/skills/hooks-settings-service.d.ts +2 -64
- package/dist/services/skills/hooks-settings-service.js +2 -215
- package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
- package/dist/services/skills/skill-statusline-renderer.js +1 -184
- package/dist/services/skills/statusline-palette.d.ts +62 -0
- package/dist/services/skills/statusline-palette.js +190 -0
- package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
- package/dist/services/slice/slice-decompose-import-edges.js +102 -0
- package/dist/services/slice/slice-decompose-service.js +3 -198
- package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
- package/dist/services/slice/slice-decompose-tarjan.js +108 -0
- package/dist/services/standards/project-standards-service.d.ts +1 -9
- package/dist/services/standards/project-standards-service.js +3 -232
- package/dist/services/standards/standards-render.d.ts +23 -0
- package/dist/services/standards/standards-render.js +238 -0
- package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
- package/dist/services/standards/ui-library-dispatch-block.js +48 -0
- package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
- package/dist/services/workspace/reconcile-migrate.js +230 -0
- package/dist/services/workspace/reconcile-service.d.ts +0 -72
- package/dist/services/workspace/reconcile-service.js +3 -220
- package/dist/services/workspace/workspace-service.js +13 -2
- package/dist/shared/incrementing-number.d.ts +11 -0
- package/dist/shared/incrementing-number.js +16 -3
- package/package.json +5 -5
- package/skills/peaks-code/SKILL.md +6 -2
- package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
- package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.30 — 2026-09-07 (搜索优先 preflight + 超行文件拆分 + CLI 漂移修复)
|
|
4
|
+
|
|
5
|
+
**Highlights**:
|
|
6
|
+
|
|
7
|
+
1. **搜索优先 preflight(fresh-context)** — 编排层新增信号触发的「先搜再答」:`peaks fresh-context preflight` 做确定性信号扫描,命中则搜 Context7+WebSearch,合成 ≤5 条「绑定指令」(用 X / 别用 Y / 因为 Z)注入 PRD/RD 派发提示词,对冲模型训练数据 2 个月滞后。带权威框架 + 全局 kill-switch + fail-soft 降级 + job 内缓存。
|
|
8
|
+
|
|
9
|
+
2. **9 个超行文件拆分** — `src/` 全部源文件降至 ≤800 行(Karpathy §2 红线):dispatch-record-writer 1376→774、dispatch-commands 961→752、worktree-auth-commands 1032→312、slice-decompose-service 1014→765、auto-compact-orchestrator 999→756、hooks-settings-service 875→642、skill-statusline-renderer 873→646、project-standards-service 862→644、reconcile-service 861→649、request-commands 806→712。
|
|
10
|
+
|
|
11
|
+
3. **CLI 漂移修复** — 修复 request-id 大小写敏感(buildNumberedFilename 小写化 slug vs lookup case-sensitive)与子代理覆盖 session 绑定两个 bug;补全 `peaks sub-agent dispatch` 强制 `--graph-node` 的文档。
|
|
12
|
+
|
|
13
|
+
4. **`peaks evidence generate`** — 一键生成 slice 的 11 个证据工件,收敛编排仪式开销。
|
|
14
|
+
|
|
15
|
+
5. **UI 组件库优先注入** — RD/UI 派发提示词直接注入检测到的组件库(`## Project stack` 块),前端开发优先用已装组件库而非原生 DOM。
|
|
16
|
+
|
|
3
17
|
## 4.0.29 — 2026-09-03 (codegraph 生命周期闭环 — pre-read + auto-refresh)
|
|
4
18
|
|
|
5
19
|
**3 atomic commits from session 2026-09-03-session-d49394** (user feedback #3):
|
|
@@ -19,10 +19,12 @@ import { registerDocCommands } from './doc-commands.js';
|
|
|
19
19
|
import { registerE2EVerifyCommand } from './e2e-verify.js';
|
|
20
20
|
import { registerEccCommands } from './ecc-commands.js';
|
|
21
21
|
import { registerEvolutionCommands } from './evolution-commands.js';
|
|
22
|
+
import { registerEvidenceCommands } from './evidence-commands.js';
|
|
22
23
|
import { registerFeedbackCommands } from './feedback-commands.js';
|
|
23
24
|
import { registerFinalReviewCommands } from './final-review-commands.js';
|
|
24
25
|
import { registerFixtureCommands } from './fixture-commands.js';
|
|
25
26
|
import { registerForkCommands } from './fork-commands.js';
|
|
27
|
+
import { registerFreshContextCommands } from './fresh-context-commands.js';
|
|
26
28
|
import { registerGateCommands } from './gate-commands.js';
|
|
27
29
|
import { registerClassifyCommands, registerContractCommands } from './governance-classify-contract-commands.js';
|
|
28
30
|
import { registerHookHandleCommand } from './hook-handle.js';
|
|
@@ -117,6 +119,8 @@ const REGISTRATIONS = [
|
|
|
117
119
|
['skill-loop-engineering-readiness-commands', registerSkillLoopEngineeringReadinessCommands],
|
|
118
120
|
['loop-commands', registerLoopCommands], ['loop-eval-commands', registerWorkflowEvalCommands],
|
|
119
121
|
['evolution-commands', registerEvolutionCommands], ['asset-commands', registerAssetCommands],
|
|
122
|
+
['evidence-commands', registerEvidenceCommands],
|
|
123
|
+
['fresh-context-commands', registerFreshContextCommands],
|
|
120
124
|
['bee-commands', registerBeeCommands], ['ecc-commands', registerEccCommands],
|
|
121
125
|
['baseline-commands', registerBaselineCommands],
|
|
122
126
|
['upgrade-commands', registerUpgradeCommands], ['code-review-commands', registerCodeReviewCommands],
|
|
@@ -2,45 +2,9 @@ import type { Command } from 'commander';
|
|
|
2
2
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
3
3
|
export declare function registerDispatchCommand(parent: Command, io: ProgramIO): void;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Why `git ls-files` and not `fs.glob`: the anti-fake-green contract
|
|
12
|
-
* is "the file the sub-agent claims to have written must ACTUALLY be
|
|
13
|
-
* tracked by git" — `git ls-files` enforces that contract; `fs.glob`
|
|
14
|
-
* would happily return untracked-but-on-disk files (false-positive
|
|
15
|
-
* for the fake-green gate).
|
|
16
|
-
*
|
|
17
|
-
* Failure modes (best-effort, never throws):
|
|
18
|
-
* - git not on PATH → empty array (`ENOENT` swallowed)
|
|
19
|
-
* - not a git repo → empty array (git exits non-zero)
|
|
20
|
-
* - glob matches zero tracked files → empty array
|
|
21
|
-
*
|
|
22
|
-
* Exported for unit-test access (`tests/unit/sub-agent/must-ls-files-flag.test.ts`).
|
|
23
|
-
* The export is intentional — the helper has zero side effects and
|
|
24
|
-
* keeps the dispatch action handler small.
|
|
5
|
+
* 2.7.0 slice-dag-dispatcher MVP — see `dispatch-from-dag.ts`.
|
|
6
|
+
* The function was pulled out of this file in slice 2026-06-23-audit-3rd
|
|
7
|
+
* #7 to honor the 800-line file cap and isolate the three heavy
|
|
8
|
+
* module loads (slice-dag / dag-orchestrator / contract-store) to the
|
|
9
|
+
* --from-dag codepath only.
|
|
25
10
|
*/
|
|
26
|
-
export declare function runGitLsFiles(projectRoot: string, glob: string): readonly string[];
|
|
27
|
-
/**
|
|
28
|
-
* `dispatchSubAgent` is the thin programmatic wrapper the integration
|
|
29
|
-
* test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
|
|
30
|
-
* It enforces --graph-node required BEFORE any record write so a
|
|
31
|
-
* caller can't bypass the CLI's requiredOption guard. Throws a typed
|
|
32
|
-
* error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
|
|
33
|
-
*/
|
|
34
|
-
export declare function dispatchSubAgent(input: {
|
|
35
|
-
projectRoot: string;
|
|
36
|
-
role: string;
|
|
37
|
-
prompt: string;
|
|
38
|
-
sessionId?: string;
|
|
39
|
-
graphNode?: string;
|
|
40
|
-
workflowId?: string;
|
|
41
|
-
graphRef?: string;
|
|
42
|
-
}): Promise<{
|
|
43
|
-
role: string;
|
|
44
|
-
toolCall: unknown;
|
|
45
|
-
dispatchRecordPath: string | null;
|
|
46
|
-
}>;
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { existsSync } from 'node:fs';
|
|
18
18
|
import { randomUUID } from 'node:crypto';
|
|
19
|
-
import { spawn as childProcessSpawn } from 'node:child_process';
|
|
20
19
|
import { fail, getErrorMessage, ok } from 'peaks-loop-shared/result';
|
|
21
20
|
import { addJsonOption, printResult } from '../cli-helpers.js';
|
|
22
21
|
import { detectInstalledIde } from '../../services/ide/ide-detector.js';
|
|
@@ -39,7 +38,11 @@ import { runDispatchFromDag } from './dispatch-from-dag.js';
|
|
|
39
38
|
import { TEST_TOOL_DETECTION_BLOCK, formatTestToolDetection } from '../../services/dispatch/test-tool-detection.js';
|
|
40
39
|
import { MemoryPreflightService } from '../../services/context/memory-preflight-service.js';
|
|
41
40
|
import { buildDispatchSystemPrompt } from '../../services/context/build-dispatch-system-prompt.js';
|
|
41
|
+
import { computeUiLibraryDispatchBlock } from '../../services/standards/ui-library-dispatch-block.js';
|
|
42
|
+
import { readFreshContextBlock } from '../../services/fresh-context/fresh-context-block.js';
|
|
42
43
|
import { createDispatchProvenanceToken, DISPATCH_PROVENANCE_ENV, writeDispatchProvenance, } from '../../services/worktree/dispatch-provenance.js';
|
|
44
|
+
import { spawnWorktreeLease, spawnContainerLease } from '../../services/dispatch/isolation-lease.js';
|
|
45
|
+
import { runGitLsFiles } from '../../services/dispatch/dispatch-sub-agent.js';
|
|
43
46
|
export function registerDispatchCommand(parent, io) {
|
|
44
47
|
addJsonOption(parent
|
|
45
48
|
.command('dispatch')
|
|
@@ -391,6 +394,28 @@ export function registerDispatchCommand(parent, io) {
|
|
|
391
394
|
codegraphBlock = null; // fail-soft: never block RD dispatch
|
|
392
395
|
}
|
|
393
396
|
}
|
|
397
|
+
// Slice 2026-09-06-ui-lib-dispatch-priority: surface the detected
|
|
398
|
+
// component library (+ CSS framework + build tool) in the RD/UI
|
|
399
|
+
// dispatch system prompt so the sub-agent prefers the library over
|
|
400
|
+
// hand-rolled native DOM. Only injected for frontend-generating roles
|
|
401
|
+
// (rd + ui) — qa/sc/txt/prd keep the legacy prompt byte-identical.
|
|
402
|
+
// Fail-soft inside the wrapper: a detection error degrades to null.
|
|
403
|
+
let projectStackBlock;
|
|
404
|
+
if (role === 'rd' || role === 'ui') {
|
|
405
|
+
projectStackBlock = computeUiLibraryDispatchBlock(projectRoot);
|
|
406
|
+
}
|
|
407
|
+
// Slice 2026-09-07-search-first-preflight: read the orchestrator-
|
|
408
|
+
// synthesized fresh-context block for rd + prd dispatches. The block
|
|
409
|
+
// was written to .peaks/_runtime/<sid>/fresh-context.md when the
|
|
410
|
+
// preflight triggered; it carries ≤5 binding directives that hedge
|
|
411
|
+
// model training-data lag. Fail-soft: a missing / unreadable file, or
|
|
412
|
+
// a file without the `## Fresh context` heading, degrades to null so
|
|
413
|
+
// the prompt stays byte-identical to the legacy shape. Other roles
|
|
414
|
+
// (qa/sc/txt/ui) keep the legacy prompt unchanged.
|
|
415
|
+
let freshContextBlock;
|
|
416
|
+
if (role === 'rd' || role === 'prd') {
|
|
417
|
+
freshContextBlock = readFreshContextBlock(projectRoot, sid);
|
|
418
|
+
}
|
|
394
419
|
// Slice 2026-07-29-context-evaluation-accuracy: capture the
|
|
395
420
|
// authoritative context-fill probe before composing the
|
|
396
421
|
// dispatch prompt. The probe is token-counted (IDE adapter's
|
|
@@ -424,7 +449,15 @@ export function registerDispatchCommand(parent, io) {
|
|
|
424
449
|
// exactOptionalPropertyTypes: only set codegraphBlock when the rd
|
|
425
450
|
// preflight actually produced a value (null = attempted-unavailable,
|
|
426
451
|
// undefined = not requested → legacy prompt unchanged).
|
|
427
|
-
...(codegraphBlock !== undefined ? { codegraphBlock } : {})
|
|
452
|
+
...(codegraphBlock !== undefined ? { codegraphBlock } : {}),
|
|
453
|
+
// Same optionality contract for the project-stack block: set only
|
|
454
|
+
// when the rd/ui detection ran (null = no library → no block,
|
|
455
|
+
// undefined = non-frontend role → legacy prompt unchanged).
|
|
456
|
+
...(projectStackBlock !== undefined ? { projectStackBlock } : {}),
|
|
457
|
+
// Same optionality contract for the fresh-context block: set only
|
|
458
|
+
// when the rd/prd read ran (null = no block on disk → no block,
|
|
459
|
+
// undefined = non-rd/prd role → legacy prompt unchanged).
|
|
460
|
+
...(freshContextBlock !== undefined ? { freshContextBlock } : {})
|
|
428
461
|
});
|
|
429
462
|
// Part 2.C: when --isolation worktree, prepend an isolation envelope
|
|
430
463
|
// block so the sub-agent sees the lease id + worktree path. The block
|
|
@@ -701,186 +734,3 @@ export function registerDispatchCommand(parent, io) {
|
|
|
701
734
|
* module loads (slice-dag / dag-orchestrator / contract-store) to the
|
|
702
735
|
* --from-dag codepath only.
|
|
703
736
|
*/
|
|
704
|
-
/**
|
|
705
|
-
* Part 2.C (slice 2026-07-29-worktree-l2-extended) — spawn a worktree
|
|
706
|
-
* lease by shelling out to `peaks worktree spawn` (avoid re-implementing
|
|
707
|
-
* the lease-write + git-worktree-add sequence in this file). The CLI
|
|
708
|
-
* does the lease write, the git worktree add, AND the error handling;
|
|
709
|
-
* we just parse the JSON envelope and surface the leaseId + path.
|
|
710
|
-
*
|
|
711
|
-
* Throws on spawn failure; the caller converts the error to a
|
|
712
|
-
* ISOLATION_SPAWN_FAILED envelope. Synchronous wait is acceptable: the
|
|
713
|
-
* dispatch is already async, the lease is a few hundred ms of FS work,
|
|
714
|
-
* and we need the leaseId before we build the dispatch record.
|
|
715
|
-
*/
|
|
716
|
-
function spawnWorktreeLease(args) {
|
|
717
|
-
return new Promise((resolve, reject) => {
|
|
718
|
-
const child = childProcessSpawn(process.execPath, [
|
|
719
|
-
// The compiled CLI lives in dist/cli/peaks.js. We pass the entry
|
|
720
|
-
// through node so the test suite (which also runs on the same
|
|
721
|
-
// process) and the production binary share the same path. When
|
|
722
|
-
// the binary is invoked as `peaks`, the package bin stub does
|
|
723
|
-
// this for us; here we explicitly use process.execPath + the
|
|
724
|
-
// resolved entry to avoid PATH surprises.
|
|
725
|
-
process.argv[1] ?? '',
|
|
726
|
-
'worktree', 'spawn',
|
|
727
|
-
'--rid', args.rid,
|
|
728
|
-
'--role', args.role,
|
|
729
|
-
'--purpose', args.purpose,
|
|
730
|
-
'--project', args.projectRoot,
|
|
731
|
-
'--session', args.sessionId,
|
|
732
|
-
'--json'
|
|
733
|
-
], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
|
|
734
|
-
let stdout = '';
|
|
735
|
-
let stderr = '';
|
|
736
|
-
child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
|
|
737
|
-
child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
|
|
738
|
-
child.on('error', (err) => reject(new Error(`worktree spawn subprocess failed: ${err.message}`)));
|
|
739
|
-
child.on('close', (code) => {
|
|
740
|
-
if (code !== 0) {
|
|
741
|
-
reject(new Error(`peaks worktree spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
let parsed;
|
|
745
|
-
try {
|
|
746
|
-
parsed = JSON.parse(stdout);
|
|
747
|
-
}
|
|
748
|
-
catch (err) {
|
|
749
|
-
reject(new Error(`peaks worktree spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
if (typeof parsed !== 'object' || parsed === null) {
|
|
753
|
-
reject(new Error('peaks worktree spawn envelope is not an object'));
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
const env = parsed;
|
|
757
|
-
if (env.ok !== true || !env.data?.lease) {
|
|
758
|
-
reject(new Error(`peaks worktree spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
resolve({
|
|
762
|
-
leaseId: env.data.lease.leaseId,
|
|
763
|
-
path: env.data.lease.path,
|
|
764
|
-
branch: env.data.lease.branch,
|
|
765
|
-
expiresAt: env.data.lease.expiresAt
|
|
766
|
-
});
|
|
767
|
-
// Part 47: unref via setImmediate so the close handler
|
|
768
|
-
// finishes first and Node's stdio 'end' events drain the
|
|
769
|
-
// stdout/stderr buffers before the parent releases the
|
|
770
|
-
// child handle. Without this microtask defer, the unref
|
|
771
|
-
// races the buffered stdout close and the test receives
|
|
772
|
-
// an empty JSON envelope.
|
|
773
|
-
setImmediate(() => { child.unref(); });
|
|
774
|
-
});
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
/**
|
|
778
|
-
* Slice 2026-07-29-worktree-l2-extended Part 12: container
|
|
779
|
-
* isolation bridge. Shells out to `peaks container spawn` to
|
|
780
|
-
* run `docker run` + write the container lease. Returns the
|
|
781
|
-
* leaseId the dispatch record needs to persist. The shape is
|
|
782
|
-
* a subset of the spawnWorktreeLease return (just leaseId);
|
|
783
|
-
* we do not need the path/branch/expiresAt for the container
|
|
784
|
-
* path because the envelope surfaces a different set of
|
|
785
|
-
* fields (image + containerId; see container-lease.ts).
|
|
786
|
-
*/
|
|
787
|
-
function spawnContainerLease(args) {
|
|
788
|
-
return new Promise((resolve, reject) => {
|
|
789
|
-
const child = childProcessSpawn(process.execPath, [
|
|
790
|
-
process.argv[1] ?? '',
|
|
791
|
-
'container', 'spawn',
|
|
792
|
-
'--rid', args.rid,
|
|
793
|
-
'--role', args.role,
|
|
794
|
-
'--purpose', args.purpose,
|
|
795
|
-
'--project', args.projectRoot,
|
|
796
|
-
'--session', args.sessionId,
|
|
797
|
-
'--json'
|
|
798
|
-
], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: true });
|
|
799
|
-
let stdout = '';
|
|
800
|
-
let stderr = '';
|
|
801
|
-
child.stdout.on('data', (d) => { stdout += d.toString('utf8'); });
|
|
802
|
-
child.stderr.on('data', (d) => { stderr += d.toString('utf8'); });
|
|
803
|
-
child.on('error', (err) => reject(new Error(`container spawn subprocess failed: ${err.message}`)));
|
|
804
|
-
child.on('close', (code) => {
|
|
805
|
-
if (code !== 0) {
|
|
806
|
-
reject(new Error(`peaks container spawn exited ${code}; stderr: ${stderr.trim() || '(empty)'}`));
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
let parsed;
|
|
810
|
-
try {
|
|
811
|
-
parsed = JSON.parse(stdout);
|
|
812
|
-
}
|
|
813
|
-
catch (err) {
|
|
814
|
-
reject(new Error(`peaks container spawn produced unparseable JSON: ${err.message}; stdout: ${stdout.slice(0, 400)}`));
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
if (typeof parsed !== 'object' || parsed === null) {
|
|
818
|
-
reject(new Error('peaks container spawn envelope is not an object'));
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
const env = parsed;
|
|
822
|
-
if (env.ok !== true || !env.data?.lease) {
|
|
823
|
-
reject(new Error(`peaks container spawn envelope missing lease; got: ${stdout.slice(0, 200)}`));
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
resolve({ leaseId: env.data.lease.leaseId });
|
|
827
|
-
});
|
|
828
|
-
// See spawnWorktreeLease above for the rationale.
|
|
829
|
-
child.unref();
|
|
830
|
-
});
|
|
831
|
-
}
|
|
832
|
-
/**
|
|
833
|
-
* F5 follow-up (sediment 2026-08-11-rid-001-redo-fake-green-recovery-closure
|
|
834
|
-
* §Lesson 1): synchronous anti-fake-green file-existence gate. Runs
|
|
835
|
-
* `git ls-files <glob>` against `projectRoot` and returns the matching
|
|
836
|
-
* tracked file paths (relative to projectRoot). Empty array when no
|
|
837
|
-
* files match (e.g. untracked new file, wrong glob, not a git repo).
|
|
838
|
-
*
|
|
839
|
-
* Why `git ls-files` and not `fs.glob`: the anti-fake-green contract
|
|
840
|
-
* is "the file the sub-agent claims to have written must ACTUALLY be
|
|
841
|
-
* tracked by git" — `git ls-files` enforces that contract; `fs.glob`
|
|
842
|
-
* would happily return untracked-but-on-disk files (false-positive
|
|
843
|
-
* for the fake-green gate).
|
|
844
|
-
*
|
|
845
|
-
* Failure modes (best-effort, never throws):
|
|
846
|
-
* - git not on PATH → empty array (`ENOENT` swallowed)
|
|
847
|
-
* - not a git repo → empty array (git exits non-zero)
|
|
848
|
-
* - glob matches zero tracked files → empty array
|
|
849
|
-
*
|
|
850
|
-
* Exported for unit-test access (`tests/unit/sub-agent/must-ls-files-flag.test.ts`).
|
|
851
|
-
* The export is intentional — the helper has zero side effects and
|
|
852
|
-
* keeps the dispatch action handler small.
|
|
853
|
-
*/
|
|
854
|
-
export function runGitLsFiles(projectRoot, glob) {
|
|
855
|
-
try {
|
|
856
|
-
const { execFileSync } = require('node:child_process');
|
|
857
|
-
const stdout = execFileSync('git', ['ls-files', '--', glob], { cwd: projectRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
858
|
-
return stdout.split('\n').filter((line) => line.length > 0);
|
|
859
|
-
}
|
|
860
|
-
catch {
|
|
861
|
-
return [];
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
/* ---------- Slice 4.0.8 RD §4 D4c: programmatic dispatcher ---------- */
|
|
865
|
-
/**
|
|
866
|
-
* `dispatchSubAgent` is the thin programmatic wrapper the integration
|
|
867
|
-
* test (`tests/integration/sub-agent-graph-binding.test.ts`) imports.
|
|
868
|
-
* It enforces --graph-node required BEFORE any record write so a
|
|
869
|
-
* caller can't bypass the CLI's requiredOption guard. Throws a typed
|
|
870
|
-
* error with `code = PEAKS_GRAPH_NODE_REQUIRED` when missing.
|
|
871
|
-
*/
|
|
872
|
-
export async function dispatchSubAgent(input) {
|
|
873
|
-
if (typeof input.graphNode !== 'string' || input.graphNode.length === 0) {
|
|
874
|
-
const err = new Error('PEAKS_GRAPH_NODE_REQUIRED: --graph-node is required (RD §4 D4c)');
|
|
875
|
-
err.code = 'PEAKS_GRAPH_NODE_REQUIRED';
|
|
876
|
-
throw err;
|
|
877
|
-
}
|
|
878
|
-
// The integration test only checks the rejection path; the success
|
|
879
|
-
// path is exercised by the existing CLI command. Return a minimal
|
|
880
|
-
// stub so any future programmatic caller has a stable surface.
|
|
881
|
-
return {
|
|
882
|
-
role: input.role,
|
|
883
|
-
toolCall: null,
|
|
884
|
-
dispatchRecordPath: null,
|
|
885
|
-
};
|
|
886
|
-
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peaks evidence * CLI surface — mechanical per-slice evidence generation.
|
|
3
|
+
*
|
|
4
|
+
* `peaks evidence generate` writes the ~11 per-slice evidence artifacts under
|
|
5
|
+
* `.peaks/_runtime/<sid>/` so the orchestrator no longer hand-writes them for
|
|
6
|
+
* a mechanical file-split (or similar) slice. The body builders live in
|
|
7
|
+
* `services/evidence/evidence-generator.ts`; this module only wires the
|
|
8
|
+
* Commander surface + project/session resolution + JSON envelope.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
12
|
+
export declare function registerEvidenceCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
2
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
3
|
+
import { generateEvidence, parseFiles, parseLineCounts } from '../../services/evidence/evidence-generator.js';
|
|
4
|
+
import { getSessionId } from '../../services/session/session-manager.js';
|
|
5
|
+
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
6
|
+
export function registerEvidenceCommands(program, io) {
|
|
7
|
+
const evidence = program.command('evidence').description('Generate per-slice evidence artifacts for mechanical slices');
|
|
8
|
+
addJsonOption(evidence
|
|
9
|
+
.command('generate')
|
|
10
|
+
.description('Write the ~11 rd/qa/audit/prd evidence artifacts for a mechanical file-split slice under .peaks/_runtime/<sid>/')
|
|
11
|
+
.requiredOption('--rid <rid>', 'request id (e.g. 2026-09-06-split-dispatch-commands)')
|
|
12
|
+
.requiredOption('--title <title>', 'one-line description of the slice')
|
|
13
|
+
.requiredOption('--files <list>', 'comma-separated changed file paths')
|
|
14
|
+
.requiredOption('--line-counts <list>', 'semicolon-separated file=lines pairs (e.g. "f=120;g=300")')
|
|
15
|
+
.option('--session-id <sid>', 'session id; defaults to the project-bound session')).action(async (options) => {
|
|
16
|
+
try {
|
|
17
|
+
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
18
|
+
const sessionId = options.sessionId ?? getSessionId(projectRoot);
|
|
19
|
+
if (sessionId === null || sessionId === undefined || sessionId.length === 0) {
|
|
20
|
+
printResult(io, fail('evidence.generate', 'SESSION_NOT_BOUND', 'No --session-id was provided and no peaks session is bound for this project.', { projectRoot }, ['Pass --session-id <sid>, or run `peaks workspace init` first so a session directory exists.']), options.json);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const files = parseFiles(options.files ?? '');
|
|
25
|
+
const lineCounts = parseLineCounts(options.lineCounts ?? '');
|
|
26
|
+
const result = await generateEvidence({
|
|
27
|
+
projectRoot,
|
|
28
|
+
rid: options.rid ?? '',
|
|
29
|
+
title: options.title ?? '',
|
|
30
|
+
files,
|
|
31
|
+
lineCounts,
|
|
32
|
+
sessionId
|
|
33
|
+
});
|
|
34
|
+
printResult(io, ok('evidence.generate', result, [], [
|
|
35
|
+
`Wrote ${result.writtenFiles.length} evidence files under ${result.sessionRoot}`
|
|
36
|
+
]), options.json);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
printResult(io, fail('evidence.generate', 'EVIDENCE_GENERATE_FAILED', getErrorMessage(error), {}, ['Verify the target project is writable.']), options.json);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `peaks fresh-context preflight` CLI — deterministic trigger scan for the
|
|
3
|
+
* search-first preflight (slice 2026-09-07-search-first-preflight).
|
|
4
|
+
*
|
|
5
|
+
* This command is the shared, deterministic surface every peaks-* orchestrator
|
|
6
|
+
* calls at orchestration-start, BEFORE the first planning action, to decide
|
|
7
|
+
* whether to search fresh context (Context7 → WebSearch) and synthesize binding
|
|
8
|
+
* directives. The actual search + synthesis is done by the LLM per the shared
|
|
9
|
+
* `skills/peaks-code/references/fresh-context-preflight.md`; this command only
|
|
10
|
+
* runs the pure trigger scan (`services/fresh-context/trigger-scan.ts`) and the
|
|
11
|
+
* kill-switch read (`services/fresh-context/config.ts`).
|
|
12
|
+
*/
|
|
13
|
+
import type { Command } from 'commander';
|
|
14
|
+
import { type ProgramIO } from '../cli-helpers.js';
|
|
15
|
+
export declare function registerFreshContextCommands(program: Command, io: ProgramIO): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
2
|
+
import { fail, ok } from 'peaks-loop-shared/result';
|
|
3
|
+
import { scanFreshContextTrigger } from '../../services/fresh-context/trigger-scan.js';
|
|
4
|
+
import { isFreshContextEnabled } from '../../services/fresh-context/config.js';
|
|
5
|
+
export function registerFreshContextCommands(program, io) {
|
|
6
|
+
const freshContext = program
|
|
7
|
+
.command('fresh-context')
|
|
8
|
+
.description('Search-first preflight for the orchestration layer (hedge training-data lag)');
|
|
9
|
+
addJsonOption(freshContext
|
|
10
|
+
.command('preflight')
|
|
11
|
+
.description('Deterministic signal scan: report whether the prompt triggers a fresh-context search')
|
|
12
|
+
.requiredOption('--prompt <text>', 'the user request text to scan for trigger keywords')
|
|
13
|
+
.option('--project <root>', 'target project root (accepted for surface parity; the scan is text + config only)')).action((options) => {
|
|
14
|
+
try {
|
|
15
|
+
const enabled = isFreshContextEnabled();
|
|
16
|
+
const result = scanFreshContextTrigger(options.prompt ?? '', enabled);
|
|
17
|
+
printResult(io, ok('fresh-context.preflight', result), options.json);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
printResult(io, fail('fresh-context.preflight', 'PREFLIGHT_FAILED', getErrorMessage(error), {}, ['Verify ~/.peaks/config.json is valid JSON']), options.json);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InvalidArgumentError } from 'commander';
|
|
2
|
-
import {
|
|
2
|
+
import { createRequestArtifact, listRequestArtifacts, showRequestArtifact, transitionRequestArtifact, PrerequisitesNotSatisfiedError, LintGateError, TypeSanityViolationError, FileSizeViolationError, VALID_REQUEST_TYPES } from '../../services/artifacts/request-artifact-service.js';
|
|
3
|
+
import { applyPerArtifactFormat, inferArtifactName, parseRequestType, parseRole, parseStateForRole, resolveDefaultFormat, VALID_ROLES, } from './request-format-helpers.js';
|
|
3
4
|
import { ConfirmationRequiredError } from '../../services/mode/mode-enforcement.js';
|
|
4
5
|
import { recordBypass, isBypassLimitReached, MAX_BYPASSES_PER_SESSION } from '../../services/mode/bypass-tracker.js';
|
|
5
6
|
import { lintRequestArtifact } from '../../services/artifacts/artifact-lint-service.js';
|
|
@@ -7,102 +8,7 @@ import { getRepairCycleStatus } from '../../services/artifacts/repair-cycle-serv
|
|
|
7
8
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
8
9
|
import { triggerBestPracticeScan } from '../../services/prd/best-practice-auto-trigger.js';
|
|
9
10
|
import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
|
|
10
|
-
import { formatMdCompact } from '../../shared/format-md-compact.js';
|
|
11
11
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
12
|
-
/**
|
|
13
|
-
* Per-artifact default format. Only PRD and tech-doc are user-review
|
|
14
|
-
* surfaces; all other RD/QA/TXT artifacts default to compact. The
|
|
15
|
-
* `--pretty` / `--compact` flags override uniformly. See tech-doc
|
|
16
|
-
* §1.3.
|
|
17
|
-
*/
|
|
18
|
-
const DEFAULT_FORMAT_BY_ARTIFACT = {
|
|
19
|
-
prd: 'pretty',
|
|
20
|
-
'tech-doc': 'pretty',
|
|
21
|
-
'code-review': 'compact',
|
|
22
|
-
'security-review': 'compact',
|
|
23
|
-
'perf-baseline': 'compact',
|
|
24
|
-
'bug-analysis': 'compact',
|
|
25
|
-
'test-cases': 'compact',
|
|
26
|
-
'test-reports': 'compact',
|
|
27
|
-
'security-findings': 'compact',
|
|
28
|
-
'performance-findings': 'compact',
|
|
29
|
-
handoff: 'compact'
|
|
30
|
-
};
|
|
31
|
-
function resolveDefaultFormat(artifactName) {
|
|
32
|
-
return DEFAULT_FORMAT_BY_ARTIFACT[artifactName] ?? 'compact';
|
|
33
|
-
}
|
|
34
|
-
function applyPerArtifactFormat(envelope, override) {
|
|
35
|
-
if (override === null || envelope === null || typeof envelope !== 'object')
|
|
36
|
-
return envelope;
|
|
37
|
-
// The service returns `{ id, sessionId, role, body, ... }` for a
|
|
38
|
-
// single-artifact show. The per-artifact `body` field is the only
|
|
39
|
-
// thing that changes; we attach a `format` field to surface the
|
|
40
|
-
// choice to the caller. Slice 023 (R3) AC6 / AC7.
|
|
41
|
-
const obj = envelope;
|
|
42
|
-
if (typeof obj.body === 'string') {
|
|
43
|
-
return {
|
|
44
|
-
...obj,
|
|
45
|
-
body: override === 'compact' ? formatMdCompact(obj.body) : obj.body,
|
|
46
|
-
format: override
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
return envelope;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Map a request-artifact envelope to a `DEFAULT_FORMAT_BY_ARTIFACT` key.
|
|
53
|
-
* The service returns the path of the artifact in `path`; the role +
|
|
54
|
-
* filename stem gives us the artifact name. Falls back to the role
|
|
55
|
-
* itself when no `path` field is present.
|
|
56
|
-
*/
|
|
57
|
-
function inferArtifactName(envelope, role) {
|
|
58
|
-
if (envelope === null || typeof envelope !== 'object')
|
|
59
|
-
return role;
|
|
60
|
-
const obj = envelope;
|
|
61
|
-
const pathField = typeof obj.path === 'string' ? obj.path : '';
|
|
62
|
-
// Path looks like `.peaks/_runtime/<sid>/<role>/requests/<file>.md`.
|
|
63
|
-
// The role is the second-to-last directory; the file stem is the
|
|
64
|
-
// last segment. For RD role, the artifact is one of {code-review,
|
|
65
|
-
// security-review, perf-baseline, bug-analysis, tech-doc}; the file
|
|
66
|
-
// stem usually carries the artifact name (e.g. `tech-doc.md`,
|
|
67
|
-
// `code-review-002.md`). We strip the trailing `-<digits>` suffix
|
|
68
|
-
// when present.
|
|
69
|
-
const stem = pathField.split(/[\\/]/).pop()?.replace(/\.md$/, '') ?? '';
|
|
70
|
-
if (stem.length > 0) {
|
|
71
|
-
// Try the stem verbatim first.
|
|
72
|
-
if (DEFAULT_FORMAT_BY_ARTIFACT[stem] !== undefined)
|
|
73
|
-
return stem;
|
|
74
|
-
// Strip a trailing -<digits> (e.g. code-review-002 -> code-review).
|
|
75
|
-
const trimmed = stem.replace(/-\d+$/, '');
|
|
76
|
-
if (DEFAULT_FORMAT_BY_ARTIFACT[trimmed] !== undefined)
|
|
77
|
-
return trimmed;
|
|
78
|
-
// Last-ditch: match by prefix.
|
|
79
|
-
for (const key of Object.keys(DEFAULT_FORMAT_BY_ARTIFACT)) {
|
|
80
|
-
if (stem.startsWith(key))
|
|
81
|
-
return key;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return role;
|
|
85
|
-
}
|
|
86
|
-
const VALID_ROLES = ['prd', 'ui', 'rd', 'qa', 'sc'];
|
|
87
|
-
function parseRole(value) {
|
|
88
|
-
if (!VALID_ROLES.includes(value)) {
|
|
89
|
-
throw new InvalidArgumentError(`must be one of ${VALID_ROLES.join(', ')}`);
|
|
90
|
-
}
|
|
91
|
-
return value;
|
|
92
|
-
}
|
|
93
|
-
function parseStateForRole(role, value) {
|
|
94
|
-
const allowed = allowedStatesForRole(role);
|
|
95
|
-
if (!allowed.includes(value)) {
|
|
96
|
-
throw new InvalidArgumentError(`must be one of ${allowed.join(', ')} for role ${role}`);
|
|
97
|
-
}
|
|
98
|
-
return value;
|
|
99
|
-
}
|
|
100
|
-
function parseRequestType(value) {
|
|
101
|
-
if (!isRequestType(value)) {
|
|
102
|
-
throw new InvalidArgumentError(`must be one of ${VALID_REQUEST_TYPES.join(', ')}`);
|
|
103
|
-
}
|
|
104
|
-
return value;
|
|
105
|
-
}
|
|
106
12
|
export function registerRequestCommands(program, io) {
|
|
107
13
|
const request = program.command('request').description('Manage per-request Peaks role artifacts (PRD / UI / RD / QA)');
|
|
108
14
|
addJsonOption(request
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format / parse helpers extracted from `request-commands.ts`
|
|
3
|
+
* (slice 2026-09-06-split-batch-b) so the command file stays under the
|
|
4
|
+
* 800-line cap. Behaviour-preserving verbatim move.
|
|
5
|
+
*/
|
|
6
|
+
import { type RequestArtifactRole, type RequestArtifactState, type RequestType } from '../../services/artifacts/request-artifact-service.js';
|
|
7
|
+
export declare function resolveDefaultFormat(artifactName: string): 'compact' | 'pretty';
|
|
8
|
+
export declare function applyPerArtifactFormat(envelope: unknown, override: 'pretty' | 'compact' | null): unknown;
|
|
9
|
+
/**
|
|
10
|
+
* Map a request-artifact envelope to a `DEFAULT_FORMAT_BY_ARTIFACT` key.
|
|
11
|
+
* The service returns the path of the artifact in `path`; the role +
|
|
12
|
+
* filename stem gives us the artifact name. Falls back to the role
|
|
13
|
+
* itself when no `path` field is present.
|
|
14
|
+
*/
|
|
15
|
+
export declare function inferArtifactName(envelope: unknown, role: string): string;
|
|
16
|
+
export declare const VALID_ROLES: ReadonlyArray<RequestArtifactRole>;
|
|
17
|
+
export declare function parseRole(value: string): RequestArtifactRole;
|
|
18
|
+
export declare function parseStateForRole(role: RequestArtifactRole, value: string): RequestArtifactState;
|
|
19
|
+
export declare function parseRequestType(value: string): RequestType;
|