sillyspec 3.26.11 → 3.26.14
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/.claude/skills/sillyspec-archive/SKILL.md +1 -1
- package/.claude/skills/sillyspec-execute/SKILL.md +1 -1
- package/.claude/skills/sillyspec-knowledge/SKILL.md +1 -1
- package/.claude/skills/sillyspec-plan/SKILL.md +5 -5
- package/.claude/skills/sillyspec-quick/SKILL.md +5 -1
- package/README.md +7 -5
- package/SKILL.md +5 -4
- package/package.json +1 -1
- package/packages/dashboard/server/executor.js +10 -1
- package/packages/dashboard/server/index.js +108 -46
- package/src/config-schema.js +3 -0
- package/src/contract-matrix.js +22 -13
- package/src/db.js +24 -4
- package/src/dispatch/probe.js +99 -63
- package/src/docs-check.js +58 -13
- package/src/docs-debt.js +6 -2
- package/src/docs-gate.js +1 -0
- package/src/doctor-diagnostics.js +208 -21
- package/src/fs-atomic.js +3 -1
- package/src/git-helper.js +7 -4
- package/src/hooks/worktree-guard.js +8 -2
- package/src/index.js +200 -27
- package/src/init.js +3 -2
- package/src/local-detect.js +9 -1
- package/src/modules.js +7 -5
- package/src/progress/stage-machine.js +589 -578
- package/src/progress/step-store.js +1 -1
- package/src/progress.js +187 -30
- package/src/quicklog.js +47 -16
- package/src/run/command.js +88 -46
- package/src/run/complete-handlers.js +27 -15
- package/src/run/complete.js +1186 -1121
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +172 -25
- package/src/run/prompt.js +17 -9
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +164 -63
- package/src/run/stage.js +41 -1
- package/src/setup.js +8 -10
- package/src/sillyhub-mcp/client.js +74 -6
- package/src/spec-dir-typo.js +3 -24
- package/src/spec-sync.js +43 -8
- package/src/stage-contract.js +12 -38
- package/src/stage-review.js +40 -1
- package/src/stages/archive.js +4 -3
- package/src/stages/brainstorm-auto.js +1 -1
- package/src/stages/brainstorm.js +27 -13
- package/src/stages/doctor.js +29 -16
- package/src/stages/execute.js +170 -154
- package/src/stages/knowledge.js +18 -3
- package/src/stages/plan-postcheck.js +72 -37
- package/src/stages/plan.js +70 -48
- package/src/stages/quick.js +3 -1
- package/src/stages/verify.js +1 -1
- package/src/sync.js +339 -225
- package/src/task-review.js +281 -64
- package/src/taskcard.js +178 -0
- package/src/verify-postcheck.js +48 -35
- package/src/worktree-apply.js +58 -34
- package/src/worktree-deps.js +120 -20
- package/src/worktree.js +55 -9
- package/.npmrc-publish +0 -2
package/src/run/shared.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 本模块仅 re-export,run/ 层现有调用方路径与行为不变。
|
|
12
12
|
*/
|
|
13
13
|
import { basename, join, resolve, dirname, sep } from 'node:path'
|
|
14
|
-
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
15
15
|
import { fileURLToPath } from 'node:url'
|
|
16
16
|
import os from 'node:os'
|
|
17
17
|
|
|
@@ -336,12 +336,26 @@ export const PLATFORM_MANAGED_FILENAME = '.sillyspec-platform-managed'
|
|
|
336
336
|
|
|
337
337
|
export function writePlatformPointer(cwd, platformOpts, extra = {}) {
|
|
338
338
|
if (!platformOpts || (!platformOpts.specRoot && !platformOpts.runtimeRoot)) return false
|
|
339
|
+
// HUB-05:合并保留既有生命周期字段。scan 完成时指针被写入 status/completedAt/scanStatus
|
|
340
|
+
//(complete-handlers),但下一次任何平台模式 run(含只读 --status)都会重写指针——
|
|
341
|
+
// 恢复链只回填 specRoot 等四字段,不保留 status → 指针永远回 active、isPointerStale
|
|
342
|
+
// 恒 false、`platform pointer --cleanup` 的 STALE 分支不可达。extra 显式传值仍可覆盖。
|
|
343
|
+
let preserved = {}
|
|
344
|
+
try {
|
|
345
|
+
const existing = JSON.parse(readFileSync(join(cwd, '.sillyspec-platform.json'), 'utf8'))
|
|
346
|
+
if (existing && typeof existing === 'object') {
|
|
347
|
+
for (const k of ['status', 'completedAt', 'scanStatus']) {
|
|
348
|
+
if (existing[k] !== undefined && extra[k] === undefined) preserved[k] = existing[k]
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch { /* 无既有指针/损坏 → 全新写入 */ }
|
|
339
352
|
const payload = {
|
|
340
353
|
specRoot: platformOpts.specRoot || null,
|
|
341
354
|
runtimeRoot: platformOpts.runtimeRoot || null,
|
|
342
355
|
workspaceId: platformOpts.workspaceId || null,
|
|
343
356
|
scanRunId: platformOpts.scanRunId || null,
|
|
344
357
|
savedAt: new Date().toISOString(),
|
|
358
|
+
...preserved,
|
|
345
359
|
...extra,
|
|
346
360
|
}
|
|
347
361
|
// 声明文件字段集独立于指针 payload(D-E@v2 四字段:无 status/savedAt/scanRunId——
|
|
@@ -353,8 +367,14 @@ export function writePlatformPointer(cwd, platformOpts, extra = {}) {
|
|
|
353
367
|
declaredAt: new Date().toISOString(),
|
|
354
368
|
}
|
|
355
369
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
370
|
+
// platform-scan.json 只在 specRoot 存在时可写(坑 pointer-runtime-root-only-null-join):
|
|
371
|
+
// runtimeRoot-only 平台组合(command.js 明确允许)下 specRoot 为 null,此前 join(null,…)
|
|
372
|
+
// 抛 ERR_INVALID_ARG_TYPE 落入总 catch → 指针与接管声明也一并静默未写,双入口 fail-closed
|
|
373
|
+
// 对该组合整体失效。分步写:specRoot 缺失只跳过 platform-scan.json,指针/声明照写。
|
|
374
|
+
if (platformOpts.specRoot) {
|
|
375
|
+
mkdirSync(join(platformOpts.specRoot, '.runtime'), { recursive: true })
|
|
376
|
+
writeFileSync(join(platformOpts.specRoot, '.runtime', 'platform-scan.json'), JSON.stringify(payload, null, 2) + '\n')
|
|
377
|
+
}
|
|
358
378
|
writeFileSync(join(cwd, '.sillyspec-platform.json'), JSON.stringify(payload, null, 2) + '\n')
|
|
359
379
|
writeFileSync(join(cwd, PLATFORM_MANAGED_FILENAME), JSON.stringify(declaration, null, 2) + '\n')
|
|
360
380
|
return true
|
|
@@ -415,54 +435,90 @@ export function resolveChangeDir(cwd, progress, specDir = null) {
|
|
|
415
435
|
// sync 总超时熔断:sync.js 是 best-effort 后台回传(每请求已有 10s 超时),但 sync() 可能串行多次
|
|
416
436
|
// fetchJson 累积等待、阻塞 --done。给 8s 总超时,超时放弃(best-effort,失败不影响正确性,下次 --done 重试)。
|
|
417
437
|
// 历史痛点:--done 在 sync 慢时体感 hang,用户被迫用外部 timeout 兜底。
|
|
438
|
+
// HUB-09:熔断超时时必须 abort 底层在飞请求(opts.signal 一路传到 fetch)——否则熔断后请求
|
|
439
|
+
// 仍在跑,平台可能已接受推送而客户端当作超时放弃(spec 树推送无 base_ts 自愈兜底)。
|
|
418
440
|
const SYNC_TOTAL_TIMEOUT_MS = 8_000
|
|
419
441
|
|
|
442
|
+
/**
|
|
443
|
+
* 熔断 race + 确定性取消:超时触发 controller.abort() 并 resolve,在飞 fetch 收到
|
|
444
|
+
* AbortError 后自然失败(各调用方 catch 返回降级值,进程不挂起)。
|
|
445
|
+
* @param {() => Promise} op 接收 { signal } 的异步操作
|
|
446
|
+
* @param {number} timeoutMs 熔断时长(测试可缩短)
|
|
447
|
+
*/
|
|
448
|
+
async function raceWithAbort(op, timeoutMs = SYNC_TOTAL_TIMEOUT_MS) {
|
|
449
|
+
const controller = new AbortController()
|
|
450
|
+
let timer
|
|
451
|
+
try {
|
|
452
|
+
await Promise.race([
|
|
453
|
+
op({ signal: controller.signal }),
|
|
454
|
+
new Promise((resolve) => { timer = setTimeout(() => { controller.abort(); resolve(null) }, timeoutMs) }),
|
|
455
|
+
])
|
|
456
|
+
} finally {
|
|
457
|
+
clearTimeout(timer)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
420
461
|
// quick 会话 ID 形态(quick-<hex8>)。与 run/command.js QUICK_SID_RE、progress.js
|
|
421
462
|
// initChange 的跳过实体目录判断同源;shared.js 不反向 import command.js(重模块),
|
|
422
463
|
// 局部复制正则保持轻载——改形态时三处同改。
|
|
423
464
|
const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
|
|
424
465
|
|
|
425
|
-
export async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
466
|
+
export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
426
467
|
// 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
|
|
427
468
|
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
428
469
|
try {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const syncMod = await import('../sync.js')
|
|
437
|
-
let timer
|
|
438
|
-
try {
|
|
439
|
-
await Promise.race([
|
|
440
|
-
syncMod.syncSpecTreeOnly(changeName, cwd),
|
|
441
|
-
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
442
|
-
])
|
|
443
|
-
} finally {
|
|
444
|
-
clearTimeout(timer)
|
|
445
|
-
}
|
|
446
|
-
}
|
|
470
|
+
// ql-20260818-011:quick 会话按设计无实体变更目录(progress.js initChange 同款
|
|
471
|
+
// 跳过建目录),progress/四件套上行对它是孤儿数据;但 spec 树增量(QUICKLOG/
|
|
472
|
+
// 模块文档的上行通道)以服务器清单为锚,与变更目录无关——降级只推 spec 树。
|
|
473
|
+
if (changeName && QUICK_SID_RE.test(changeName) && !existsSync(join(cwd, '.sillyspec', 'changes', changeName))) {
|
|
474
|
+
// shared.js 在 src/run/,sync.js 在 src/ → 退一层
|
|
475
|
+
const syncMod = await import('../sync.js')
|
|
476
|
+
await raceWithAbort((sig) => syncMod.syncSpecTreeOnly(changeName, cwd, sig), opts.timeoutMs)
|
|
447
477
|
return
|
|
448
478
|
}
|
|
479
|
+
// 目录缺失的非 quick 变更不再静默 return(archive-final-state-sync):归档把
|
|
480
|
+
// changes/<name>/ 移到 changes/archive/<name>/ 并注销 DB 行后,第 4/5 步收尾的
|
|
481
|
+
// triggerSync 仍会触发——DB 里的终态(steps 完成态 + status='archived')与文件树
|
|
482
|
+
// 移动必须推平台,否则平台进度永远停在移动前的最后一步。sync() 内部对目录缺失
|
|
483
|
+
// 本就是 warn-and-continue(serializeForSync 从 DB 读不依赖目录);真实拼错名时
|
|
484
|
+
// 无 DB 行,sync() 在发出任何 HTTP 前即 return,仅多一行 warn(未连接平台则完全静默)。
|
|
449
485
|
// shared.js 在 src/run/,sync.js 在 src/ → 退一层
|
|
450
486
|
const syncMod = await import('../sync.js')
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
await Promise.race([
|
|
454
|
-
syncMod.sync(changeName, cwd),
|
|
455
|
-
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
456
|
-
])
|
|
457
|
-
} finally {
|
|
458
|
-
clearTimeout(timer) // sync 先完成则清掉未触发的 timer,避免泄漏
|
|
459
|
-
}
|
|
487
|
+
await raceWithAbort((sig) => syncMod.sync(changeName, cwd, sig), opts.timeoutMs)
|
|
460
488
|
} catch (e) {
|
|
461
489
|
// sync.js 不存在或同步失败,静默跳过
|
|
462
490
|
console.warn('⚠️ 同步失败:', e.message)
|
|
463
491
|
}
|
|
464
492
|
}
|
|
465
493
|
|
|
494
|
+
/**
|
|
495
|
+
* 自动 pull 节流(体检 PERF-02):triggerPull/triggerPullActiveChange 在每条 stage 命令
|
|
496
|
+
* 启动时注入,agent 命令往往是秒级连发——每次都发 HTTP GET(daemon 挂/慢时单命令最多
|
|
497
|
+
* 阻塞 8s)。跨进程 marker 节流:10s 内已有自动 pull 则跳过(手动 `platform pull` 不走
|
|
498
|
+
* 此门,语义不变)。错过窗口的平台更新由下一条 >10s 的命令或 push 409 自愈兜底。
|
|
499
|
+
*/
|
|
500
|
+
const AUTO_PULL_THROTTLE_MS = 10_000
|
|
501
|
+
|
|
502
|
+
function _autoPullThrottlePath(cwd) {
|
|
503
|
+
return join(cwd, '.sillyspec', '.runtime', 'auto-pull-throttle.json')
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function _autoPullRecently(cwd) {
|
|
507
|
+
try {
|
|
508
|
+
const t = JSON.parse(readFileSync(_autoPullThrottlePath(cwd), 'utf8'))
|
|
509
|
+
return typeof t.at === 'number' && Date.now() - t.at < AUTO_PULL_THROTTLE_MS
|
|
510
|
+
} catch {
|
|
511
|
+
return false
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function _stampAutoPull(cwd) {
|
|
516
|
+
try {
|
|
517
|
+
mkdirSync(dirname(_autoPullThrottlePath(cwd)), { recursive: true })
|
|
518
|
+
writeFileSync(_autoPullThrottlePath(cwd), JSON.stringify({ at: Date.now() }) + '\n', 'utf8')
|
|
519
|
+
} catch { /* best-effort:节流失效仅退化为逐次 pull */ }
|
|
520
|
+
}
|
|
521
|
+
|
|
466
522
|
/**
|
|
467
523
|
* 触发 pull(下行同步,task-10 / D-009 / FR-04 / FR-06)。
|
|
468
524
|
* 复用 triggerSync 的 8s 熔断与 Best Effort 语义;未连接平台静默跳过(与现状一致)。
|
|
@@ -474,23 +530,18 @@ export async function triggerSync(cwd, changeName, platformOpts = {}) {
|
|
|
474
530
|
* @param {string} changeName - 当前活跃变更(多变更时传 null 跳过,避免误拉)
|
|
475
531
|
* @param {object} [platformOpts] - 平台模式 opts(specRoot/runtimeRoot 存在则跳过,走平台自有链路)
|
|
476
532
|
*/
|
|
477
|
-
export async function triggerPull(cwd, changeName, platformOpts = {}) {
|
|
533
|
+
export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
478
534
|
// 平台模式(SillyHub)走自己的链路,跳过
|
|
479
535
|
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return
|
|
480
536
|
try {
|
|
481
537
|
const syncMod = await import('../sync.js')
|
|
482
538
|
const sm = new syncMod.SyncManager(cwd)
|
|
483
|
-
//
|
|
539
|
+
// 未连接平台静默跳过(本地独立用户合法状态,不噪音)——节流 stamp 必须在此之后:
|
|
540
|
+
// 否则未连接的项目每次 run 都写 .sillyspec/.runtime/ 标记(凭空造目录)
|
|
484
541
|
if (!sm._getPlatform()) return
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
sm.pull(changeName, { skipIfLocalDirty: true }),
|
|
489
|
-
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
490
|
-
])
|
|
491
|
-
} finally {
|
|
492
|
-
clearTimeout(timer)
|
|
493
|
-
}
|
|
542
|
+
if (_autoPullRecently(cwd)) return
|
|
543
|
+
_stampAutoPull(cwd)
|
|
544
|
+
await raceWithAbort((sig) => sm.pull(changeName, { skipIfLocalDirty: true, signal: sig.signal }), opts.timeoutMs)
|
|
494
545
|
} catch (e) {
|
|
495
546
|
// pull 失败静默跳过(Best Effort,失败不影响正确性)
|
|
496
547
|
console.warn('⚠️ 拉取失败:', e.message)
|
|
@@ -513,6 +564,10 @@ export async function triggerPullActiveChange(cwd, platformOpts = {}) {
|
|
|
513
564
|
} catch {
|
|
514
565
|
return
|
|
515
566
|
}
|
|
567
|
+
// PERF-02 节流(连接确认后才 stamp——未连接项目不得凭空写 .sillyspec/.runtime/):
|
|
568
|
+
// 与 triggerPull 共用同一 marker(本函数与它在同一命令启动链上互斥出现)
|
|
569
|
+
if (_autoPullRecently(cwd)) return
|
|
570
|
+
_stampAutoPull(cwd)
|
|
516
571
|
let cn = null
|
|
517
572
|
try {
|
|
518
573
|
const { ProgressManager } = await import('../progress.js')
|
|
@@ -523,29 +578,48 @@ export async function triggerPullActiveChange(cwd, platformOpts = {}) {
|
|
|
523
578
|
// progress 不可达则跳过(Best Effort)
|
|
524
579
|
}
|
|
525
580
|
if (!cn) return
|
|
526
|
-
// 已确认连接 + 单活跃变更,调 pull(复用
|
|
581
|
+
// 已确认连接 + 单活跃变更,调 pull(复用 8s 熔断 + abort(HUB-09);skipIfLocalDirty 保守守卫
|
|
527
582
|
// 同 triggerPull——本地脏时跳过 import,防平台旧快照覆盖本地领先进度,ql-20260818-008)
|
|
528
583
|
try {
|
|
529
|
-
|
|
530
|
-
try {
|
|
531
|
-
await Promise.race([
|
|
532
|
-
sm.pull(cn, { skipIfLocalDirty: true }),
|
|
533
|
-
new Promise((resolve) => { timer = setTimeout(resolve, SYNC_TOTAL_TIMEOUT_MS) }),
|
|
534
|
-
])
|
|
535
|
-
} finally {
|
|
536
|
-
clearTimeout(timer)
|
|
537
|
-
}
|
|
584
|
+
await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal }))
|
|
538
585
|
} catch (e) {
|
|
539
586
|
console.warn('⚠️ 拉取失败:', e.message)
|
|
540
587
|
}
|
|
541
588
|
}
|
|
542
589
|
|
|
590
|
+
/**
|
|
591
|
+
* 审批状态 unknown(体检 HUB-07)的统一处置:醒目多行警告 + 落 .runtime/approval-unknown.log 留痕。
|
|
592
|
+
*
|
|
593
|
+
* 语义边界:unknown = 已连接平台但 404/断网/超时/非 JSON——**不是审批通过**。放行是
|
|
594
|
+
* fail-open 的既定取舍(网络故障不应硬阻断每一条 execute),但静默放行会让团队审批
|
|
595
|
+
* 形同虚设且无迹可查;至少要(a)让操作者当场看见(b)留下可审计记录。
|
|
596
|
+
* 留痕选 .runtime 日志而非 QUICKLOG:QUICKLOG 是 quick 会话的任务记录,run 流程的
|
|
597
|
+
* 审批事件写进去会污染 quick 语义与边界审计。
|
|
598
|
+
* @param {string} cwd
|
|
599
|
+
* @param {string} changeName
|
|
600
|
+
* @param {string} reason checkApproval 返回的 reason
|
|
601
|
+
*/
|
|
602
|
+
export function warnApprovalUnknown(cwd, changeName, reason) {
|
|
603
|
+
console.warn('')
|
|
604
|
+
console.warn('⚠️⚠️⚠️══════════════════════════════════════════════')
|
|
605
|
+
console.warn(`⚠️ 审批状态未知:变更「${changeName}」无法核实审批(${reason || '请求失败'})`)
|
|
606
|
+
console.warn('⚠️ 本次 execute 按 fail-open 放行——这不是审批通过;平台恢复后请到平台复核审批记录')
|
|
607
|
+
console.warn('⚠️ 排查:平台服务是否可达(sillyspec platform status)/ 网络代理;确认无需审批可 --skip-approval 显式跳过')
|
|
608
|
+
console.warn('⚠️ 本次放行已记录:.sillyspec/.runtime/approval-unknown.log')
|
|
609
|
+
console.warn('⚠️⚠️══════════════════════════════════════════════')
|
|
610
|
+
try {
|
|
611
|
+
const logDir = join(cwd, '.sillyspec', '.runtime')
|
|
612
|
+
mkdirSync(logDir, { recursive: true })
|
|
613
|
+
appendFileSync(join(logDir, 'approval-unknown.log'),
|
|
614
|
+
`${new Date().toISOString()} | change=${changeName} | reason=${reason || 'unknown'}\n`, 'utf8')
|
|
615
|
+
} catch { /* 留痕失败不阻断流程(警告横幅已输出) */ }
|
|
616
|
+
}
|
|
617
|
+
|
|
543
618
|
/**
|
|
544
619
|
* 审批检查:execute 阶段启动前检查(W6 Step8a 从 run.js 搬入,runStage + runAutoMode 共用)。
|
|
545
620
|
* 平台模式走自己的链路,跳过;否则 await import sync.js。
|
|
546
621
|
* @returns {{ status: string, reason?: string } | null}
|
|
547
|
-
*/
|
|
548
|
-
export async function checkApproval(cwd, changeName, platformOpts = {}) {
|
|
622
|
+
*/export async function checkApproval(cwd, changeName, platformOpts = {}) {
|
|
549
623
|
// 平台模式不需要 CLI 内置审批检查
|
|
550
624
|
if (platformOpts?.specRoot || platformOpts?.runtimeRoot) return null
|
|
551
625
|
try {
|
|
@@ -774,7 +848,11 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
774
848
|
]
|
|
775
849
|
|
|
776
850
|
for (const entry of currentEntries) {
|
|
777
|
-
|
|
851
|
+
// porcelain 两字符状态码(X=暂存区 Y=工作区)不能先 trim:' D'.trim()==='D' 使第二分支恒假、
|
|
852
|
+
// 'DD'/'AD' 等 trim 后不等于 'D' 而漏检——--allow-delete 门对此类删除静默放行(体检 BUG-04)。
|
|
853
|
+
// 用原始两字符判定任一侧为 D 即删除。
|
|
854
|
+
const rawStatus = entry.slice(0, 2)
|
|
855
|
+
const status = rawStatus.trim()
|
|
778
856
|
const file = parsePorcelainPath(entry) // 已去引号/处理 rename/归一化,修正首行丢首字符
|
|
779
857
|
if (!file) continue
|
|
780
858
|
|
|
@@ -786,7 +864,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
786
864
|
if (isBaselineFile(file)) continue
|
|
787
865
|
|
|
788
866
|
result.changedFiles.push(file)
|
|
789
|
-
if (
|
|
867
|
+
if (rawStatus[0] === 'D' || rawStatus[1] === 'D') result.deletedFiles.push(file)
|
|
790
868
|
if (status === '??') result.newFiles.push(file)
|
|
791
869
|
|
|
792
870
|
// 检查是否命中 baseline protected files
|
|
@@ -841,13 +919,17 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
841
919
|
// 致 flag 对 status 失效。)
|
|
842
920
|
if ((!forceBaseline && result.baselineHit.length > 0) || (!allowDelete && result.deletedFiles.length > 0) || result.reasons.some(r => r.startsWith('危险') || r.startsWith('删除'))) {
|
|
843
921
|
result.status = 'blocked'
|
|
844
|
-
} else if ((!allowNew && result.newFiles.
|
|
922
|
+
} else if ((!allowNew && result.newFiles.some(f => !isQuickMetadata(f, guard.linkedChanges))) || (allowedFiles.length > 0 && result.reasons.some(r => r.startsWith('超出')))) {
|
|
923
|
+
// newFiles 按元数据过滤后判定(坑 quick-metadata-warning-no-reason):.sillyspec/ 被 git
|
|
924
|
+
// 跟踪的仓库里 quick 新建的 quicklog/*.md 是合法元数据,不过滤会 status=warning 而
|
|
925
|
+
// reasons 为空(reasons 侧已过滤),打印空原因的 WARNING 噪声。
|
|
845
926
|
result.status = 'warning'
|
|
846
927
|
}
|
|
847
928
|
|
|
848
|
-
// quicklog
|
|
929
|
+
// quicklog 存在性检查(specBase 感知:平台模式 quicklog 写在 specRoot 下,stage.js
|
|
930
|
+
// allocateQuicklogEntry 同源;硬编码 cwd/.sillyspec 会恒误报「目录不存在」抬 warning)
|
|
849
931
|
try {
|
|
850
|
-
const quicklogDir = join(cwd, '.sillyspec', 'quicklog')
|
|
932
|
+
const quicklogDir = join(specBase || join(cwd, '.sillyspec'), 'quicklog')
|
|
851
933
|
if (existsSync(quicklogDir)) {
|
|
852
934
|
const qlFiles = readdirSync(quicklogDir).filter(f => f.endsWith('.md'))
|
|
853
935
|
if (qlFiles.length === 0) {
|
|
@@ -885,8 +967,12 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
885
967
|
const mdChanged = result.changedFiles.filter((f) => f.endsWith('.md') && !deletedSet.has(f) && !isQuickMetadata(f, guard.linkedChanges))
|
|
886
968
|
if (mdChanged.length > 0) {
|
|
887
969
|
try {
|
|
888
|
-
const { runDocsCheck } = await import('../docs-check.js')
|
|
889
|
-
|
|
970
|
+
const { runDocsCheck, readDocsCheckConfig } = await import('../docs-check.js')
|
|
971
|
+
// HUB-06:补传 crossRepoRoots(与 docs gate / index.js docs check 同口径)——
|
|
972
|
+
// 否则 repo:// 跨仓引用在这条链路恒跳过;无 local.yaml 配置时 undefined = 不启用
|
|
973
|
+
let quickCfg = {}
|
|
974
|
+
try { quickCfg = readDocsCheckConfig(cwd) || {} } catch { quickCfg = {} }
|
|
975
|
+
const dc = runDocsCheck({ projectRoot: cwd, docs: mdChanged, crossRepoRoots: quickCfg.crossRepoRoots })
|
|
890
976
|
if (!dc.ok) {
|
|
891
977
|
result.reasons.push(`本次改动文档含 ${dc.invalid.length} 处失效 file:line 引用(sillyspec docs check 可复现)`)
|
|
892
978
|
result.docsCheckHint = { invalid: dc.invalid.length, total: dc.total }
|
|
@@ -906,7 +992,11 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
906
992
|
const srcChangedFiles = result.changedFiles.filter((f) => f.startsWith('src/') && !deletedSet.has(f))
|
|
907
993
|
if (srcChangedFiles.length > 0) {
|
|
908
994
|
try {
|
|
909
|
-
const { collectDocRefs, runDocsCheck } = await import('../docs-check.js')
|
|
995
|
+
const { collectDocRefs, runDocsCheck, readDocsCheckConfig } = await import('../docs-check.js')
|
|
996
|
+
// HUB-06:活文档(platform-interface-map.md 等最可能写 repo:// 引用的文档)同样补传
|
|
997
|
+
// crossRepoRoots;配置读一次提到循环外(per-cwd 恒同)
|
|
998
|
+
let livingCfg = {}
|
|
999
|
+
try { livingCfg = readDocsCheckConfig(cwd) || {} } catch { livingCfg = {} }
|
|
910
1000
|
const livingDocs = await resolveLivingDocs(cwd)
|
|
911
1001
|
const files = []
|
|
912
1002
|
const docs = []
|
|
@@ -918,7 +1008,7 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
918
1008
|
if (hit.length === 0) continue
|
|
919
1009
|
files.push(...hit)
|
|
920
1010
|
docs.push(docRel)
|
|
921
|
-
const r = runDocsCheck({ projectRoot: cwd, docs: [docRel], keywordAssert: true })
|
|
1011
|
+
const r = runDocsCheck({ projectRoot: cwd, docs: [docRel], keywordAssert: true, crossRepoRoots: livingCfg.crossRepoRoots })
|
|
922
1012
|
for (const x of r.invalid) invalidAll.push({ ...x, doc: docRel })
|
|
923
1013
|
}
|
|
924
1014
|
const invalid = matchInvalidRefsToChanged(invalidAll, srcChangedFiles)
|
|
@@ -1262,6 +1352,17 @@ export async function getStageSteps(stageName, cwd, progress, specDir = null) {
|
|
|
1262
1352
|
const changeDir = resolveChangeDir(cwd, progress, specDir)
|
|
1263
1353
|
return buildPlanSteps(changeDir)
|
|
1264
1354
|
}
|
|
1355
|
+
if (stageName === 'scan') {
|
|
1356
|
+
// quick 档感知(坑 scan-quick-profile-step-mismatch):scan 的 scanProfile 在首进程
|
|
1357
|
+
// runStage 里算出并持久化(stageData.scanProfile),quick 档把 11 步注册表裁成 3 步。
|
|
1358
|
+
// 此处恒返回注册表会让后续进程 ensureStageSteps 见 3 vs 11 触发漂移重播种(quick 已完成
|
|
1359
|
+
// 步丢失、从未执行的注册表步骤被 --done),同进程 outputStep 也用错位 defSteps 渲染。
|
|
1360
|
+
// quick 档 → 与 applyScanProfileSteps 同源返回 3 步表;standard/deep 与注册表同长度零影响。
|
|
1361
|
+
if (progress?.stages?.scan?.scanProfile?.mode === 'quick') {
|
|
1362
|
+
const { buildQuickScanSteps } = await import('./scan-profile.js')
|
|
1363
|
+
return buildQuickScanSteps()
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1265
1366
|
const def = stageRegistry[stageName]
|
|
1266
1367
|
return def ? def.steps : null
|
|
1267
1368
|
}
|
package/src/run/stage.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { join, dirname } from 'node:path'
|
|
20
20
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
21
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
|
-
import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps } from './shared.js'
|
|
22
|
+
import { resolveSpecDir, resolveChangeDir, resolveRuntimeRoot, resolveQuickSessionsDir, triggerSync, safeGit, parsePorcelainPath, formatWaitOptions, checkApproval, getStageSteps, warnApprovalUnknown } from './shared.js'
|
|
23
23
|
import { computeScanProfile, applyScanProfileSteps, executeScanPreflight, executeScanPostcheck } from './scan-profile.js'
|
|
24
24
|
import { outputStep } from './prompt.js'
|
|
25
25
|
import { allocateQuicklogEntry, deriveTitleFromLinkedChange, sanitizeDesc } from '../quicklog.js'
|
|
@@ -56,6 +56,11 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
56
56
|
console.log(`⏳ 变更 ${changeName} 的执行审批待处理中...`)
|
|
57
57
|
console.log(' 提示:使用 --skip-approval 跳过审批检查')
|
|
58
58
|
}
|
|
59
|
+
// HUB-07:unknown(连接了平台但 404/断网/非 JSON)此前静默落空(fail-open 无提示)——
|
|
60
|
+
// 醒目警告 + 留痕,放行但可审计
|
|
61
|
+
if (approval.status === 'unknown') {
|
|
62
|
+
warnApprovalUnknown(cwd, changeName, approval.reason)
|
|
63
|
+
}
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -118,6 +123,10 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
118
123
|
`请检查该路径是否为普通文件/只读,清理后重跑(sillyspec run execute --change ${changeName} --skip-approval)`)
|
|
119
124
|
}
|
|
120
125
|
writeFileSync(runIdFile, currentExecuteRunId + '\n')
|
|
126
|
+
// change 归属戳(坑 worktree-cleanup-marker-chain 根治):run 目录自带变更身份,
|
|
127
|
+
// marker 断裂(worktree cleanup / 归档清理 / 并行误删)后按戳归属,不再错配其他变更的 run
|
|
128
|
+
const { stampExecuteRunChange } = await import('../task-review.js')
|
|
129
|
+
stampExecuteRunChange(runtimeRoot, currentExecuteRunId, changeName)
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -244,6 +253,25 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
244
253
|
if (existingGuard) {
|
|
245
254
|
// 跨进程重入:复用已分配的 ql-ID,跳过 baseline 重捕与分配(幂等)
|
|
246
255
|
progress.quickGuard = existingGuard
|
|
256
|
+
// 中途追加边界:会话中途发现要改启动声明外的文件,此前 resume 会把 --files 静默丢弃、
|
|
257
|
+
// 边界冻结在启动时刻,只能靠事后归属/审计行兜底。恢复时带 --files 即并入 allowedFiles
|
|
258
|
+
// (追加不替换、去重保序),同时点录 allowedFilesHash 供同文件并发检测(文件不存在跳过,
|
|
259
|
+
// 与启动同语义)。--done 审计直读 guard.json(complete-handlers §4.6),追加即被归属消费。
|
|
260
|
+
const resumeFiles = Array.isArray(quickOpts?.quickFiles) ? quickOpts.quickFiles.filter(Boolean) : []
|
|
261
|
+
if (resumeFiles.length > 0) {
|
|
262
|
+
const known = new Set(progress.quickGuard.allowedFiles || [])
|
|
263
|
+
const added = resumeFiles.filter(f => !known.has(f))
|
|
264
|
+
if (added.length > 0) {
|
|
265
|
+
progress.quickGuard.allowedFiles = [...(progress.quickGuard.allowedFiles || []), ...added]
|
|
266
|
+
progress.quickGuard.allowedFilesHash = { ...(progress.quickGuard.allowedFilesHash || {}) }
|
|
267
|
+
for (const f of added) {
|
|
268
|
+
try { progress.quickGuard.allowedFilesHash[f] = createHash('sha256').update(readFileSync(join(cwd, f))).digest('hex') }
|
|
269
|
+
catch { /* 预声明将新建的文件,与启动时 allowedFilesHash 同语义:不存在跳过 */ }
|
|
270
|
+
}
|
|
271
|
+
writeAtomicSync(guardFile, JSON.stringify(progress.quickGuard, null, 2))
|
|
272
|
+
console.log(`🛡️ quick 边界已追加: ${added.length} 个文件(累计 ${progress.quickGuard.allowedFiles.length} 个): ${added.join(', ')}`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
247
275
|
} else {
|
|
248
276
|
// baseline 采集用 safeGit(带 -c safe.directory,避免 linked worktree/容器异 uid/Windows 挂载点
|
|
249
277
|
// 下裸 `git status` 抛错)。safeGit 已消除 safe.directory 类失败;若仍失败(真非 git 目录等),
|
|
@@ -317,6 +345,16 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
317
345
|
if (allowDelete) parts.push('允许删除文件')
|
|
318
346
|
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
319
347
|
console.log(`📝 QUICKLOG 条目已创建: ${qlId}`)
|
|
348
|
+
// 缺 --input 且关联变更无可提取标题 → 条目落「(quick 任务)」占位标题,平台「快速修复」列表
|
|
349
|
+
// 默认隐藏进行中占位(task-06 口径),长会话全程不可见、语义标题要到最终 --done 才回填。
|
|
350
|
+
// 此刻会话刚起步零沉没成本,提示放弃重启带 --input 是最便宜的自愈点。
|
|
351
|
+
if (!quickDesc) {
|
|
352
|
+
console.warn('')
|
|
353
|
+
console.warn('⚠️ 本次 quick 未带 --input:QUICKLOG 条目将以占位标题「(quick 任务)」落盘')
|
|
354
|
+
console.warn(' 平台「快速修复」列表默认隐藏进行中的占位条目,语义标题要到最终 --done 才回填。')
|
|
355
|
+
console.warn(' 长任务建议放弃本会话、带一句话任务描述重启(旧会话 run quick --reset --change <sessionId> 重置):')
|
|
356
|
+
console.warn(' sillyspec run quick --input "<一句话任务描述>" [--linked-changes <a,b>] [--files <...>]')
|
|
357
|
+
}
|
|
320
358
|
// 回填 DB changes 行的 title + quicklog_id,让 quick-<hex> 可读、DB↔QUICKLOG 可对账。
|
|
321
359
|
// title 用 quickDesc(任务描述或关联变更标题)经 sanitizeDesc 压一行限长,与 QUICKLOG 条目标题同源。
|
|
322
360
|
try {
|
|
@@ -380,6 +418,8 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
380
418
|
await executeScanPostcheck(cwd, platformOpts, scanProfile)
|
|
381
419
|
} else if (cliAction === 'planPostcheck') {
|
|
382
420
|
await executePlanPostcheck(cwd, platformOpts, progress)
|
|
421
|
+
} else {
|
|
422
|
+
throw new Error(`noAI 步骤 ${stepName} 的未知 _cliAction: ${cliAction}——请在 stage.js 注册对应分支`)
|
|
383
423
|
}
|
|
384
424
|
stageData.steps[currentIdx].status = 'completed'
|
|
385
425
|
stageData.steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN', { hour12: false })
|
package/src/setup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
|
-
import { execSync
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { gitQuiet } from './git-helper.js';
|
|
4
5
|
import chalk from 'chalk';
|
|
5
6
|
import ora from 'ora';
|
|
6
7
|
import { checkbox, input } from '@inquirer/prompts';
|
|
@@ -359,16 +360,13 @@ export async function cmdSetup(dir, options = {}) {
|
|
|
359
360
|
// 凭据提交风险提示(sec-c):.claude/mcp.json / .cursor/mcp.json 是项目共享文件、通常随 git
|
|
360
361
|
// 提交,而 DB MCP 的 env 含明文连接串/密码。检测 git 跟踪状态,已跟踪/未忽略时显式警告。
|
|
361
362
|
if (selectedDb.length > 0) {
|
|
362
|
-
|
|
363
|
-
|
|
363
|
+
// QUAL-01 收口:裸 execFileSync → git-helper(-C 由 helper 统一注入)。
|
|
364
|
+
// 语义映射:ls-files --error-unmatch 命中(exit 0)= 已跟踪 → 警告;
|
|
365
|
+
// check-ignore -q 命中(exit 0)= 已忽略 → 不警告
|
|
366
|
+
if (gitQuiet(dir, ['ls-files', '--error-unmatch', path]) !== null) {
|
|
364
367
|
console.warn(chalk.yellow(` ⚠️ ${path} 已被 git 跟踪——其中数据库连接串/密码会随提交进入仓库,建议加入 .gitignore 或改用环境变量`));
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
try {
|
|
368
|
-
execFileSync('git', ['-C', dir, 'check-ignore', '-q', path], { stdio: 'pipe' });
|
|
369
|
-
} catch {
|
|
370
|
-
console.warn(chalk.yellow(` ⚠️ ${path} 含数据库连接串/密码且未被 .gitignore 忽略——提交前请先加入 .gitignore 或改用环境变量`));
|
|
371
|
-
}
|
|
368
|
+
} else if (gitQuiet(dir, ['check-ignore', '-q', path]) === null) {
|
|
369
|
+
console.warn(chalk.yellow(` ⚠️ ${path} 含数据库连接串/密码且未被 .gitignore 忽略——提交前请先加入 .gitignore 或改用环境变量`));
|
|
372
370
|
}
|
|
373
371
|
}
|
|
374
372
|
}
|
|
@@ -49,7 +49,6 @@ export class SillyHubMcpClient {
|
|
|
49
49
|
if (this._url && !/^https:\/\//i.test(this._url) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i.test(this._url)) {
|
|
50
50
|
console.warn(`[sillyhub-mcp] mcp.url 非 https(${this._url.slice(0, 60)}),Bearer token 将明文传输——请确认为受控环境`);
|
|
51
51
|
}
|
|
52
|
-
this._token = typeof t === 'string' ? t : '';
|
|
53
52
|
this._timeoutMs = typeof timeoutMs === 'number' && timeoutMs > 0
|
|
54
53
|
? timeoutMs
|
|
55
54
|
: DEFAULT_TIMEOUT_MS;
|
|
@@ -206,6 +205,13 @@ export class SillyHubMcpClient {
|
|
|
206
205
|
}
|
|
207
206
|
|
|
208
207
|
if (rpc.error) {
|
|
208
|
+
// HUB-11:session 过期第二种形态——HTTP 200 + JSON-RPC error -32600「Missing session」
|
|
209
|
+
// (api-reference 记载的形态;FastMCP 实测走 HTTP 400,两种都认)。落哨兵触发
|
|
210
|
+
// _sendRpc 重置重连;其余 error 维持 warn+null(如 -32602 Invalid params 是调用侧问题,
|
|
211
|
+
// 重连救不回,不误触发)
|
|
212
|
+
if (rpc.error.code === -32600 && /missing session/i.test(String(rpc.error.message || ''))) {
|
|
213
|
+
return SESSION_EXPIRED;
|
|
214
|
+
}
|
|
209
215
|
if (!quiet) console.warn(`[sillyhub-mcp] ${tag} JSON-RPC error: ${JSON.stringify(rpc.error)}`);
|
|
210
216
|
return null;
|
|
211
217
|
}
|
|
@@ -228,11 +234,8 @@ export class SillyHubMcpClient {
|
|
|
228
234
|
|
|
229
235
|
/**
|
|
230
236
|
* MCP initialize 握手:POST initialize → 从响应 header(或 body _meta)读 mcp-session-id。
|
|
231
|
-
* 成功存 this._sessionId
|
|
232
|
-
*
|
|
233
|
-
* 注:协议要求 initialize 后发 notifications/initialized,本客户端只做探测
|
|
234
|
-
* (tools/list / 只读 tool),实测 FastMCP 不强制该通知即可用(2026-08-14 验证),
|
|
235
|
-
* 为省一次 RPC 不发;若未来 server 强校验再补。
|
|
237
|
+
* 成功存 this._sessionId 并补发 notifications/initialized(协议要求;FastMCP 实测不强制,
|
|
238
|
+
* best-effort 失败仅 warn);失败返回 false(best-effort,warn 不抛)。
|
|
236
239
|
* @returns {Promise<boolean>}
|
|
237
240
|
*/
|
|
238
241
|
async _initialize(quiet = false) {
|
|
@@ -299,6 +302,28 @@ export class SillyHubMcpClient {
|
|
|
299
302
|
return false;
|
|
300
303
|
}
|
|
301
304
|
this._sessionId = sid;
|
|
305
|
+
// MCP 2025-11-25 协议要求 initialize 后发 notifications/initialized(无 id 通知,server 不回包)。
|
|
306
|
+
// 2026-08-14 实测 FastMCP v1.29 不强制;补发是为防未来 server 强校验拒掉所有 tools/call。
|
|
307
|
+
// best-effort:失败仅 warn,不阻断(session 已建,宽松 server 仍可用)。
|
|
308
|
+
try {
|
|
309
|
+
const noteController = new AbortController();
|
|
310
|
+
const noteTimer = setTimeout(() => noteController.abort(), this._timeoutMs);
|
|
311
|
+
await fetch(this._endpoint, {
|
|
312
|
+
method: 'POST',
|
|
313
|
+
headers: {
|
|
314
|
+
Authorization: `Bearer ${this._token}`,
|
|
315
|
+
'Content-Type': 'application/json',
|
|
316
|
+
Accept: 'application/json, text/event-stream',
|
|
317
|
+
'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
|
|
318
|
+
'Mcp-Session-Id': this._sessionId,
|
|
319
|
+
},
|
|
320
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
|
|
321
|
+
signal: noteController.signal,
|
|
322
|
+
}).catch(() => { /* best-effort */ });
|
|
323
|
+
clearTimeout(noteTimer);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (!quiet) console.warn(`[sillyhub-mcp] notifications/initialized 发送失败(忽略): ${err && err.message ? err.message : err}`);
|
|
326
|
+
}
|
|
302
327
|
return true;
|
|
303
328
|
}
|
|
304
329
|
|
|
@@ -387,6 +412,49 @@ export class SillyHubMcpClient {
|
|
|
387
412
|
return null;
|
|
388
413
|
}
|
|
389
414
|
|
|
415
|
+
/**
|
|
416
|
+
* HUB-12a:tools/list 的完整 result(一次请求同时喂路径A schema 预热与 root_path 越界
|
|
417
|
+
* 校验——此前 listTools + getRootPath 各发一次同 method 请求,probe 链路重复 RPC)。
|
|
418
|
+
* best-effort:未配置/失败 → null 不抛。形态:{tools:[...], root_path?} 或裸数组。
|
|
419
|
+
*/
|
|
420
|
+
async listToolsWithMeta({ quiet = true } = {}) {
|
|
421
|
+
if (!this._configured) return null;
|
|
422
|
+
return this._sendRpc('tools/list', {}, { quiet, label: 'tools/list' });
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* HUB-12b:关闭 MCP session(streamable HTTP DELETE,2025-11-25 协议规定的会话终止方式)。
|
|
427
|
+
* CLI 短进程原本把 session 留给 server TTL 清理;长驻场景(未来 daemon 化)会积累半开
|
|
428
|
+
* session。best-effort:网络失败/无 session 返回 false 不抛;成功后清本地 session 态,
|
|
429
|
+
* 后续调用自动重新 initialize。
|
|
430
|
+
* @returns {Promise<boolean>} 是否发出了有效的 DELETE
|
|
431
|
+
*/
|
|
432
|
+
async close() {
|
|
433
|
+
if (!this._configured || !this._sessionId) return false;
|
|
434
|
+
const sid = this._sessionId;
|
|
435
|
+
this._sessionId = null; // 先清本地态:DELETE 失败也不留「以为还活着」的 session
|
|
436
|
+
try {
|
|
437
|
+
const controller = new AbortController();
|
|
438
|
+
const timer = setTimeout(() => controller.abort(), this._timeoutMs);
|
|
439
|
+
try {
|
|
440
|
+
const res = await fetch(this._endpoint, {
|
|
441
|
+
method: 'DELETE',
|
|
442
|
+
headers: {
|
|
443
|
+
Authorization: `Bearer ${this._token}`,
|
|
444
|
+
'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
|
|
445
|
+
'Mcp-Session-Id': sid,
|
|
446
|
+
},
|
|
447
|
+
signal: controller.signal,
|
|
448
|
+
});
|
|
449
|
+
return res.ok;
|
|
450
|
+
} finally {
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
}
|
|
453
|
+
} catch {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
390
458
|
/**
|
|
391
459
|
* best-effort 从 daemon 拿 workspace root_path(task-12 路径A 越界校验用)。
|
|
392
460
|
*
|
package/src/spec-dir-typo.js
CHANGED
|
@@ -13,30 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
import { dirname, basename, join } from 'path'
|
|
15
15
|
import { readdirSync } from 'fs'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* @param {string} a
|
|
20
|
-
* @param {string} b
|
|
21
|
-
* @returns {number}
|
|
22
|
-
*/
|
|
23
|
-
function levenshtein(a, b) {
|
|
24
|
-
const m = a.length
|
|
25
|
-
const n = b.length
|
|
26
|
-
if (m === 0) return n
|
|
27
|
-
if (n === 0) return m
|
|
28
|
-
let prev = Array.from({ length: n + 1 }, (_, j) => j)
|
|
29
|
-
let curr = new Array(n + 1)
|
|
30
|
-
for (let i = 1; i <= m; i++) {
|
|
31
|
-
curr[0] = i
|
|
32
|
-
for (let j = 1; j <= n; j++) {
|
|
33
|
-
const cost = a[i - 1].toLowerCase() === b[j - 1].toLowerCase() ? 0 : 1
|
|
34
|
-
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost)
|
|
35
|
-
}
|
|
36
|
-
;[prev, curr] = [curr, prev]
|
|
37
|
-
}
|
|
38
|
-
return prev[n]
|
|
39
|
-
}
|
|
16
|
+
// levenshtein 单一实现收敛到 run/shared.js(原本地副本与 shared.js 逐行相同);
|
|
17
|
+
// shared.js 不静态 import review 族,此处引入不成环
|
|
18
|
+
import { levenshtein } from './run/shared.js'
|
|
40
19
|
|
|
41
20
|
/**
|
|
42
21
|
* 检测 spec 目录拼写变体。
|