sillyspec 3.28.7 → 3.28.9
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-quick/SKILL.md +2 -0
- package/.husky/pre-push +3 -1
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/agent-session-log.js +446 -35
- package/src/change-risk-profile.js +20 -0
- package/src/config-cat.js +6 -3
- package/src/config-schema.js +32 -0
- package/src/constants.js +3 -0
- package/src/db.js +23 -6
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-check.js +137 -12
- package/src/docs-gate.js +108 -6
- package/src/doctor-diagnostics.js +243 -9
- package/src/friction-ledger.js +171 -0
- package/src/friction-tally.js +7 -2
- package/src/git-helper.js +6 -4
- package/src/hooks/worktree-guard.js +72 -30
- package/src/index.js +356 -81
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/local-detect.js +30 -15
- package/src/local-register.js +13 -4
- package/src/mcp-server.js +38 -8
- package/src/progress/change-registry.js +224 -43
- package/src/progress/consistency-doctor.js +11 -4
- package/src/progress/shared.js +3 -2
- package/src/progress/stage-machine.js +3 -1
- package/src/progress/step-store.js +30 -17
- package/src/progress.js +123 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +199 -32
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +124 -15
- package/src/run/complete-handlers.js +514 -24
- package/src/run/complete.js +82 -25
- package/src/run/concurrent-detect.js +99 -1
- package/src/run/gate-snapshot.js +115 -5
- package/src/run/gates.js +85 -14
- package/src/run/prompt.js +143 -6
- package/src/run/quick-audit.js +136 -15
- package/src/run/shared.js +174 -20
- package/src/run/stage.js +6 -0
- package/src/run/verify-quality-scan.js +14 -3
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +365 -35
- package/src/spec-sync.js +19 -14
- package/src/stage-contract-spec.js +4 -3
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +29 -12
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +91 -4
- package/src/stages/knowledge.js +52 -3
- package/src/stages/plan-postcheck.js +57 -12
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +201 -11
- package/src/task-review.js +333 -27
- package/src/verify-facts-schema.js +10 -3
- package/src/verify-postcheck.js +41 -6
- package/src/verify-probes.js +348 -7
- package/src/worktree-apply.js +594 -62
- package/src/worktree-cross.js +12 -18
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +255 -30
- package/src/wt-commit.js +128 -0
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/sa-rename-probe/src/new.js +0 -4
package/src/agent-session-log.js
CHANGED
|
@@ -35,6 +35,13 @@
|
|
|
35
35
|
* (链路 A 进度同步在平台模式跳过是因为 daemon 自有进度回传链路;agent 日志没有
|
|
36
36
|
* daemon 链路,本上报就是它的主通道)。env SILLYSPEC_AGENT_LOG_PUSH=0 可关。
|
|
37
37
|
*
|
|
38
|
+
* 会话身份锚定(2026-09-11-agent-log-attribution-refactor):探测之上叠 own 集合解析
|
|
39
|
+
* (resolveOwnLogPaths)——per harness 锚定「本 run 所属 agent 会话」的日志文件(claude-code
|
|
40
|
+
* =CLAUDE_SESSION_ID 精确/project 目录最新;zcode=db.sqlite 主会话+parent_id 子代理链,
|
|
41
|
+
* 不可用回退最新 cwd 匹配主文件(workdir 标记校验,不匹配不打标);pi/dsh=safePath 目录直算;
|
|
42
|
+
* codex=rollout 首行 cwd 最新;SILLYSPEC_AGENT_LOG 覆盖=own)。ctx 打标与平台推送只作用于 own 条目(D-001/D-007),
|
|
43
|
+
* quick/change 双向互斥(D-006@v2);cursor/opencode(loose)不锚定不打标不推送仅本地留底。
|
|
44
|
+
*
|
|
38
45
|
* 铁律:全程 best-effort——探测不到不发不写、任何失败只 warn 一行绝不阻断 run 主流程。
|
|
39
46
|
* 只上报路径 + 元信息(session_id/format/originator),不读日志内容(解析归平台侧),
|
|
40
47
|
* 不引入新依赖。
|
|
@@ -42,10 +49,17 @@
|
|
|
42
49
|
import { existsSync, readdirSync, statSync, readFileSync, mkdirSync, openSync, readSync, closeSync } from 'fs';
|
|
43
50
|
import { join, isAbsolute } from 'path';
|
|
44
51
|
import { homedir } from 'os';
|
|
52
|
+
import { createRequire } from 'module';
|
|
53
|
+
import { pathToFileURL } from 'url';
|
|
45
54
|
import { writeAtomicSync } from './fs-atomic.js';
|
|
46
55
|
import { withFileLock } from './quicklog.js';
|
|
47
56
|
import { resolveRuntimeRoot } from './run/shared.js';
|
|
48
57
|
|
|
58
|
+
// node:sqlite 惰性同步加载通道(zcode 锚定读 db.sqlite 用):ESM 下无同步动态 import,
|
|
59
|
+
// createRequire 在调用点 require——node:sqlite 不可用(Node<22.13/构建裁剪)时 try/catch
|
|
60
|
+
// 降级回退链,绝不在模块顶层 import 硬依赖。
|
|
61
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
62
|
+
|
|
49
63
|
export const AGENT_LOG_SCHEMA_VERSION = 1;
|
|
50
64
|
export const AGENT_LOG_ARTIFACT_FILENAME = 'agent-session-log.json';
|
|
51
65
|
/** daemon/用户显式指定 agent 日志路径的 env 名(绝对路径;相对路径忽略)。 */
|
|
@@ -493,21 +507,326 @@ const HARNESS_DETECTORS = [
|
|
|
493
507
|
{ name: 'opencode', tier: 'loose', detect: detectOpencode },
|
|
494
508
|
];
|
|
495
509
|
|
|
510
|
+
// ── 会话身份锚定器(2026-09-11-agent-log-attribution-refactor,D-001/D-008)──
|
|
511
|
+
//
|
|
512
|
+
// own 集合 = 本 run 所属 agent 会话的日志文件(锚定主会话 + zcode parent_id 子代理链)。
|
|
513
|
+
// 用途:① ctx 打标收敛——recordAgentLogInvocation 仅对 detected∩own 条目写 ctx/计数;
|
|
514
|
+
// ② 推送收敛(D-007)——push payload 只含 own 条目(留底仍全量);③ 探测上限豁免
|
|
515
|
+
// (DG-17)——own 文件被 MAX_PER_HARNESS 裁剪或 cwd 等值过滤挤出 detected 时定向补收
|
|
516
|
+
// (zcode worktree 子代理 directory=worktree≠仓库根,绕过 cwd 等值过滤是主场景)。
|
|
517
|
+
// 宁缺毋滥(D-001):锚定识别失败不打标,绝不用 cwd 猜测兜底打标。
|
|
518
|
+
// cursor/opencode(loose 档)无归属线索不锚定——不出现在 own 集,仅本地留底探测(DG-06)。
|
|
519
|
+
// anchor 对象与探测 entry 同构(除 mtime_ms 外字段一致),补收时只需补 stat。
|
|
520
|
+
|
|
521
|
+
/** env 覆盖(SILLYSPEC_AGENT_LOG 绝对路径):显式指定 = 调用方负责正确性,恒视为 own。 */
|
|
522
|
+
function anchorEnvOverride(ctx) {
|
|
523
|
+
const { cwdCandidates, env } = ctx;
|
|
524
|
+
const override = env[AGENT_LOG_ENV_OVERRIDE];
|
|
525
|
+
if (typeof override !== 'string' || !override.trim()) return [];
|
|
526
|
+
const p = override.trim();
|
|
527
|
+
if (!isAbsolute(p)) return [];
|
|
528
|
+
return [{
|
|
529
|
+
harness: 'env-override', format: p.endsWith('.jsonl') ? 'jsonl' : 'unknown', detected_via: 'env',
|
|
530
|
+
log_path: toPosix(p), agent_cwd: toPosix(cwdCandidates[0] || ''),
|
|
531
|
+
session_id: null, originator: null,
|
|
532
|
+
}];
|
|
533
|
+
}
|
|
534
|
+
|
|
496
535
|
/**
|
|
497
|
-
*
|
|
536
|
+
* claude-code 锚定:env CLAUDE_SESSION_ID → <claudeRoot>/projects/<slug(cwd)>/<sessionId>.jsonl
|
|
537
|
+
* 精确(cwd 候选逐个试,命中存在的);env 缺席/未命中回退 project 目录窗口内最新 mtime 的
|
|
538
|
+
* jsonl(DG-07)。与探测同 env 门控(CLAUDECODE/CLAUDE_CODE_ENTRYPOINT)——不在 claude 会话
|
|
539
|
+
* 内时不应认领其他窗口的会话文件。
|
|
540
|
+
*/
|
|
541
|
+
function anchorClaudeCode(ctx) {
|
|
542
|
+
const { cwdCandidates, env, homeDir, now, windowMs } = ctx;
|
|
543
|
+
if (!(env.CLAUDECODE === '1' || (typeof env.CLAUDE_CODE_ENTRYPOINT === 'string' && env.CLAUDE_CODE_ENTRYPOINT))) {
|
|
544
|
+
return [];
|
|
545
|
+
}
|
|
546
|
+
const claudeRoot = env.CLAUDE_CONFIG_DIR || join(homeDir, '.claude');
|
|
547
|
+
const projectsRoot = join(claudeRoot, 'projects');
|
|
548
|
+
const sessionId = typeof env.CLAUDE_SESSION_ID === 'string' ? env.CLAUDE_SESSION_ID.trim() : '';
|
|
549
|
+
if (sessionId) {
|
|
550
|
+
for (const rawCwd of cwdCandidates) {
|
|
551
|
+
if (!rawCwd) continue;
|
|
552
|
+
const exact = join(projectsRoot, mungeClaudeProjectDir(rawCwd), `${sessionId}.jsonl`);
|
|
553
|
+
if (!statSafe(exact)) continue;
|
|
554
|
+
return [{
|
|
555
|
+
harness: 'claude-code', format: 'claude-code-jsonl', detected_via: 'claude-code-own-anchor',
|
|
556
|
+
log_path: toPosix(exact), agent_cwd: toPosix(rawCwd), session_id: sessionId, originator: null,
|
|
557
|
+
}];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// 回退:各 cwd 候选 project 目录内窗口内最新者(同一会话 transcript 只有一个,取全局最新)
|
|
561
|
+
let best = null;
|
|
562
|
+
for (const rawCwd of cwdCandidates) {
|
|
563
|
+
if (!rawCwd) continue;
|
|
564
|
+
const files = listActiveFiles(join(projectsRoot, mungeClaudeProjectDir(rawCwd)), { now, windowMs, limit: 1 });
|
|
565
|
+
if (files.length && (!best || files[0].mtimeMs > best.mtimeMs)) best = { ...files[0], cwd: rawCwd };
|
|
566
|
+
}
|
|
567
|
+
if (!best) return [];
|
|
568
|
+
return [{
|
|
569
|
+
harness: 'claude-code', format: 'claude-code-jsonl', detected_via: 'claude-code-own-anchor',
|
|
570
|
+
log_path: toPosix(best.full), agent_cwd: toPosix(best.cwd),
|
|
571
|
+
session_id: best.name.replace(/\.jsonl$/, ''), originator: null,
|
|
572
|
+
}];
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* zcode 锚定主路径(D-008):node:sqlite 只读 URI(mode=ro)开 ~/.zcode/cli/db/db.sqlite,
|
|
577
|
+
* session 表取 directory==cwd 的最新主会话(time_updated desc;directory 实测存 Windows
|
|
578
|
+
* 反斜杠原样,候选「原样/正斜杠归一/normCwd 归一」逐形态等值尝试,全 miss 再近期主会话
|
|
579
|
+
* 扫描 + cwdsMatch 归一兜底——DG-16)+ parent_id==主会话 id 的全部子代理行(不比
|
|
580
|
+
* directory——worktree TaskCard 子代理 directory=worktree≠仓库根)。
|
|
581
|
+
* @returns {Array|null} anchor 列表(主会话 rollout 文件在才算命中);null = 库不可用/无主会话,走回退链
|
|
582
|
+
*/
|
|
583
|
+
function anchorZcodeFromDb(ctx) {
|
|
584
|
+
const { cwdCandidates, homeDir } = ctx;
|
|
585
|
+
let db = null;
|
|
586
|
+
try {
|
|
587
|
+
let DatabaseSync = null;
|
|
588
|
+
try {
|
|
589
|
+
({ DatabaseSync } = nodeRequire('node:sqlite'));
|
|
590
|
+
} catch (e) {
|
|
591
|
+
debugLog(`zcode 锚定回退(原因:node:sqlite 不可用: ${e && e.message ? e.message : e})`);
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
// 只读打开(file:...?mode=ro + readOnly,不写入不加锁不重试);pathToFileURL 处理
|
|
596
|
+
// Windows 反斜杠/盘符/特殊字符的 URI 编码
|
|
597
|
+
db = new DatabaseSync(pathToFileURL(join(homeDir, '.zcode', 'cli', 'db', 'db.sqlite')).href + '?mode=ro', { readOnly: true });
|
|
598
|
+
} catch (e) {
|
|
599
|
+
debugLog(`zcode 锚定回退(原因:db.sqlite 打开失败: ${e && e.message ? e.message : e})`);
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
// 候选形态逐个等值尝试:原样 / 正斜杠归一 / normCwd 归一(DG-16——directory 实测存
|
|
603
|
+
// Windows 反斜杠原样,但等值口径要对齐 cwdsMatch 的归一语义);全部 miss 再用一次
|
|
604
|
+
// 近期主会话扫描 + JS 侧 cwdsMatch 兜底(覆盖 db 侧大小写/分隔符形态差异)。
|
|
605
|
+
const forms = [...new Set(cwdCandidates.flatMap(c => (c ? [c, toPosix(c), normCwd(c)] : [])))];
|
|
606
|
+
for (const dirVal of forms) {
|
|
607
|
+
let main = null;
|
|
608
|
+
try {
|
|
609
|
+
main = db.prepare("SELECT id, directory FROM session WHERE directory = ? AND parent_id IS NULL AND id NOT LIKE 'sess_subagent_agent_%' ORDER BY time_updated DESC LIMIT 1").get(dirVal);
|
|
610
|
+
} catch (e) {
|
|
611
|
+
debugLog(`zcode 锚定回退(原因:session 表查询失败: ${e && e.message ? e.message : e})`);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
if (!main) continue;
|
|
615
|
+
const anchored = anchorsForMainSession(db, homeDir, main);
|
|
616
|
+
if (anchored) return anchored;
|
|
617
|
+
}
|
|
618
|
+
const scanned = matchMainSessionByNorm(db, cwdCandidates);
|
|
619
|
+
if (scanned) {
|
|
620
|
+
const anchored = anchorsForMainSession(db, homeDir, scanned);
|
|
621
|
+
if (anchored) return anchored;
|
|
622
|
+
}
|
|
623
|
+
debugLog('zcode 锚定回退(原因:db 无该 cwd 的主会话行)');
|
|
624
|
+
return null;
|
|
625
|
+
} finally {
|
|
626
|
+
if (db) { try { db.close(); } catch { /* best effort */ } }
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** 近期主会话扫描 + cwdsMatch 归一匹配(形态等值全 miss 时的兜底,DG-16 归一口径)。 */
|
|
631
|
+
function matchMainSessionByNorm(db, cwdCandidates) {
|
|
632
|
+
const cwds = cwdCandidates.filter(Boolean);
|
|
633
|
+
if (cwds.length === 0) return null;
|
|
634
|
+
try {
|
|
635
|
+
for (const row of db.prepare("SELECT id, directory FROM session WHERE parent_id IS NULL AND id NOT LIKE 'sess_subagent_agent_%' ORDER BY time_updated DESC LIMIT 50").all()) {
|
|
636
|
+
if (cwds.some(c => row.directory && cwdsMatch(c, row.directory))) return row;
|
|
637
|
+
}
|
|
638
|
+
} catch { /* 兜底扫描失败按未命中处理 */ }
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* 主会话行 → anchor 列表(主会话 rollout 文件在才算命中;附 parent_id 子代理链,
|
|
644
|
+
* 子代理不比 directory——worktree TaskCard 子代理 directory=worktree≠仓库根)。
|
|
645
|
+
* @returns {Array|null}
|
|
646
|
+
*/
|
|
647
|
+
function anchorsForMainSession(db, homeDir, main) {
|
|
648
|
+
// session id → rollout 文件名映射(两形态通用):sess_<x> / sess_subagent_agent_<x>
|
|
649
|
+
// 都映射为 model-io-<id>.jsonl
|
|
650
|
+
const anchorFor = (id, directory) => {
|
|
651
|
+
const filePath = join(homeDir, '.zcode', 'cli', 'rollout', `model-io-${id}.jsonl`);
|
|
652
|
+
if (!statSafe(filePath)) return null; // 行在但 rollout 已清理(实证存在)不算命中
|
|
653
|
+
return {
|
|
654
|
+
harness: 'zcode', format: 'zcode-model-io-jsonl', detected_via: 'zcode-own-anchor',
|
|
655
|
+
log_path: toPosix(filePath), agent_cwd: toPosix(directory || ''),
|
|
656
|
+
session_id: `model-io-${id}.jsonl`.replace(/^model-io-sess_/, '').replace(/\.jsonl$/, ''),
|
|
657
|
+
originator: null,
|
|
658
|
+
};
|
|
659
|
+
};
|
|
660
|
+
const mainAnchor = anchorFor(main.id, main.directory);
|
|
661
|
+
if (!mainAnchor) return null; // 主文件不在 → 调用方试下一形态/走回退
|
|
662
|
+
const anchors = [mainAnchor];
|
|
663
|
+
try {
|
|
664
|
+
for (const sub of db.prepare('SELECT id, directory FROM session WHERE parent_id = ?').all(main.id)) {
|
|
665
|
+
const a = anchorFor(sub.id, sub.directory);
|
|
666
|
+
if (a) anchors.push(a); // 子代理文件可能已清理,缺一个不少链
|
|
667
|
+
}
|
|
668
|
+
} catch { /* 子代理链查询失败按空链处理(主会话锚定不受影响,best-effort) */ }
|
|
669
|
+
return anchors;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* zcode 锚定:与探测同 env 门控(ZCODE_*)——不在 zcode 会话内不认领 rollout 文件。
|
|
674
|
+
* db 主路径命中即用;失败回退链(D-008):rollout 目录窗口内主文件(排除 subagent 文件
|
|
675
|
+
* 名)按 mtime 新→旧逐个过 cwd 校验——extractZcodeWorkdir 读首块工作目录标记与
|
|
676
|
+
* cwdCandidates cwdsMatch(与探测同源同口径),标记读不出 fail-closed 跳过;取首个匹配,
|
|
677
|
+
* 全不匹配返回空集不打标(宁缺毋滥——回退无 db 身份证据,绝不做无 cwd 校验的认领,
|
|
678
|
+
* 否则多项目机器会把别的项目活跃会话错认成 own,复现 D-001 跨项目抢标)。子代理不打标,
|
|
679
|
+
* debug 输出原因行。
|
|
680
|
+
*/
|
|
681
|
+
function anchorZcode(ctx) {
|
|
682
|
+
const { cwdCandidates, env, homeDir, now, windowMs } = ctx;
|
|
683
|
+
if (!(env.ZCODE_APP_VERSION || env.ZCODE_ENV || env.ZCODE_RUNTIME_ENV)) return [];
|
|
684
|
+
const fromDb = anchorZcodeFromDb(ctx);
|
|
685
|
+
if (fromDb) return fromDb;
|
|
686
|
+
const rolloutDir = join(homeDir, '.zcode', 'cli', 'rollout');
|
|
687
|
+
// 回退:仅主文件(model-io-sess_<uuid>.jsonl,排除 sess_subagent_agent_ 前缀),mtime 新→旧
|
|
688
|
+
const active = listActiveFiles(rolloutDir, { now, windowMs, limit: 64 })
|
|
689
|
+
.filter(f => !f.name.startsWith('model-io-sess_subagent_agent_'));
|
|
690
|
+
if (active.length === 0) {
|
|
691
|
+
debugLog('zcode 锚定回退:rollout 窗口内无主文件,own 为空(不打标)');
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
for (const f of active) {
|
|
695
|
+
const workdir = extractZcodeWorkdir(f.full);
|
|
696
|
+
if (!workdir) continue; // 标记读不出 fail-closed(错认比漏认危害大,协议 §3 同口径)
|
|
697
|
+
if (!cwdCandidates.some(c => c && cwdsMatch(c, workdir))) continue;
|
|
698
|
+
debugLog(`zcode 锚定回退生效:rollout 主文件 cwd 匹配 ${f.name}(子代理不打标)`);
|
|
699
|
+
return [{
|
|
700
|
+
harness: 'zcode', format: 'zcode-model-io-jsonl', detected_via: 'zcode-own-anchor',
|
|
701
|
+
log_path: toPosix(f.full), agent_cwd: toPosix(workdir),
|
|
702
|
+
session_id: f.name.replace(/^model-io-sess_/, '').replace(/\.jsonl$/, ''),
|
|
703
|
+
originator: null,
|
|
704
|
+
}];
|
|
705
|
+
}
|
|
706
|
+
debugLog('zcode 锚定回退:窗口内主文件无一 cwd 匹配(含无标记跳过),own 为空(不打标)');
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* codex 锚定:与探测同口径扫描(今天/昨天 sessions 目录,首行 session_meta.cwd 与 cwd
|
|
712
|
+
* 候选 cwdsMatch),取最新主文件一条(跨目录按 mtime 取最新——零点前后两个日期目录都
|
|
713
|
+
* 可能持有活跃文件)。
|
|
714
|
+
*/
|
|
715
|
+
function anchorCodex(ctx) {
|
|
716
|
+
const { cwdCandidates, env, homeDir, now, windowMs } = ctx;
|
|
717
|
+
const codexHome = env.CODEX_HOME || join(homeDir, '.codex');
|
|
718
|
+
const sessionsRoot = join(codexHome, 'sessions');
|
|
719
|
+
if (!existsSync(sessionsRoot)) return [];
|
|
720
|
+
const dayDirs = [new Date(now), new Date(now - 24 * 60 * 60 * 1000)]
|
|
721
|
+
.map(d => join(sessionsRoot, String(d.getFullYear()), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0')));
|
|
722
|
+
let best = null;
|
|
723
|
+
for (const dir of [...new Set(dayDirs)]) {
|
|
724
|
+
for (const f of listActiveFiles(dir, { now, windowMs, limit: MAX_PER_HARNESS })) {
|
|
725
|
+
const meta = readFirstLineJson(f.full);
|
|
726
|
+
const payload = meta && meta.type === 'session_meta' ? (meta.payload || {}) : {};
|
|
727
|
+
if (!cwdCandidates.some(c => c && payload.cwd && cwdsMatch(c, payload.cwd))) continue;
|
|
728
|
+
if (!best || f.mtimeMs > best.f.mtimeMs) best = { f, payload };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (!best) return [];
|
|
732
|
+
return [{
|
|
733
|
+
harness: 'codex', format: 'codex-rollout-jsonl', detected_via: 'codex-session-meta-cwd',
|
|
734
|
+
log_path: toPosix(best.f.full), agent_cwd: toPosix(best.payload.cwd),
|
|
735
|
+
session_id: typeof best.payload.session_id === 'string' ? best.payload.session_id : null,
|
|
736
|
+
originator: typeof best.payload.originator === 'string' ? best.payload.originator : null,
|
|
737
|
+
}];
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** pi 锚定:safePath 目录名即 cwd 编码(天然锚定),主文件恒为该目录 session.jsonl。 */
|
|
741
|
+
function anchorPi(ctx) {
|
|
742
|
+
const { cwdCandidates, homeDir, now, windowMs } = ctx;
|
|
743
|
+
const anchors = [];
|
|
744
|
+
for (const rawCwd of cwdCandidates) {
|
|
745
|
+
if (!rawCwd) continue;
|
|
746
|
+
const sessionFile = join(homeDir, '.pi', 'agent', 'sessions', mungePiSafePath(rawCwd), 'session.jsonl');
|
|
747
|
+
const st = statSafe(sessionFile);
|
|
748
|
+
if (!st || now - st.mtimeMs > windowMs) continue;
|
|
749
|
+
anchors.push({
|
|
750
|
+
harness: 'pi', format: 'pi-session-jsonl', detected_via: 'pi-safe-path',
|
|
751
|
+
log_path: toPosix(sessionFile), agent_cwd: toPosix(rawCwd), session_id: null, originator: null,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return anchors;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** dsh 锚定:同 pi 构 safePath 直算,取目录下最新活跃 session-<uuid>/session.jsonl.zstd。 */
|
|
758
|
+
function anchorDsh(ctx) {
|
|
759
|
+
const { cwdCandidates, homeDir, now, windowMs } = ctx;
|
|
760
|
+
const anchors = [];
|
|
761
|
+
for (const rawCwd of cwdCandidates) {
|
|
762
|
+
if (!rawCwd) continue;
|
|
763
|
+
const projectDir = join(homeDir, '.dsh', 'sessions', mungeDshSafePath(rawCwd));
|
|
764
|
+
let sessionDirs;
|
|
765
|
+
try {
|
|
766
|
+
sessionDirs = readdirSync(projectDir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name);
|
|
767
|
+
} catch { continue; }
|
|
768
|
+
let best = null;
|
|
769
|
+
for (const name of sessionDirs) {
|
|
770
|
+
const logFile = join(projectDir, name, 'session.jsonl.zstd');
|
|
771
|
+
const st = statSafe(logFile);
|
|
772
|
+
if (!st || now - st.mtimeMs > windowMs) continue;
|
|
773
|
+
if (!best || st.mtimeMs > best.mtimeMs) best = { name, logFile, mtimeMs: st.mtimeMs };
|
|
774
|
+
}
|
|
775
|
+
if (!best) continue;
|
|
776
|
+
anchors.push({
|
|
777
|
+
harness: 'deepseek-dsh', format: 'dsh-session-jsonl-zstd', detected_via: 'dsh-safe-path',
|
|
778
|
+
log_path: toPosix(best.logFile), agent_cwd: toPosix(rawCwd),
|
|
779
|
+
session_id: best.name.replace(/^session-/, ''), originator: null,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return anchors;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** own anchor 全量解析(env 覆盖优先;cursor/opencode 不参与)。任何异常由调用方兜底。 */
|
|
786
|
+
function resolveOwnLogAnchors(ctx) {
|
|
787
|
+
const anchors = [];
|
|
788
|
+
for (const a of [
|
|
789
|
+
...anchorEnvOverride(ctx),
|
|
790
|
+
...anchorClaudeCode(ctx),
|
|
791
|
+
...anchorZcode(ctx),
|
|
792
|
+
...anchorCodex(ctx),
|
|
793
|
+
...anchorPi(ctx),
|
|
794
|
+
...anchorDsh(ctx),
|
|
795
|
+
]) {
|
|
796
|
+
if (!anchors.some(x => x.log_path === a.log_path)) anchors.push(a);
|
|
797
|
+
}
|
|
798
|
+
return anchors;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* 解析本 run 的 own 日志文件集合(锚定主会话 + 子代理,posix log_path,与 ledger/push 同口径)。
|
|
803
|
+
* 识别失败返回空集(仅回退主文件或空)——调用方据此不打标不推送(宁缺毋滥,D-001)。
|
|
804
|
+
* @param {object} [opts] - 入参与 detectAgentLogEntries 对齐(cwdCandidates/env/homeDir/now/windowMs)
|
|
805
|
+
* @returns {Set<string>} posix log_path 集合
|
|
806
|
+
*/
|
|
807
|
+
export function resolveOwnLogPaths({
|
|
808
|
+
cwdCandidates = [],
|
|
809
|
+
env = process.env,
|
|
810
|
+
homeDir = homedir(),
|
|
811
|
+
now = Date.now(),
|
|
812
|
+
windowMs = ACTIVE_WINDOW_MS,
|
|
813
|
+
} = {}) {
|
|
814
|
+
return new Set(resolveOwnLogAnchors({ cwdCandidates, env, homeDir, now, windowMs }).map(a => a.log_path));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* 探测当前驱动 CLI 的本地 agent 会话日志(纯读,不写盘)+ own 锚定。
|
|
498
819
|
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
* @param {
|
|
505
|
-
* @
|
|
506
|
-
*
|
|
507
|
-
* agent_cwd:string, session_id:string|null, originator:string|null, mtime_ms:number|null}>}
|
|
508
|
-
* 探测结果(mtime_ms 新→旧);空数组 = 未探测到(不在 agent 环境或无活跃会话)
|
|
820
|
+
* 常规探测照旧(cwd 等值过滤 + 单 harness 上限裁剪 + loose 压制);其上叠加 own 豁免
|
|
821
|
+
* (DG-17):锚定命中但被裁剪/被 cwd 过滤挤出的 own 文件(zcode worktree 子代理为主场景)
|
|
822
|
+
* 定向补收进 entries——own 不得漏推优先于 12 条总数上限。锚定本身 best-effort,任何异常
|
|
823
|
+
* 按空 own 处理(探测结果不受影响)。
|
|
824
|
+
*
|
|
825
|
+
* @param {object} [opts] - 同 detectAgentLogEntries
|
|
826
|
+
* @returns {{entries: Array, ownAnchors: Array}} entries 同 detectAgentLogEntries 返回形状;
|
|
827
|
+
* ownAnchors 与 entry 同构(无 mtime_ms)
|
|
509
828
|
*/
|
|
510
|
-
|
|
829
|
+
function detectAgentLogEntriesWithOwn({
|
|
511
830
|
cwdCandidates = [],
|
|
512
831
|
env = process.env,
|
|
513
832
|
homeDir = homedir(),
|
|
@@ -551,7 +870,42 @@ export function detectAgentLogEntries({
|
|
|
551
870
|
}
|
|
552
871
|
|
|
553
872
|
entries.sort((a, b) => (b.mtime_ms ?? 0) - (a.mtime_ms ?? 0));
|
|
554
|
-
|
|
873
|
+
const capped = entries.slice(0, 12);
|
|
874
|
+
|
|
875
|
+
// ── own 豁免补收(DG-17):own 集合中不在 entries 的文件(被上限挤出/被 cwd 等值过滤,
|
|
876
|
+
// 如 directory=worktree 的 zcode 子代理)定向补收;own 不漏推优先于 12 条上限 ──
|
|
877
|
+
let ownAnchors = [];
|
|
878
|
+
try {
|
|
879
|
+
ownAnchors = resolveOwnLogAnchors(ctx);
|
|
880
|
+
} catch (e) {
|
|
881
|
+
debugLog(`own 锚定异常(按空 own 处理): ${e && e.message ? e.message : e}`);
|
|
882
|
+
}
|
|
883
|
+
const seen = new Set(capped.map(e => e.log_path));
|
|
884
|
+
for (const a of ownAnchors) {
|
|
885
|
+
if (seen.has(a.log_path)) continue;
|
|
886
|
+
const st = statSafe(a.log_path);
|
|
887
|
+
if (!st) continue;
|
|
888
|
+
capped.push({ ...a, mtime_ms: st.mtimeMs });
|
|
889
|
+
}
|
|
890
|
+
return { entries: capped, ownAnchors };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* 探测当前驱动 CLI 的本地 agent 会话日志(纯读,不写盘)。
|
|
895
|
+
*
|
|
896
|
+
* @param {object} [opts]
|
|
897
|
+
* @param {string[]} [opts.cwdCandidates] - agent 可能的 cwd 列表(CLI 原始 cwd + 纠正后项目根;
|
|
898
|
+
* agent 在子目录启动时部分 harness 日志挂在子目录维度,两者都探)
|
|
899
|
+
* @param {object} [opts.env] - 默认 process.env(可注入测试)
|
|
900
|
+
* @param {string} [opts.homeDir] - 默认 homedir()(可注入测试)
|
|
901
|
+
* @param {number} [opts.now] - 默认 Date.now()(可注入测试)
|
|
902
|
+
* @param {number} [opts.windowMs] - 活跃窗口,默认 15min
|
|
903
|
+
* @returns {Array<{harness:string, log_path:string, format:string, detected_via:string,
|
|
904
|
+
* agent_cwd:string, session_id:string|null, originator:string|null, mtime_ms:number|null}>}
|
|
905
|
+
* 探测结果(mtime_ms 新→旧,含 own 豁免补收条目);空数组 = 未探测到(不在 agent 环境或无活跃会话)
|
|
906
|
+
*/
|
|
907
|
+
export function detectAgentLogEntries(opts) {
|
|
908
|
+
return detectAgentLogEntriesWithOwn(opts).entries;
|
|
555
909
|
}
|
|
556
910
|
|
|
557
911
|
/**
|
|
@@ -670,8 +1024,10 @@ export function readAgentLogArtifact(runtimeRoot) {
|
|
|
670
1024
|
* @param {string} [p.command] - 触发登记的命令描述(flag 名级,不含 flag 值)
|
|
671
1025
|
* @param {object} [p.context] - 会话化上下文(2026-08-23-agent-activity-sessions,可选):
|
|
672
1026
|
* {hubSessionId, changeKey, quickId}——hubSessionId 为 body 级(run 所属平台会话唯一,
|
|
673
|
-
* daemon 注入 env SILLYHUB_SESSION_ID 的兜底也在此读取);changeKey/quickId 为 entry
|
|
674
|
-
*
|
|
1027
|
+
* daemon 注入 env SILLYHUB_SESSION_ID 的兜底也在此读取);changeKey/quickId 为 entry 级,
|
|
1028
|
+
* 仅写入本 run own 条目(D-001 会话身份锚定:锚定主会话+子代理,非 own 只留底不被改写),
|
|
1029
|
+
* 双向互斥(D-006@v2:quick run 清 change_key,change run 清 quick_id;两键皆空 keep-prev)。
|
|
1030
|
+
* 缺省全不带。
|
|
675
1031
|
* @param {object} [p.env] - 探测用 env(默认 process.env;测试注入用)
|
|
676
1032
|
* @param {string} [p.homeDir] - 探测用 home(默认 homedir();测试注入用)
|
|
677
1033
|
* @param {number} [p.now] - 探测用时钟(默认 Date.now();测试注入用)
|
|
@@ -683,8 +1039,11 @@ export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts =
|
|
|
683
1039
|
try {
|
|
684
1040
|
if (!cwd || !specBase) return null;
|
|
685
1041
|
const cwdCandidates = [...new Set([invokedCwd, cwd].filter(Boolean))];
|
|
686
|
-
const detected =
|
|
1042
|
+
const { entries: detected, ownAnchors } = detectAgentLogEntriesWithOwn({ cwdCandidates, env, homeDir, now, windowMs });
|
|
687
1043
|
if (detected.length === 0) return null;
|
|
1044
|
+
// own 集合(D-001 会话身份锚定):仅 detected∩own 条目写 ctx/计数/推送;锚定识别
|
|
1045
|
+
// 失败为空集 → 只留底不归属(宁缺毋滥)
|
|
1046
|
+
const ownPaths = new Set(ownAnchors.map(a => a.log_path));
|
|
688
1047
|
|
|
689
1048
|
// 会话化上下文(best-effort 消毒,解析失败不带):entry 级 ctx(changeKey/quickId)随
|
|
690
1049
|
// 检出/更新该 entry 的本次 run 持久化;hubSessionId 为 body 级——context 显式传入优先,
|
|
@@ -710,6 +1069,51 @@ export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts =
|
|
|
710
1069
|
for (const det of detected) {
|
|
711
1070
|
const st = statSafe(det.log_path);
|
|
712
1071
|
const prev = byPath.get(det.log_path);
|
|
1072
|
+
// 非 own 条目(D-001 打标收敛):只留底探测事实(exists/size/mtime 刷新),ctx/计数/
|
|
1073
|
+
// 时间戳/last_command 全部 keep-prev 不动——同 cwd 活跃窗口内他人会话的日志不被本
|
|
1074
|
+
// run 改写归属(FR-01/FR-06)。新见的非 own 条目也入留底(本地可见性不变),不计数。
|
|
1075
|
+
if (!ownPaths.has(det.log_path)) {
|
|
1076
|
+
byPath.set(det.log_path, prev ? {
|
|
1077
|
+
...prev,
|
|
1078
|
+
exists: Boolean(st),
|
|
1079
|
+
size_bytes: st ? st.size : null,
|
|
1080
|
+
mtime_ms: st ? st.mtimeMs : (det.mtime_ms ?? null),
|
|
1081
|
+
} : {
|
|
1082
|
+
harness: det.harness,
|
|
1083
|
+
log_path: det.log_path,
|
|
1084
|
+
format: det.format,
|
|
1085
|
+
detected_via: det.detected_via,
|
|
1086
|
+
agent_cwd: det.agent_cwd,
|
|
1087
|
+
session_id: det.session_id ?? null,
|
|
1088
|
+
originator: det.originator ?? null,
|
|
1089
|
+
change_key: null,
|
|
1090
|
+
quick_id: null,
|
|
1091
|
+
exists: Boolean(st),
|
|
1092
|
+
size_bytes: st ? st.size : null,
|
|
1093
|
+
mtime_ms: st ? st.mtimeMs : (det.mtime_ms ?? null),
|
|
1094
|
+
first_seen_at: nowIso,
|
|
1095
|
+
last_seen_at: nowIso,
|
|
1096
|
+
invocations: 0,
|
|
1097
|
+
last_command: null,
|
|
1098
|
+
seq: ++seq,
|
|
1099
|
+
});
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1102
|
+
// own 条目:entry 级 ctx 双向互斥(D-006@v2)——quick run 置 quick_id 且清空
|
|
1103
|
+
// change_key;change run 置 change_key 且清空 quick_id(单向清会留镜像键,change
|
|
1104
|
+
// 日志漏进旧 quick 会话);两键皆空的 run(如 status)keep-prev 保留原值。
|
|
1105
|
+
let nextChangeKey;
|
|
1106
|
+
let nextQuickId;
|
|
1107
|
+
if (ctxQuickId) {
|
|
1108
|
+
nextQuickId = ctxQuickId;
|
|
1109
|
+
nextChangeKey = null;
|
|
1110
|
+
} else if (ctxChangeKey) {
|
|
1111
|
+
nextChangeKey = ctxChangeKey;
|
|
1112
|
+
nextQuickId = null;
|
|
1113
|
+
} else {
|
|
1114
|
+
nextChangeKey = prev?.change_key ?? null;
|
|
1115
|
+
nextQuickId = prev?.quick_id ?? null;
|
|
1116
|
+
}
|
|
713
1117
|
byPath.set(det.log_path, {
|
|
714
1118
|
...(prev || {}),
|
|
715
1119
|
harness: det.harness,
|
|
@@ -719,10 +1123,8 @@ export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts =
|
|
|
719
1123
|
agent_cwd: det.agent_cwd,
|
|
720
1124
|
session_id: det.session_id ?? prev?.session_id ?? null,
|
|
721
1125
|
originator: det.originator ?? prev?.originator ?? null,
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
change_key: ctxChangeKey ?? prev?.change_key ?? null,
|
|
725
|
-
quick_id: ctxQuickId ?? prev?.quick_id ?? null,
|
|
1126
|
+
change_key: nextChangeKey,
|
|
1127
|
+
quick_id: nextQuickId,
|
|
726
1128
|
exists: Boolean(st),
|
|
727
1129
|
size_bytes: st ? st.size : null,
|
|
728
1130
|
mtime_ms: st ? st.mtimeMs : (det.mtime_ms ?? null),
|
|
@@ -756,21 +1158,30 @@ export async function recordAgentLogInvocation({ cwd, invokedCwd, platformOpts =
|
|
|
756
1158
|
|
|
757
1159
|
// 上报平台(主通道,与进度上报同风格;失败本地产物兜底)。不受平台模式 sentinel
|
|
758
1160
|
// 限制——进度同步在平台模式跳过是因为 daemon 有自有进度链路,agent 日志没有。
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1161
|
+
// 推送收敛(D-007):payload 只含本 run own 条目(按 log_path 过滤,非 own 不进任何
|
|
1162
|
+
// 推送——hub 会话不再把共享留底里别人的条目整批挂走);留底产物已写全量(上方
|
|
1163
|
+
// merged.entries,本地 sillyspec agent-log 可见性不变);own 留底条目为空不发请求。
|
|
1164
|
+
const payloadEntries = merged.entries.filter(e => ownPaths.has(e.log_path));
|
|
1165
|
+
let push = { pushed: null, reason: 'no-own-entries' };
|
|
1166
|
+
if (payloadEntries.length > 0) {
|
|
1167
|
+
push = await pushAgentLogToPlatform({
|
|
1168
|
+
specBase,
|
|
1169
|
+
env: env || process.env,
|
|
1170
|
+
payload: {
|
|
1171
|
+
schema_version: AGENT_LOG_SCHEMA_VERSION,
|
|
1172
|
+
pushed_at: nowIso,
|
|
1173
|
+
agent_cwd: toPosix(invokedCwd || cwd),
|
|
1174
|
+
workspace_id: platformOpts.workspaceId ?? null,
|
|
1175
|
+
scan_run_id: platformOpts.scanRunId ?? null,
|
|
1176
|
+
// body 级会话关联键(run 所属平台会话唯一;非空才带,schema_version 保持 1 纯可选增量)
|
|
1177
|
+
...(hubSessionId ? { hub_session_id: hubSessionId } : {}),
|
|
1178
|
+
// entries 各自带 entry 级 ctx(change_key/quick_id,随 entry 持久化的原值或本次 run 的新值)
|
|
1179
|
+
entries: payloadEntries,
|
|
1180
|
+
},
|
|
1181
|
+
});
|
|
1182
|
+
} else {
|
|
1183
|
+
debugLog('无 own 留底条目(D-007 推送收敛),跳过上报');
|
|
1184
|
+
}
|
|
774
1185
|
|
|
775
1186
|
return {
|
|
776
1187
|
artifactPath: toPosix(artifactPath),
|
|
@@ -51,6 +51,26 @@ const INTEGRATION_FILE_PATTERNS = [
|
|
|
51
51
|
/startup/i,
|
|
52
52
|
]
|
|
53
53
|
|
|
54
|
+
// ============ quick 出口门禁:路径模式风险表(2026-09-14-quick-exit-tiered-gates task-01) ============
|
|
55
|
+
//
|
|
56
|
+
// quick 侧「风险特征命中」的单一数据源(design D-004@v2:v1 仅路径模式——不做 diff 关键词
|
|
57
|
+
// 扫描、不加 git 子进程,R-04 零子进程承诺;diff 维度待真实需求另立变更)。与上方 verify 侧
|
|
58
|
+
// INTEGRATION_FILE_PATTERNS 语义不同(那张表收跨进程/集成域、供 detectChangeRisk 判级),本表
|
|
59
|
+
// 收 quick 实证踩坑域(R-03 从窄收录:auth/permission/billing/migration/锁/调度),供
|
|
60
|
+
// quick-gate-profile.js 的 computeGateProfile 经 opts.riskTable 默认引用——命中即画像 L2 +
|
|
61
|
+
// checks.runtimeEvidence='required'。detectChangeRisk 判级逻辑不消费本表(verify 侧行为零变化)。
|
|
62
|
+
//
|
|
63
|
+
// 匹配口径:POSIX 路径、大小写不敏感、段边界锚定——前界 (^|[/_-]) + 后界 (?=[/._-]|$),
|
|
64
|
+
// 防 author/booking/lockfile 类子串假阳(R-03);pattern 为稳定域标识(riskHits 审计输出用)。
|
|
65
|
+
export const QUICK_RISK_PATH_PATTERNS = [
|
|
66
|
+
{ pattern: 'auth', re: /(?:^|[/_-])(?:oauth2?|auth(?:orization|entication|enticator)?)(?=[/._-]|$)/i },
|
|
67
|
+
{ pattern: 'permission', re: /(?:^|[/_-])permissions?(?=[/._-]|$)/i },
|
|
68
|
+
{ pattern: 'billing', re: /(?:^|[/_-])billing(?=[/._-]|$)/i },
|
|
69
|
+
{ pattern: 'migration', re: /(?:^|[/_-])(?:migrations?|migrate)(?=[/._-]|$)/i },
|
|
70
|
+
{ pattern: 'lock', re: /(?:^|[/_-])(?:locks?|mutex(?:es)?)(?=[/._-]|$)/i },
|
|
71
|
+
{ pattern: 'scheduling', re: /(?:^|[/_-])(?:schedul(?:er|ing)|cron|jobs?)(?=[/._-]|$)/i },
|
|
72
|
+
]
|
|
73
|
+
|
|
54
74
|
/**
|
|
55
75
|
* task 级「端到端/deployment-critical」文本判定(execute 批量完成 autoCheckPlanFromReviews 用)。
|
|
56
76
|
*
|
package/src/config-cat.js
CHANGED
|
@@ -47,17 +47,20 @@ export function resolveLocalYaml(dir, opts = {}) {
|
|
|
47
47
|
// 经 resolveLocalYamlWriteTarget 同链解析,误命中会把项目注册静默写进全局配置
|
|
48
48
|
// (local-register.test.mjs 实证)。
|
|
49
49
|
const home = homedir()
|
|
50
|
-
|
|
50
|
+
// 大小写归一比较(批 D-④):Windows 盘符/路径大小写不敏感,--dir 手输小写形态绕过
|
|
51
|
+
// startsWith 守卫 → 误命中 ~/.sillyspec 全局配置(写侧同链会把项目注册写进全局)
|
|
52
|
+
const ci = (p) => (process.platform === 'win32' ? String(p).toLowerCase() : String(p))
|
|
53
|
+
const originBelowHome = start !== home && ci(start).startsWith(ci(home) + sep)
|
|
51
54
|
if (opts.specBase) push(join(opts.specBase, 'local.yaml'), 'spec 根(--spec-dir/平台 pointer/最近 .sillyspec)')
|
|
52
55
|
let passedHome = false
|
|
53
56
|
for (let d = start; ; d = dirname(d)) {
|
|
54
57
|
// passedHome:经过 home 后不再入候选(resolveSpecDir e4f2855 同款语义)——套件隔离下
|
|
55
58
|
// HOME 与 fixture 可能同根(HOME=suiteTmp、fixture=suiteTmp/x),遍历越过 home 层后
|
|
56
59
|
// 仍会撞真实 home 的 ~/.sillyspec/local.yaml,须整段截断。
|
|
57
|
-
if (!(passedHome || (originBelowHome && d === home))) {
|
|
60
|
+
if (!(passedHome || (originBelowHome && ci(d) === ci(home)))) {
|
|
58
61
|
push(join(d, '.sillyspec', 'local.yaml'), 'cwd 祖先链(worktree 内向上命中主仓;home 层受守卫跳过)')
|
|
59
62
|
}
|
|
60
|
-
if (d === home) passedHome = true
|
|
63
|
+
if (ci(d) === ci(home)) passedHome = true
|
|
61
64
|
const parent = dirname(d)
|
|
62
65
|
if (parent === d) break
|
|
63
66
|
}
|
package/src/config-schema.js
CHANGED
|
@@ -181,6 +181,25 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
181
181
|
{ path: 'semantic_guard.enabled', type: 'boolean', optional: true, status: 'live', readers: ['readSemanticGuardEnabled (src/semantic-guard.js)'], desc: '语义守卫总开关,默认 true;设 false 时 quick 进场注入与 --done 断言 WARNING 全停零开销。', example: 'true' },
|
|
182
182
|
],
|
|
183
183
|
},
|
|
184
|
+
{
|
|
185
|
+
id: 'quick-gate',
|
|
186
|
+
title: 'quick 出口门禁阈值',
|
|
187
|
+
note: 'quick --done 分级门禁(L0/L1/L2 advisory)的四个数值阈值覆写(2026-09-14-quick-exit-tiered-gates,D-009)。缺省=src/quick-gate-profile.js 的 THRESHOLDS 代码内校准值,仅确知调参时配置;键非法(非 ≥1 整数)回退默认并 warn。',
|
|
188
|
+
keys: [
|
|
189
|
+
{ path: 'quick-gate.l1_span', type: 'integer', optional: true, status: 'live', readers: ['resolveGateThresholds (src/quick-gate-profile.js)'], desc: 'L1 模块跨度阈值:非文档文件命中 ≥N 个模块 → L1(缺省 2)。', example: '2' },
|
|
190
|
+
{ path: 'quick-gate.l1_files', type: 'integer', optional: true, status: 'live', readers: ['resolveGateThresholds (src/quick-gate-profile.js)'], desc: 'L1 文件数阈值:变更文件 ≥N → L1(缺省 4)。', example: '4' },
|
|
191
|
+
{ path: 'quick-gate.l2_span', type: 'integer', optional: true, status: 'live', readers: ['resolveGateThresholds (src/quick-gate-profile.js)'], desc: 'L2 模块跨度阈值:命中 ≥N 个模块 → L2(缺省 4)。', example: '4' },
|
|
192
|
+
{ path: 'quick-gate.l2_files_degraded', type: 'integer', optional: true, status: 'live', readers: ['resolveGateThresholds (src/quick-gate-profile.js)'], desc: 'module-map 缺失降级档的 L2 文件数阈值:变更文件 ≥N → L2(缺省 8)。', example: '8' },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'change-ownership',
|
|
197
|
+
title: '变更所有权守卫',
|
|
198
|
+
note: '接管类操作(apply/cleanup/archive)对他人活跃 change 的所有权校验调参(2026-09-14-change-ownership-guards,D-001/D-005@v1)。缺省 15 分钟,仅确知调参时配置。',
|
|
199
|
+
keys: [
|
|
200
|
+
{ path: 'change-ownership.heartbeat_minutes', type: 'integer', optional: true, status: 'live', readers: ['assertChangeOwnership (src/progress/change-registry.js — 本变更 task-02 接线消费)'], desc: '所有者活跃心跳窗口(分钟):owner 非本会话且 last_active 距今在窗口内 → 拒绝接管类操作(--takeover 显式接管);窗口外 → 放行并自动接管。缺省 15。', example: '15' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
184
203
|
],
|
|
185
204
|
};
|
|
186
205
|
|
|
@@ -350,5 +369,18 @@ docs-check:
|
|
|
350
369
|
# - docs/sillyspec/architecture.md
|
|
351
370
|
# cross_repo_roots: # repo://<仓库名> 跨仓引用 → 本机仓库根(每台设备各自配,绝对路径勿提交)
|
|
352
371
|
# sillyspec: /c/Users/you/IdeaProjects/sillyspec
|
|
372
|
+
|
|
373
|
+
# ── quick 出口门禁阈值(quick --done 分级门禁 L0/L1/L2 的 advisory 阈值覆写;缺省=代码内校准值)──
|
|
374
|
+
# 仅确知调参时配置;键非法(非 ≥1 整数)回退默认并 warn。
|
|
375
|
+
# quick-gate:
|
|
376
|
+
# l1_span: 2 # 跨 ≥2 模块 → L1
|
|
377
|
+
# l1_files: 4 # 或 ≥4 文件 → L1
|
|
378
|
+
# l2_span: 4 # 跨 ≥4 模块 → L2
|
|
379
|
+
# l2_files_degraded: 8 # module-map 缺失降级档:≥8 文件 → L2
|
|
380
|
+
|
|
381
|
+
# ── 变更所有权守卫(接管类操作 apply/cleanup/archive 对他人活跃 change 校验;缺省 15 分钟)──
|
|
382
|
+
# owner 非本会话且 last_active 在窗口内 → 拒绝(--takeover 显式接管);窗口外 → 自动接管。
|
|
383
|
+
# change-ownership:
|
|
384
|
+
# heartbeat_minutes: 15 # 所有者活跃心跳窗口(分钟)
|
|
353
385
|
`;
|
|
354
386
|
}
|