peaks-loop 4.0.36 → 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 +24 -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.js +21 -5
- package/dist/cli/commands/hooks-commands.js +10 -1
- package/dist/cli/commands/job-commands.js +107 -25
- package/dist/cli/commands/scan-commands.js +1 -1
- 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/context-audit-hint.d.ts +79 -0
- package/dist/services/context/context-audit-hint.js +150 -0
- package/dist/services/hooks/auto-compact-hook-install.js +10 -1
- package/dist/services/hooks/write-gate.js +88 -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/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/peaks-code/SKILL.md +10 -0
- package/skills/peaks-code/references/browser-workflow.md +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
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
|
+
|
|
3
27
|
## 4.0.36 — 2026-09-10 (派发提示词瘦身 + 波次调度 + 编排器上下文审计)
|
|
4
28
|
|
|
5
29
|
**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,
|
|
@@ -14,6 +14,7 @@ import { fail, ok } from 'peaks-loop-shared/result';
|
|
|
14
14
|
import { detectPostCompactResume, formatPostCompactResumeLogLine } from '../../services/code/post-compact-detector.js';
|
|
15
15
|
import { runAutoCompact } from '../../services/code/auto-compact-orchestrator.js';
|
|
16
16
|
import { auditContext } from '../../services/context/context-audit.js';
|
|
17
|
+
import { buildContextAuditHint } from '../../services/context/context-audit-hint.js';
|
|
17
18
|
import { evaluateStep08, STEP_08_BACKUP_REGEX } from '../../services/code/step-08-gate.js';
|
|
18
19
|
import { evaluateEmitHandoff, JOB_NOT_INITIALIZED, JOB_REMAINING_BLOCKED } from '../../services/code/emit-handoff.js';
|
|
19
20
|
import { readJobShapeDecision, JobShapeDecisionError } from '../../services/code/job-shape-decision.js';
|
|
@@ -320,7 +321,8 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
320
321
|
'fail-closed backup regex when missing. Exit 0 = allow, exit 2 = block. ' +
|
|
321
322
|
'When the decision says isJob=true AND progress.json exists, the stdout ' +
|
|
322
323
|
'also carries `Next: slice #N+1 of M (<currentSlice>)` so the LLM cannot ' +
|
|
323
|
-
'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).')
|
|
324
326
|
.requiredOption('--project <path>', 'target project root (the hook passes "." so resolveCanonicalProjectRoot promotes it to the git root)')
|
|
325
327
|
.option('--session-id <sid>', 'override session id (default: read from active presence)')
|
|
326
328
|
.option('--prompt <text>', 'explicit prompt text (default: read last-prompt.txt; stdin ignored)')).action((opts) => {
|
|
@@ -340,6 +342,17 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
340
342
|
printResult(io, envelope, opts.json);
|
|
341
343
|
return;
|
|
342
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];
|
|
343
356
|
const evalInput = {
|
|
344
357
|
projectRoot: opts.project,
|
|
345
358
|
sessionId
|
|
@@ -355,7 +368,7 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
355
368
|
decision: verdict.decision,
|
|
356
369
|
progress: verdict.progress,
|
|
357
370
|
nextSlice: result.nextSliceLine
|
|
358
|
-
}, [], result.nextSliceLine !== null ? [result.nextSliceLine] : []);
|
|
371
|
+
}, [], [...(result.nextSliceLine !== null ? [result.nextSliceLine] : []), ...hintActions]);
|
|
359
372
|
printResult(io, envelope, opts.json);
|
|
360
373
|
return;
|
|
361
374
|
}
|
|
@@ -366,7 +379,8 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
366
379
|
decision: null,
|
|
367
380
|
nextSlice: null
|
|
368
381
|
}, [], [
|
|
369
|
-
'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
|
|
370
384
|
]);
|
|
371
385
|
printResult(io, envelope, opts.json);
|
|
372
386
|
return;
|
|
@@ -381,7 +395,8 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
381
395
|
backupRegex: STEP_08_BACKUP_REGEX.toString()
|
|
382
396
|
}, [
|
|
383
397
|
'Run `peaks code detect-job --is-job true --rationale <text> --suggested-job-id <slug>` to record the Job-shape verdict.',
|
|
384
|
-
'Then re-run the Bash tool call.'
|
|
398
|
+
'Then re-run the Bash tool call.',
|
|
399
|
+
...hintActions
|
|
385
400
|
]);
|
|
386
401
|
io.stderr(`${blockMessage}\n`);
|
|
387
402
|
printResult(io, envelope, opts.json);
|
|
@@ -396,7 +411,8 @@ export function registerCodeRuntimeCommands(code, io) {
|
|
|
396
411
|
nextSlice: null,
|
|
397
412
|
promptSource: verdict.promptSource
|
|
398
413
|
}, [], [
|
|
399
|
-
'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
|
|
400
416
|
]);
|
|
401
417
|
printResult(io, envelope, opts.json);
|
|
402
418
|
return;
|
|
@@ -300,6 +300,12 @@ export function registerHooksCommands(program, io) {
|
|
|
300
300
|
// install left behind.
|
|
301
301
|
const settingsPath = status.settingsPath;
|
|
302
302
|
const settings = existsSync(settingsPath) ? readJsonObjectFile(settingsPath) : {};
|
|
303
|
+
// The gate-enforce entry is materialized into the machine-local
|
|
304
|
+
// settings file (see `resolveHookTargets`), so the on-disk entry list
|
|
305
|
+
// must be read from both files or `status` would report it missing.
|
|
306
|
+
const localSettings = status.localSettingsPath !== undefined && existsSync(status.localSettingsPath)
|
|
307
|
+
? readJsonObjectFile(status.localSettingsPath)
|
|
308
|
+
: {};
|
|
303
309
|
// Slice 2026-07-29-worktree-layer3-deny: report the Layer 3 deny
|
|
304
310
|
// entries actually on disk. We read the existing settings.json
|
|
305
311
|
// (above) and surface its `permissions.deny` block. Any entry
|
|
@@ -311,7 +317,10 @@ export function registerHooksCommands(program, io) {
|
|
|
311
317
|
printResult(io, ok('hooks.status', {
|
|
312
318
|
...status,
|
|
313
319
|
ide,
|
|
314
|
-
entries:
|
|
320
|
+
entries: [
|
|
321
|
+
...readInstalledEntriesFromSettings(settings, ide),
|
|
322
|
+
...readInstalledEntriesFromSettings(localSettings, ide)
|
|
323
|
+
],
|
|
315
324
|
permissionsDenyEntries: listSuperpowersDenyEntries(),
|
|
316
325
|
permissionsDenyOnDisk: onDiskDeny
|
|
317
326
|
}), options.json);
|