peaks-loop 4.0.35 → 4.0.37
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 +40 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/bin/peaks.js +71 -1
- package/dist/cli/cli-helpers.js +7 -0
- package/dist/cli/commands/_register.js +2 -0
- package/dist/cli/commands/best-practice-scan-command.d.ts +14 -1
- package/dist/cli/commands/best-practice-scan-command.js +67 -9
- package/dist/cli/commands/code-runtime-commands.d.ts +5 -2
- package/dist/cli/commands/code-runtime-commands.js +78 -7
- 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/hooks-commands.js +10 -1
- package/dist/cli/commands/job-commands.js +107 -25
- 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/scan-commands.js +1 -1
- 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/cli/commands/web-commands.d.ts +28 -0
- package/dist/cli/commands/web-commands.js +327 -0
- package/dist/cli/commands/web-lifecycle-commands.d.ts +49 -0
- package/dist/cli/commands/web-lifecycle-commands.js +321 -0
- package/dist/services/best-practice/scan-orchestrator.d.ts +22 -0
- package/dist/services/best-practice/scan-orchestrator.js +14 -5
- package/dist/services/code/orchestrator-can-do.js +27 -4
- 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-hint.d.ts +79 -0
- package/dist/services/context/context-audit-hint.js +150 -0
- 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/hooks/auto-compact-hook-install.js +10 -1
- package/dist/services/hooks/write-gate.js +88 -0
- 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/lint/detect-eslint.d.ts +2 -0
- package/dist/services/lint/detect-eslint.js +23 -9
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +38 -14
- 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/dist/services/release/version-precheck-service.js +9 -2
- package/dist/services/scan/file-size-scan.d.ts +29 -0
- package/dist/services/scan/file-size-scan.js +63 -0
- package/dist/services/session/caller-binding-service.d.ts +24 -0
- package/dist/services/session/caller-binding-service.js +34 -0
- package/dist/services/session/getSessionDir.js +15 -10
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +33 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +34 -3
- package/dist/services/skills/hooks-settings-service.d.ts +10 -0
- package/dist/services/skills/hooks-settings-service.js +152 -61
- package/dist/services/slice/slice-check-service.d.ts +14 -0
- package/dist/services/slice/slice-check-service.js +110 -50
- package/dist/services/slice/slice-check-types.d.ts +12 -7
- package/dist/services/slice/slice-check-types.js +8 -3
- package/dist/services/slice/slice-decompose-runners.js +24 -21
- package/dist/services/sop/sop-check-service.js +12 -1
- package/dist/services/web/bounded-output.d.ts +34 -0
- package/dist/services/web/bounded-output.js +68 -0
- package/dist/services/web/browser-acquire.d.ts +14 -0
- package/dist/services/web/browser-acquire.js +84 -0
- package/dist/services/web/browser-session-manager.d.ts +111 -0
- package/dist/services/web/browser-session-manager.js +413 -0
- package/dist/services/web/daemon-entry.d.ts +1 -0
- package/dist/services/web/daemon-entry.js +65 -0
- package/dist/services/web/daemon-registry.d.ts +42 -0
- package/dist/services/web/daemon-registry.js +164 -0
- package/dist/services/web/daemon-supervisor.d.ts +144 -0
- package/dist/services/web/daemon-supervisor.js +455 -0
- package/dist/services/web/playwright-loader.d.ts +89 -0
- package/dist/services/web/playwright-loader.js +253 -0
- package/dist/services/web/snapshot-pruner.d.ts +48 -0
- package/dist/services/web/snapshot-pruner.js +241 -0
- package/dist/services/web/untrusted-envelope.d.ts +27 -0
- package/dist/services/web/untrusted-envelope.js +44 -0
- package/dist/services/web/web-artifact-paths.d.ts +79 -0
- package/dist/services/web/web-artifact-paths.js +163 -0
- package/dist/services/web/web-client.d.ts +19 -0
- package/dist/services/web/web-client.js +55 -0
- package/dist/services/web/web-daemon-service.d.ts +38 -0
- package/dist/services/web/web-daemon-service.js +416 -0
- package/dist/services/web/web-fallback.d.ts +70 -0
- package/dist/services/web/web-fallback.js +121 -0
- package/dist/services/web/web-install-service.d.ts +91 -0
- package/dist/services/web/web-install-service.js +346 -0
- package/dist/services/web/web-login-profile.d.ts +89 -0
- package/dist/services/web/web-login-profile.js +612 -0
- package/dist/services/web/web-login-staging.d.ts +27 -0
- package/dist/services/web/web-login-staging.js +173 -0
- package/dist/services/web/web-protocol.d.ts +58 -0
- package/dist/services/web/web-protocol.js +58 -0
- package/dist/services/web/web-status-report.d.ts +33 -0
- package/dist/services/web/web-status-report.js +47 -0
- package/dist/services/workspace/claude-settings-template.d.ts +41 -5
- package/dist/services/workspace/claude-settings-template.js +116 -64
- package/dist/services/workspace/workspace-claude-settings-materializer.js +5 -1
- package/dist/services/workspace/workspace-service.js +33 -0
- package/package.json +5 -5
- package/scripts/copy-templates.mjs +12 -0
- package/scripts/sync-version.mjs +20 -0
- 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 +18 -0
- package/skills/peaks-code/references/browser-workflow.md +10 -1
- 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,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.0.37 — 2026-09-10 (Windows spawn 根因 + 会话解析 + 诚实性修复)
|
|
4
|
+
|
|
5
|
+
**Highlights**:
|
|
6
|
+
|
|
7
|
+
1. **Windows `.cmd` spawn 根因,7 处一并修复** — `execFile`/`spawnSync` 一个 `.cmd` shim:**带 shell 时路径里的空格会被 shell 切开,不带 shell 时 Node ≥20 直接 `EINVAL`**。两条分支都不成立,所以在不含空格的路径上一切正常、在每个测试里都绿。
|
|
8
|
+
- `peaks slice check` 在**路径含空格的项目上整体失效**——这是本仓库自己所在的平台。改为 `process.execPath` + 包的 JS entry(无 shim、无 shell)。
|
|
9
|
+
- `version-precheck`:`projectRoot` 作为 **git 的参数**传入,空格路径下 git 退出 128,该层退化成 "layer skipped" —— **把一个真实存在的 tag 冲突报告成了"只是延后"**。
|
|
10
|
+
- `peaks code orchestrator-can-do`:两个探针在 Windows 上**恒返回失败**,所以 `q2SubAgentAvailable` 永远是 false、`q4ContextRatio` 永远是硬编码的 `0`(那是 fallback,不是读数)。
|
|
11
|
+
- 其余:`slice-decompose-runners`(失败被吞进 `indexed:false`,codegraph 信号静默消失)、`detect-eslint`、`coverage-c8`(`--src=`/`--reports-dir=` 携带项目路径)、若干 release 脚本。
|
|
12
|
+
|
|
13
|
+
2. **会话解析根因——显式 rebind 此前只对一半 CLI 生效** — 两个函数都在回答"当前是哪个 session",来源却不同:`getCurrentSessionId` 读 `.peaks/_runtime/session.json`,`getSessionIdCanonical` **优先 per-caller binding**。`peaks workspace init --session-id X --allow-session-rebind` 只改前者,于是 caller binding **遮蔽**了显式 rebind。实测后果:`session checkpoint` 与 `24h-mode` 把状态写进**另一个 session 的目录**,而 `peaks job` 读的是正确的那个。现在 rebind 同步更新当前 caller 的 binding——**更新而非清除**,因为清除会丢掉 per-caller 身份且没有任何代码会重建它。未 rebind 的其它 caller 文件**字节不变**,隔离不受影响。
|
|
14
|
+
|
|
15
|
+
3. **job 可按 session 寻址(D6)+ `--slice-id` 不再静默 no-op(D7)** — 11 个 job 子命令补上 `--session-id`,解析顺序 `flag → PEAKS_SESSION_ID → binding`;job 不在绑定的 session 里时不再吐 `UNHANDLED_ERROR`,而是 `JOB_NOT_IN_SESSION` **指名它在哪个 session** 并给出重跑命令。`--slice-id` 现在接受 label 别名,未匹配则 `SLICE_NOT_FOUND` 并**列出全部合法 id**,且**不写入任何文件**(此前 `ok: true` 却什么都没做)。
|
|
16
|
+
|
|
17
|
+
4. **`peaks web open --profile <name>`——持久登录终于有人消费** — S4 落的 `~/.peaks/web-profiles/<name>/storageState.json` 此前**没有任何动词读取**。`--profile` 只加在 `open` 上;名字在 **CLI 与 daemon 两侧**各校验一次(走线上的名字不是证据);profile **只读**(自动浏览是否该回写用户级凭据文件是未决设计问题,本轮不替你决定);指定了不存在的 profile **按名字失败**,而不是静默用一个未登录的浏览器。
|
|
18
|
+
|
|
19
|
+
5. **诚实性修复——工具不再报告它没有的证据** — `peaks best-practice-scan` 此前把 stub 片段渲染成 8 行表格 + "方案 A ★" + 强制 ⚠️ 闸门,**全部来自合成文本**;现在合成结果直接拒绝(`ok:false`、exit 1,无推荐无表格)。`peaks slice check` 的 typecheck 阶段此前跑默认 `tsconfig.json`(272 个既有错误、**全部在 `tests/`**),**永远红**;现在 gate 于干净的 build 配置,宽口径计数对基线比较并在 detail 里如实报出。`peaks scan file-size` 不再把**生成文件**(记忆索引、lockfile)算作超长。
|
|
20
|
+
|
|
21
|
+
6. **两个"不可能失败"的守卫测试** — 重建被删的 session-dir 静态扫描时发现**原版三个不变量都失效**:`src/` 正则只匹配链条**末尾**的 session id(而它要防的 bug 在**中间**,命中 0 次);`skills/` 遍历只下一层(**完全看不到** `skills/bee/<skill>/references/`,包括它当初就是为了守的那个文件);占位符比自己的注释窄。两侧都补了永久性的可证伪用例。`withEnv` 此前通过**在测试体内部注册 `afterEach`** 来还原环境变量——作用域错误,值会漏给下一个测试;改用 `onTestFinished`。**当前没有测试是因为这个泄漏才通过的**——真实的假通过来源,暂无受害者。
|
|
22
|
+
|
|
23
|
+
7. **write gate 收紧且与 shell 解耦** — 放行范围从"**除 8 个名字外全部放行**"(那会放行顶层的 `.peaks/<change-id>/`,而 CLAUDE.md 明令禁止)收紧为**仅 `.peaks/_runtime/`**。handler 从 `node -e "<bash 专属转义的内联 JS>"` 改为调用随包发布的脚本文件,因而可像它的兄弟一样 pin PowerShell。顺带查明:**这个 hook 在 Windows 上从未真正生效过**——Claude Code 的 payload 走 stdin,而它读 argv,所以每次写入都落到 fall-through。
|
|
24
|
+
|
|
25
|
+
**验证**:build clean、`tsc -p tsconfig.build.json` exit 0、宽口径 `tsconfig.json` 维持既有 142;全量 **175 files / 1679 passed(3 skipped)**;0 个 `daemon-entry` 进程残留。以上每一项都在**构建产物**(`bin/peaks.js`,非 `tsx src/`)上冒烟过,含"路径含空格的项目"这一验收场景。
|
|
26
|
+
|
|
27
|
+
## 4.0.36 — 2026-09-10 (派发提示词瘦身 + 波次调度 + 编排器上下文审计)
|
|
28
|
+
|
|
29
|
+
**Highlights**:
|
|
30
|
+
|
|
31
|
+
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** 全角色断言。
|
|
32
|
+
- 契约指针实验**已回退**——它每次多 165 B 却不减任何内容,净亏。
|
|
33
|
+
|
|
34
|
+
2. **文件重叠感知的波次调度** — 新 planner 按切面文件集计算波次:同一波内文件两两不重叠,重叠的推迟到后续波并**注明被哪个文件挡住**;附加式接入 `--from-dag`(`firstLevelWaves`),不改原调度语义。
|
|
35
|
+
|
|
36
|
+
3. **编排器上下文审计 + 纪律** — 新增 `peaks code context-audit`:读会话 transcript 按 (工具, 命令) 汇总上下文消耗 Top-N。本仓库实测 **779 KB / 586 组**,且 Top-5 仅占 ~15%——**是长尾,不是单一元凶**。
|
|
37
|
+
- `--summary` 用于 memory reindex/list、doctor、request list:envelope **9.4–59 KB → 1.7–1.9 KB**(−79% ~ −97%),默认 `--json` 形状字节不变。
|
|
38
|
+
- SKILL.md 绑定规则:单次工具输出 >2 KB 禁止直接灌入编排器上下文。
|
|
39
|
+
- 子代理最终报告封顶 **≤40 行 / 2 KB**(5 个必填字段,细节写 artifact),已纳入 `BINDING_RULE_TOKENS`。
|
|
40
|
+
|
|
41
|
+
**验证**:build clean、tsc clean、全量 141 files / 1270 passed(1 skipped)。
|
|
42
|
+
|
|
3
43
|
## 4.0.35 — 2026-09-10 (记忆系统 overhaul — 写入合一 / 索引重建 / 按任务调取 / 漂移健康检查 / rotate)
|
|
4
44
|
|
|
5
45
|
**Highlights**:
|
package/README-en.md
CHANGED
|
@@ -140,7 +140,7 @@ Every lane opens with **one slash command**.
|
|
|
140
140
|
|
|
141
141
|
| | |
|
|
142
142
|
| --- | --- |
|
|
143
|
-
| **Latest** | [](https://www.npmjs.com/package/peaks-loop) — 4.0.
|
|
143
|
+
| **Latest** | [](https://www.npmjs.com/package/peaks-loop) — 4.0.37 (2026-09-10) |
|
|
144
144
|
| **Domains** | Code (`peaks-code`) · Content (`peaks-content`) · Project health (`peaks-doctor`) · Issue sweep (`peaks-issue-fix-orchestrator`) · Custom SOP (`peaks-sop`) · Cross-domain primitives (`peaks-solo` dispatcher · `peaks-resume` · `peaks-status` · `peaks-test` · `peaks-slice-decompose`) |
|
|
145
145
|
| **Sediment pool** | `~/.peaks/` local pool · twice-clean runs auto-promote to a bee · broken runs come back for you to redefine · the bee grows with your taste |
|
|
146
146
|
| **Test suite** | 285+ cases · 4 packages (peaks-loop / peaks-loop-mut / peaks-loop-shared-channel / peaks-loop-shared) · **0 timeouts** · 14 BDD caller-binding edge cases |
|
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ npm i -g peaks-loop
|
|
|
140
140
|
|
|
141
141
|
| | |
|
|
142
142
|
| --- | --- |
|
|
143
|
-
| **最新版本** | [](https://www.npmjs.com/package/peaks-loop) — 4.0.
|
|
143
|
+
| **最新版本** | [](https://www.npmjs.com/package/peaks-loop) — 4.0.37(2026-09-10) |
|
|
144
144
|
| **覆盖域** | 代码(`peaks-code`) · 内容(`peaks-content`) · 项目健康(`peaks-doctor`) · 批量修 issue(`peaks-issue-fix-orchestrator`) · 自定义 SOP(`peaks-sop`) · 通用原语(`peaks-solo` 分诊 / `peaks-resume` 续 / `peaks-status` 看 / `peaks-test` 测 / `peaks-slice-decompose` 切片) |
|
|
145
145
|
| **沉淀池** | `~/.peaks/` 本地池 · 跑两次自动晋升成 bee · 跑翻车让你重定义 · bee 跟着你的口味长 |
|
|
146
146
|
| **测试套件** | 1096 cases · 4 packages (peaks-loop 1015 / runtime 39 / mut 22 / shared-channel 20) · **CI 首次全绿**(ubuntu + windows) · 14 BDD caller-binding coverage |
|
package/bin/peaks.js
CHANGED
|
@@ -1,2 +1,72 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* peaks-loop CLI shim.
|
|
4
|
+
*
|
|
5
|
+
* This file used to be a bare static `import '../dist/cli/index.js'`. That
|
|
6
|
+
* dies with a raw `ERR_MODULE_NOT_FOUND` stack trace — before any peaks code
|
|
7
|
+
* runs — whenever the local build is stale or incomplete. The reproducible
|
|
8
|
+
* trigger: `scripts/sync-version.mjs` (run by `npm run build`, `pretest`,
|
|
9
|
+
* `prepublish`) unlinks `packages/peaks-loop-shared/dist/version.js`, so every
|
|
10
|
+
* `node bin/peaks.js <anything>` fails with an opaque Node error until the
|
|
11
|
+
* workspace packages are rebuilt.
|
|
12
|
+
*
|
|
13
|
+
* The guard below is the only behaviour added. The happy path is unchanged:
|
|
14
|
+
* the same entry module is imported with the same `process.argv`, and the
|
|
15
|
+
* CLI's own exit codes pass through untouched. Only a resolution failure
|
|
16
|
+
* whose missing specifier is OURS — an internal workspace package or any
|
|
17
|
+
* `dist/` artifact — is converted into an actionable message; every other
|
|
18
|
+
* error is rethrown unchanged (a genuinely missing third-party dependency
|
|
19
|
+
* must still surface as the raw Node error).
|
|
20
|
+
*/
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
24
|
+
|
|
25
|
+
/** Workspace-internal packages whose absence always means "stale build". */
|
|
26
|
+
const INTERNAL_PACKAGE_NAMES = ['peaks-loop-shared', 'peaks-loop-internal-runtime'];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The specifier Node could not resolve. Prefer the structured `err.url`
|
|
30
|
+
* (a clean `file://` URL); fall back to the quoted target in the message.
|
|
31
|
+
* The importer path is deliberately never parsed — `dist/cli/index.js` is
|
|
32
|
+
* the importer for every error, so matching it would flag third-party
|
|
33
|
+
* misses as stale-build misses.
|
|
34
|
+
*/
|
|
35
|
+
function missingSpecifier(err) {
|
|
36
|
+
if (typeof err?.url === 'string' && err.url.length > 0) return err.url;
|
|
37
|
+
const message = typeof err?.message === 'string' ? err.message : '';
|
|
38
|
+
const pkg = /Cannot find package '([^']+)'/.exec(message);
|
|
39
|
+
if (pkg !== null) return pkg[1];
|
|
40
|
+
const mod = /Cannot find module '([^']+)'/.exec(message);
|
|
41
|
+
if (mod !== null) return mod[1];
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isInternalSpecifier(specifier) {
|
|
46
|
+
const normalized = specifier.replace(/\\/g, '/');
|
|
47
|
+
if (normalized.length === 0) return false;
|
|
48
|
+
for (const name of INTERNAL_PACKAGE_NAMES) {
|
|
49
|
+
if (
|
|
50
|
+
normalized === name ||
|
|
51
|
+
normalized.startsWith(`${name}/`) ||
|
|
52
|
+
normalized.includes(`/${name}/`) ||
|
|
53
|
+
normalized.endsWith(`/${name}`)
|
|
54
|
+
) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return normalized.startsWith('dist/') || /\/dist\//.test(normalized);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await import('../dist/cli/index.js');
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const specifier = err?.code === 'ERR_MODULE_NOT_FOUND' ? missingSpecifier(err) : '';
|
|
65
|
+
if (specifier.length === 0 || !isInternalSpecifier(specifier)) throw err;
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
'peaks-loop: internal module not found — the local build is stale or incomplete.\n' +
|
|
68
|
+
` missing: ${specifier}\n` +
|
|
69
|
+
` fix: run \`npm run build\` in ${PACKAGE_ROOT}, then retry.\n`
|
|
70
|
+
);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
}
|
package/dist/cli/cli-helpers.js
CHANGED
|
@@ -6,6 +6,13 @@ export function printResult(io, result, asJson = false) {
|
|
|
6
6
|
}
|
|
7
7
|
if (!result.ok) {
|
|
8
8
|
io.stderr(`${result.code}: ${result.message}`);
|
|
9
|
+
// S1's F1, at the printer: a warning carried by a FAILED envelope is the
|
|
10
|
+
// only news some failures have — `peaks web login` reports a headed browser
|
|
11
|
+
// it could not close this way. `fail()` hard-codes `warnings: []`, so only
|
|
12
|
+
// an envelope that spreads its own warnings over a `fail()` reaches here.
|
|
13
|
+
for (const warning of result.warnings) {
|
|
14
|
+
io.stderr(`warning: ${warning}`);
|
|
15
|
+
}
|
|
9
16
|
for (const action of result.nextActions) {
|
|
10
17
|
io.stderr(`- ${action}`);
|
|
11
18
|
}
|
|
@@ -80,6 +80,7 @@ import { registerVendorDetectCommand } from './vendor-detect.js';
|
|
|
80
80
|
import { registerUpgradeCommands } from './upgrade-commands.js';
|
|
81
81
|
import { registerUserTouchpointCommands } from './user-touchpoint-commands.js';
|
|
82
82
|
import { registerVerdictAggregateCommands } from './verdict-aggregate-command.js';
|
|
83
|
+
import { registerWebCommands } from './web-commands.js';
|
|
83
84
|
import { registerWorkflowCommands } from './workflow-commands.js';
|
|
84
85
|
import { registerWorkflowPlanCommands } from './workflow-plan-commands.js';
|
|
85
86
|
import { registerWorktreeAuthCommand } from './worktree-auth-commands.js';
|
|
@@ -139,6 +140,7 @@ const REGISTRATIONS = [
|
|
|
139
140
|
['session-spill-demo', registerSpillDemoCommand],
|
|
140
141
|
['outer-cache-commands', registerOuterCacheCommands],
|
|
141
142
|
['vendor-detect', registerVendorDetectCommand],
|
|
143
|
+
['web-commands', registerWebCommands],
|
|
142
144
|
];
|
|
143
145
|
function dispatchRegister(register, program, io) {
|
|
144
146
|
if (register.length <= 1) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Slice 2026-08-12 best-practice-scan — CLI subcommand.
|
|
3
3
|
*
|
|
4
|
-
* `peaks best-practice-scan --project <path> [--lang <lang>] [--commit]`
|
|
4
|
+
* `peaks best-practice-scan --intent <goal> --project <path> [--lang <lang>] [--commit]`
|
|
5
5
|
*
|
|
6
6
|
* Pipeline:
|
|
7
7
|
* 1. detect language via language-detector (or use --lang override)
|
|
@@ -12,6 +12,19 @@
|
|
|
12
12
|
* a future slice that opts the artifact into git tracking)
|
|
13
13
|
* 5. print artifact path + the 8-row table to stdout
|
|
14
14
|
*
|
|
15
|
+
* `--intent` is REQUIRED: it is the business goal the scan is for (Step 2.5
|
|
16
|
+
* fires from a non-empty business goal), and it is what the artifact slug is
|
|
17
|
+
* derived from. The project path is NOT an intent — falling back to it was
|
|
18
|
+
* the defect this command was fixed for (a `.`-titled "best-practice scan").
|
|
19
|
+
* Omitting `--intent` fails with `BEST_PRACTICE_SCAN_INTENT_REQUIRED`.
|
|
20
|
+
*
|
|
21
|
+
* Honesty gate: v1's Context7 / WebSearch lookups are stubs, so a scan can
|
|
22
|
+
* come back `synthetic: true` (see scan-orchestrator). A synthetic result is
|
|
23
|
+
* NOT a scan result — the command then prints a refusal (no table, no
|
|
24
|
+
* recommendation, no ⚠️ catch gate), records the skip, and exits non-zero
|
|
25
|
+
* with `BEST_PRACTICE_SCAN_SYNTHETIC_LOOKUP` so no automated caller can
|
|
26
|
+
* treat Step 2.5 as satisfied.
|
|
27
|
+
*
|
|
15
28
|
* Catch-gate (spec §7): the command emits a 3-line prompt asking the
|
|
16
29
|
* user to ack / pick alt / reject + reason. The gate is read from
|
|
17
30
|
* stdin via `PEAKS_BEST_PRACTICE_STDIN` (test seam) or the real stdin
|
|
@@ -9,6 +9,37 @@ const CATCH_GATE_PROMPT = [
|
|
|
9
9
|
'⚠️ 任何跟你真实业务不一样,改 — LLM 推荐可能错。',
|
|
10
10
|
'回应 (默认 = 接受): 接受 / 接受方案 A|接受方案 B|接受方案 C / 拒绝 + 原因'
|
|
11
11
|
].join('\n');
|
|
12
|
+
const INTENT_REQUIRED_MESSAGE = 'best-practice-scan requires --intent <text>: the intent is the business goal the scan is ' +
|
|
13
|
+
'for, not the project path. Deriving it from the project path labelled a scan with a ' +
|
|
14
|
+
'non-goal, so there is no fallback.';
|
|
15
|
+
const SYNTHETIC_SOURCE_MESSAGE = 'best-practice-scan: SKIPPED — the Context7 / WebSearch lookups in this build are stubs ' +
|
|
16
|
+
'(synthetic fragments), so no real documentation was consulted and there is nothing to ' +
|
|
17
|
+
'recommend. No recommendation, no comparison table and no ⚠️ catch gate were produced.';
|
|
18
|
+
function bestPracticeDir(projectRoot) {
|
|
19
|
+
return join(projectRoot, 'best-practice');
|
|
20
|
+
}
|
|
21
|
+
function artifactPathFor(projectRoot, slugSource) {
|
|
22
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
23
|
+
const slug = slugSource.replace(/[^a-zA-Z0-9_-]+/g, '-').slice(0, 32) || 'intent';
|
|
24
|
+
return join(bestPracticeDir(projectRoot), `${dateStr}-${slug}.md`);
|
|
25
|
+
}
|
|
26
|
+
/** Refusal body for a synthetic scan. It is written to the artifact path so
|
|
27
|
+
* the skip survives the fire-and-forget caller (`best-practice-auto-trigger`
|
|
28
|
+
* spawns with `stdio: 'ignore'`, so stdout never reaches anyone). */
|
|
29
|
+
function renderSyntheticRefusal(opts) {
|
|
30
|
+
return [
|
|
31
|
+
`# Best-Practice Scan — ${opts.intent}`,
|
|
32
|
+
'',
|
|
33
|
+
'> SKIPPED — synthetic (stub) lookup. No real documentation lookup was performed.',
|
|
34
|
+
'',
|
|
35
|
+
SYNTHETIC_SOURCE_MESSAGE,
|
|
36
|
+
'',
|
|
37
|
+
`- language: ${opts.language}`,
|
|
38
|
+
`- transport that answered: ${opts.source} (stub)`,
|
|
39
|
+
'- Step 2.5: recorded as skipped, reason "synthetic-lookup". Re-run this step once the real',
|
|
40
|
+
' lookup lands; a gate cannot be satisfied from a fabricated scan.'
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
12
43
|
async function readUserInput() {
|
|
13
44
|
const override = process.env.PEAKS_BEST_PRACTICE_STDIN;
|
|
14
45
|
if (override !== undefined)
|
|
@@ -59,25 +90,54 @@ export function registerBestPracticeScanCommand(program, io) {
|
|
|
59
90
|
.description('2026-08-12 best-practice-scan: language-aware + business-aware doc-fragment lookup ' +
|
|
60
91
|
'via Context7 (priority 1) → WebSearch (priority 2) → empty fallback. ' +
|
|
61
92
|
'Renders the 8-row comparison table from spec §5 + §6 + §7. ' +
|
|
62
|
-
'Writes the artifact to <project>/best-practice/<date>-<intent>.md (gitignored).'
|
|
93
|
+
'Writes the artifact to <project>/best-practice/<date>-<intent>.md (gitignored). ' +
|
|
94
|
+
'--intent is required; a synthetic (stub) lookup refuses instead of rendering a ' +
|
|
95
|
+
'recommendation and exits non-zero.')
|
|
96
|
+
.option('--intent <text>', 'business goal the scan is for (required)')
|
|
63
97
|
.option('--project <path>', 'project root (default cwd)', process.cwd())
|
|
64
98
|
.option('--lang <lang>', 'language override (skip auto-detect)')
|
|
65
99
|
.option('--commit', 'reserved flag — opts the artifact into git tracking (future slice)')).action(async (opts) => {
|
|
66
100
|
try {
|
|
101
|
+
const intent = (opts.intent ?? '').trim();
|
|
102
|
+
if (intent.length === 0) {
|
|
103
|
+
printResult(io, fail('best-practice.scan', 'BEST_PRACTICE_SCAN_INTENT_REQUIRED', INTENT_REQUIRED_MESSAGE, { project: opts.project }, [
|
|
104
|
+
'Rerun with --intent "<business goal>" — the goal from the PRD artifact'
|
|
105
|
+
]), opts.json === true);
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
67
109
|
const detection = opts.lang === undefined ? detectLanguage(opts.project) : null;
|
|
68
110
|
const language = opts.lang ?? detection?.language ?? 'unknown';
|
|
69
|
-
io.stdout(`[best-practice-scan] project=${opts.project} language=${language}`);
|
|
111
|
+
io.stdout(`[best-practice-scan] project=${opts.project} intent=${intent} language=${language}`);
|
|
70
112
|
const scan = await scanBestPractice({
|
|
71
|
-
intent
|
|
113
|
+
intent,
|
|
72
114
|
language,
|
|
73
115
|
projectRoot: opts.project,
|
|
74
116
|
io
|
|
75
117
|
});
|
|
118
|
+
if (scan.synthetic) {
|
|
119
|
+
const refusal = renderSyntheticRefusal({ intent, language, source: scan.source });
|
|
120
|
+
io.stdout(refusal);
|
|
121
|
+
const refusedPath = artifactPathFor(opts.project, intent);
|
|
122
|
+
mkdirSync(bestPracticeDir(opts.project), { recursive: true });
|
|
123
|
+
writeFileSync(refusedPath, refusal + '\n', 'utf8');
|
|
124
|
+
printResult(io, fail('best-practice.scan', 'BEST_PRACTICE_SCAN_SYNTHETIC_LOOKUP', SYNTHETIC_SOURCE_MESSAGE, {
|
|
125
|
+
project: opts.project,
|
|
126
|
+
intent,
|
|
127
|
+
language,
|
|
128
|
+
scanSource: scan.source,
|
|
129
|
+
synthetic: true,
|
|
130
|
+
step25: 'skipped-synthetic-lookup',
|
|
131
|
+
artifactPath: refusedPath
|
|
132
|
+
}, ['Re-run once real Context7 / WebSearch wiring lands', `Skip record written to ${refusedPath}`]), opts.json === true);
|
|
133
|
+
process.exitCode = 1;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
76
136
|
const recommendation = scan.results.length > 0 ? 'A' : 'B';
|
|
77
137
|
const reasoning = `基于 ${scan.fragments.length} 个 ${scan.source} 文档片段;` +
|
|
78
138
|
`语言=${language};LLM 推断方案 ${recommendation} 与项目阶段最匹配。`;
|
|
79
139
|
const table = formatOutputTable({
|
|
80
|
-
intent
|
|
140
|
+
intent,
|
|
81
141
|
language,
|
|
82
142
|
fragments: scan.results,
|
|
83
143
|
recommendation,
|
|
@@ -92,14 +152,12 @@ export function registerBestPracticeScanCommand(program, io) {
|
|
|
92
152
|
io.stdout(CATCH_GATE_PROMPT);
|
|
93
153
|
const userInput = await readUserInput();
|
|
94
154
|
const outcome = parseCatchGateReply(userInput, recommendation);
|
|
95
|
-
const
|
|
96
|
-
mkdirSync(
|
|
97
|
-
const dateStr = new Date().toISOString().slice(0, 10);
|
|
98
|
-
const slug = opts.project.replace(/[^a-zA-Z0-9_-]+/g, '-').slice(0, 32) || 'intent';
|
|
99
|
-
const artifactPath = join(outDir, `${dateStr}-${slug}.md`);
|
|
155
|
+
const artifactPath = artifactPathFor(opts.project, intent);
|
|
156
|
+
mkdirSync(bestPracticeDir(opts.project), { recursive: true });
|
|
100
157
|
writeFileSync(artifactPath, table + '\n', 'utf8');
|
|
101
158
|
printResult(io, ok('best-practice.scan', {
|
|
102
159
|
project: opts.project,
|
|
160
|
+
intent,
|
|
103
161
|
language,
|
|
104
162
|
scanSource: scan.source,
|
|
105
163
|
fragments: scan.results,
|
|
@@ -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,20 @@
|
|
|
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';
|
|
17
|
+
import { buildContextAuditHint } from '../../services/context/context-audit-hint.js';
|
|
13
18
|
import { evaluateStep08, STEP_08_BACKUP_REGEX } from '../../services/code/step-08-gate.js';
|
|
14
19
|
import { evaluateEmitHandoff, JOB_NOT_INITIALIZED, JOB_REMAINING_BLOCKED } from '../../services/code/emit-handoff.js';
|
|
15
20
|
import { readJobShapeDecision, JobShapeDecisionError } from '../../services/code/job-shape-decision.js';
|
|
@@ -254,6 +259,57 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
254
259
|
process.exitCode = 1;
|
|
255
260
|
}
|
|
256
261
|
});
|
|
262
|
+
// Slice 2026-09-10-context-audit-and-discipline (Slice A): visibility into
|
|
263
|
+
// WHAT fills the orchestrator window. `context-now` returns a ratio; this
|
|
264
|
+
// returns the grouped byte breakdown of tool results from the live
|
|
265
|
+
// transcript. Read-only + fail-soft: an unavailable transcript reports
|
|
266
|
+
// `available:false` with a reason and NEVER sets a non-zero exit code.
|
|
267
|
+
addJsonOption(code
|
|
268
|
+
.command('context-audit')
|
|
269
|
+
.description('Slice 2026-09-10 Slice A: report what fills the current session\'s ' +
|
|
270
|
+
'context window, grouped by tool + short input key (command line / ' +
|
|
271
|
+
'path tail / pattern), sorted by bytes. Locates the CURRENT session\'s ' +
|
|
272
|
+
'IDE transcript through the active adapter\'s ' +
|
|
273
|
+
'`compact.resolveTranscriptPath` (vendor-neutral); emits total bytes, ' +
|
|
274
|
+
'entry count, and the top-N groups ' +
|
|
275
|
+
'(`{tool, key, bytes, pctOfTotal, count}`). Read-only and fail-soft — ' +
|
|
276
|
+
'a missing/oversized/corrupt transcript returns `available:false` ' +
|
|
277
|
+
'with a reason and never blocks. Never dumps tool result content.')
|
|
278
|
+
.requiredOption('--project <path>', 'target project root')
|
|
279
|
+
.option('--session-id <sid>', 'override session id (default: read from active presence)')
|
|
280
|
+
.option('--top <n>', 'number of top entries to emit (default 15, max 100)', (value) => Number(value))
|
|
281
|
+
.option('--transcript <path>', 'override the transcript jsonl path (test seam)')).action((opts) => {
|
|
282
|
+
try {
|
|
283
|
+
// `resolveOuterSessionId` checks the env signal FIRST, so the peaks
|
|
284
|
+
// session id is only a fallback lookup key — mirror context-now's
|
|
285
|
+
// 'unknown' default so an unbound presence still resolves via env.
|
|
286
|
+
const sessionId = opts.sessionId ?? readActiveSid(opts.project) ?? 'unknown';
|
|
287
|
+
const outerSessionId = resolveOuterSessionId(opts.project, sessionId);
|
|
288
|
+
const result = auditContext({
|
|
289
|
+
outerSessionId: outerSessionId ?? null,
|
|
290
|
+
...(opts.top !== undefined ? { topN: opts.top } : {}),
|
|
291
|
+
...(opts.transcript !== undefined ? { transcriptPath: opts.transcript } : {}),
|
|
292
|
+
});
|
|
293
|
+
// Fail-soft contract: unavailability is DATA, not an error. The exit
|
|
294
|
+
// code stays 0 so a `context-audit` call can never block a workflow.
|
|
295
|
+
const nextActions = result.available
|
|
296
|
+
? [`${result.entryCount} tool result(s) across ${result.groupCount} group(s); showing top ${result.entries.length}.`]
|
|
297
|
+
: [`context-audit unavailable: ${result.reason ?? 'unknown'} — continue without it (read-only probe).`];
|
|
298
|
+
printResult(io, ok('code.context-audit', { ...result }, [], nextActions), opts.json);
|
|
299
|
+
}
|
|
300
|
+
catch (err) {
|
|
301
|
+
printResult(io, ok('code.context-audit', {
|
|
302
|
+
available: false,
|
|
303
|
+
reason: `audit-failed: ${getErrorMessage(err)}`,
|
|
304
|
+
transcriptPath: null,
|
|
305
|
+
totalBytes: 0,
|
|
306
|
+
entryCount: 0,
|
|
307
|
+
groupCount: 0,
|
|
308
|
+
topN: 0,
|
|
309
|
+
entries: []
|
|
310
|
+
}, [], ['context-audit is a read-only probe; continue without it.']), opts.json);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
257
313
|
// v3.1.2 Step 0.8 — Mechanical PreToolUse gate.
|
|
258
314
|
// Wire-installed by `peaks workspace init` (extends the existing hook
|
|
259
315
|
// installer). Exit code is the load-bearing contract:
|
|
@@ -265,7 +321,8 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
265
321
|
'fail-closed backup regex when missing. Exit 0 = allow, exit 2 = block. ' +
|
|
266
322
|
'When the decision says isJob=true AND progress.json exists, the stdout ' +
|
|
267
323
|
'also carries `Next: slice #N+1 of M (<currentSlice>)` so the LLM cannot ' +
|
|
268
|
-
'wake up cold.'
|
|
324
|
+
'wake up cold. When the context ratio is ≥ 0.70 the stdout gains ONE more ' +
|
|
325
|
+
'line naming the largest context consumer (audit cached ≥ 5 min; fail-soft).')
|
|
269
326
|
.requiredOption('--project <path>', 'target project root (the hook passes "." so resolveCanonicalProjectRoot promotes it to the git root)')
|
|
270
327
|
.option('--session-id <sid>', 'override session id (default: read from active presence)')
|
|
271
328
|
.option('--prompt <text>', 'explicit prompt text (default: read last-prompt.txt; stdin ignored)')).action((opts) => {
|
|
@@ -285,6 +342,17 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
285
342
|
printResult(io, envelope, opts.json);
|
|
286
343
|
return;
|
|
287
344
|
}
|
|
345
|
+
// Slice 2026-09-10-three-fixes (Slice 2): proactive context-consumer
|
|
346
|
+
// hint. Runs ONLY when the window is ≥ 0.70 full, caches the audit
|
|
347
|
+
// result for ≥ 5 min so the transcript is scanned at most once per
|
|
348
|
+
// TTL window, and is fail-soft (null → no extra line). It never
|
|
349
|
+
// changes the exit code and never blocks.
|
|
350
|
+
const hintLine = buildContextAuditHint({
|
|
351
|
+
projectRoot: opts.project,
|
|
352
|
+
sessionId,
|
|
353
|
+
outerSessionId: resolveOuterSessionId(opts.project, sessionId)
|
|
354
|
+
});
|
|
355
|
+
const hintActions = hintLine === null ? [] : [hintLine];
|
|
288
356
|
const evalInput = {
|
|
289
357
|
projectRoot: opts.project,
|
|
290
358
|
sessionId
|
|
@@ -300,7 +368,7 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
300
368
|
decision: verdict.decision,
|
|
301
369
|
progress: verdict.progress,
|
|
302
370
|
nextSlice: result.nextSliceLine
|
|
303
|
-
}, [], result.nextSliceLine !== null ? [result.nextSliceLine] : []);
|
|
371
|
+
}, [], [...(result.nextSliceLine !== null ? [result.nextSliceLine] : []), ...hintActions]);
|
|
304
372
|
printResult(io, envelope, opts.json);
|
|
305
373
|
return;
|
|
306
374
|
}
|
|
@@ -311,7 +379,8 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
311
379
|
decision: null,
|
|
312
380
|
nextSlice: null
|
|
313
381
|
}, [], [
|
|
314
|
-
'job-shape.json says isJob=false; single-rid mode (gate allows).'
|
|
382
|
+
'job-shape.json says isJob=false; single-rid mode (gate allows).',
|
|
383
|
+
...hintActions
|
|
315
384
|
]);
|
|
316
385
|
printResult(io, envelope, opts.json);
|
|
317
386
|
return;
|
|
@@ -326,7 +395,8 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
326
395
|
backupRegex: STEP_08_BACKUP_REGEX.toString()
|
|
327
396
|
}, [
|
|
328
397
|
'Run `peaks code detect-job --is-job true --rationale <text> --suggested-job-id <slug>` to record the Job-shape verdict.',
|
|
329
|
-
'Then re-run the Bash tool call.'
|
|
398
|
+
'Then re-run the Bash tool call.',
|
|
399
|
+
...hintActions
|
|
330
400
|
]);
|
|
331
401
|
io.stderr(`${blockMessage}\n`);
|
|
332
402
|
printResult(io, envelope, opts.json);
|
|
@@ -341,7 +411,8 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
341
411
|
nextSlice: null,
|
|
342
412
|
promptSource: verdict.promptSource
|
|
343
413
|
}, [], [
|
|
344
|
-
'No job-shape.json AND no backup-regex match on prompt → allow (most prompts are not Job-shaped).'
|
|
414
|
+
'No job-shape.json AND no backup-regex match on prompt → allow (most prompts are not Job-shaped).',
|
|
415
|
+
...hintActions
|
|
345
416
|
]);
|
|
346
417
|
printResult(io, envelope, opts.json);
|
|
347
418
|
return;
|
|
@@ -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) => {
|