@fenglimg/fabric-cli 2.0.0 → 2.1.0-rc.2
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/LICENSE +21 -0
- package/README.md +6 -5
- package/dist/chunk-BATF4PEJ.js +361 -0
- package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
- package/dist/chunk-F46ORPOA.js +903 -0
- package/dist/chunk-HFQVXY6P.js +86 -0
- package/dist/chunk-L4Q55UC4.js +52 -0
- package/dist/chunk-LFIKMVY7.js +27 -0
- package/dist/chunk-MF3OTILQ.js +544 -0
- package/dist/chunk-PWLW3B57.js +18 -0
- package/dist/chunk-RYAFBNES.js +33 -0
- package/dist/chunk-T5RPGCCM.js +40 -0
- package/dist/chunk-WU6GAPKH.js +36 -0
- package/dist/config-XJIPZNUP.js +13 -0
- package/dist/doctor-QVNPHLJK.js +920 -0
- package/dist/index.js +23 -8
- package/dist/{init-BIRSIOXO.js → install-2HDO5FTQ.js} +807 -705
- package/dist/metrics-ACEQFPDU.js +122 -0
- package/dist/onboard-coverage-MFCAEBDO.js +220 -0
- package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
- package/dist/scope-explain-2F2R5URO.js +33 -0
- package/dist/status-GLQWLWH6.js +23 -0
- package/dist/store-XTSE5TY6.js +105 -0
- package/dist/sync-BJCWDPNC.js +245 -0
- package/dist/uninstall-TAXSUSKH.js +1073 -0
- package/dist/whoami-B6AEMSEV.js +31 -0
- package/package.json +30 -5
- package/templates/hooks/cite-policy-evict.cjs +231 -0
- package/templates/hooks/configs/README.md +29 -6
- package/templates/hooks/configs/claude-code.json +14 -3
- package/templates/hooks/configs/codex-hooks.json +6 -3
- package/templates/hooks/configs/cursor-hooks.json +8 -10
- package/templates/hooks/fabric-hint.cjs +873 -105
- package/templates/hooks/knowledge-hint-broad.cjs +549 -135
- package/templates/hooks/knowledge-hint-narrow.cjs +830 -26
- package/templates/hooks/lib/banner-i18n.cjs +309 -0
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +81 -0
- package/templates/hooks/lib/cite-contract-reminder.cjs +179 -0
- package/templates/hooks/lib/cite-line-parser.cjs +180 -0
- package/templates/hooks/lib/client-adapter.cjs +106 -0
- package/templates/hooks/lib/config-cache.cjs +107 -0
- package/templates/hooks/lib/state-store.cjs +84 -0
- package/templates/hooks/lib/summary-fallback.cjs +210 -0
- package/templates/skills/fabric-archive/SKILL.md +97 -419
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
- package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
- package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
- package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
- package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
- package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
- package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
- package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
- package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
- package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
- package/templates/skills/fabric-import/SKILL.md +77 -514
- package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
- package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
- package/templates/skills/fabric-import/ref/output-contract.md +61 -0
- package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
- package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
- package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
- package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
- package/templates/skills/fabric-review/SKILL.md +90 -284
- package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
- package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
- package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
- package/templates/skills/fabric-review/ref/output-contract.md +58 -0
- package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
- package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
- package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
- package/templates/skills/fabric-sync/SKILL.md +46 -0
- package/templates/skills/lib/shared-policy.md +69 -0
- package/dist/chunk-6ICJICVU.js +0 -10
- package/dist/chunk-74SZWYPH.js +0 -658
- package/dist/chunk-EYIDD2YS.js +0 -1000
- package/dist/doctor-T7JWODKG.js +0 -282
- package/dist/hooks-Y74Y5LQS.js +0 -12
- package/dist/scan-LMK3UCWL.js +0 -22
- package/dist/serve-H554BHLG.js +0 -124
- package/templates/agents-md/AGENTS.md.template +0 -59
- package/templates/bootstrap/CLAUDE.md +0 -8
- package/templates/bootstrap/codex-AGENTS-header.md +0 -6
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# E5 Scheduled Daily Recap — full reference
|
|
2
|
+
|
|
3
|
+
> **Loaded on demand.** Only relevant when invocation context = `cron` / `/loop` (E5 entry). SKILL.md's Phase 0 already gates this — if the user just typed `/fabric-archive`, none of the below applies.
|
|
4
|
+
|
|
5
|
+
## E5 周期触发 (Scheduled Daily Recap)
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`今日复盘` = E5 entry point. Default scope = today. Falls back to historical scan if today yields no candidates (silent-skip per Phase 4.5).
|
|
10
|
+
|
|
11
|
+
E5 是 5 入口模型中唯一由 OS 调度器或 Claude Code `/loop` 周期触发的入口形态。fabric 端**零代码**——不提供 `fabric schedule` 子命令,亦不内嵌 daemon。用户基于自己的执行环境二选一接入: `/loop`(Claude Code 原生,推荐) 或 OS cron(跨平台 fallback)。
|
|
12
|
+
|
|
13
|
+
### /loop sample (primary path for Claude Code)
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
/loop /fabric-archive 今日复盘 --cron "0 23 * * *"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
每晚 23:00 在当前 Claude Code session 中触发 fabric-archive skill,scope = today。`/loop` 复用现有 Claude session 鉴权,无需独立 token。
|
|
20
|
+
|
|
21
|
+
### OS cron sample (cross-platform alternative)
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
# crontab -e
|
|
25
|
+
0 23 * * * cd /path/to/project && claude code -p "/fabric-archive 今日复盘" 2>&1 >> /var/log/fabric-daily-recap.log
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
适用于:
|
|
29
|
+
- 非 Claude Code 环境(纯 server / CI 节点)
|
|
30
|
+
- 希望脱离 /loop session 生命周期独立运行的场景
|
|
31
|
+
- 已有 cron / launchd 调度基础设施的团队
|
|
32
|
+
|
|
33
|
+
macOS 用户可改用 `launchd` plist;Linux 用户直接 `crontab -e`。命令需自行确保 `claude code` CLI 已安装且鉴权可用。
|
|
34
|
+
|
|
35
|
+
### E5 prompt parse contract
|
|
36
|
+
|
|
37
|
+
当用户或 cron 以 `今日复盘` / `daily recap` 字面短语触发 fabric-archive 时,skill 应按以下契约处理:
|
|
38
|
+
|
|
39
|
+
- **Phase 0 Range Resolution**: 识别 `今日复盘` / `daily recap` 为 magic phrase, 直接设置 `time_window = today` (00:00 local timezone → current ts), 无需 AskUserQuestion 兜底。
|
|
40
|
+
- **Phase 1.5 Onboard Coverage**: 跳过 (entry_point = E5_cron, 非 E2_explicit, 不弹 onboard 弹问)。
|
|
41
|
+
- **Phase 4.5 Persist Archive Attempt**: 始终写入 `session_archive_attempted` event。当今日无 archive 信号触发 viability gate FAIL 时,走 silent-skip 路径(outcome = `skipped_no_signal`),skill 静默退出,cron 日志为空。
|
|
42
|
+
|
|
43
|
+
### Trade-off table (/loop vs OS cron)
|
|
44
|
+
|
|
45
|
+
| 维度 | /loop | OS cron |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| 鉴权 | 复用 Claude session | 独立 token / login |
|
|
48
|
+
| 跨平台 | Claude Code 全平台一致 | macOS launchd / Linux cron 不同 |
|
|
49
|
+
| Token 成本 | 累积 (长 session) | 每 tick fresh, 无累积 |
|
|
50
|
+
| 调试 | Claude UI 可见 | 日志文件 |
|
|
51
|
+
|
|
52
|
+
### Failure modes
|
|
53
|
+
|
|
54
|
+
- **/loop session crash**: 归档暂停,用户需重启 `/loop`。无自动恢复机制——`/loop` 与 Claude Code session 生命周期绑定。
|
|
55
|
+
- **OS cron**: 自带恢复(下一个 tick 重新启动),但需独立 `claude code` CLI 安装与鉴权;鉴权 token 过期时 cron job 会静默失败,需人工 `claude login` 重置。
|
|
56
|
+
|
|
57
|
+
### NOT in scope
|
|
58
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# UX i18n Policy — full reference
|
|
2
|
+
|
|
3
|
+
> **Shared core (rc.37 NEW-13):** the cross-skill invariants — protected-token
|
|
4
|
+
> NEVER-translate list, AskUserQuestion routing-key rule, layer heuristic, and
|
|
5
|
+
> events-emit convention — live once in `../../lib/shared-policy.md`. This file
|
|
6
|
+
> keeps only the fabric-archive-specific 5-class examples. Read the shared lib
|
|
7
|
+
> for the common rules; do not fork them here.
|
|
8
|
+
|
|
9
|
+
> **Loaded on demand.** Only consult when rendering bilingual output AND you're unsure which class a string belongs to. SKILL.md gives the operative rule: read `.fabric/fabric-config.json` → `fabric_language`, emit prose in resolved variant, never translate protected tokens. The 5-class taxonomy below disambiguates edge cases.
|
|
10
|
+
|
|
11
|
+
## UX i18n Policy (5-class bilingualization)
|
|
12
|
+
|
|
13
|
+
The skill consults `fabric_language` from `.fabric/fabric-config.json`
|
|
14
|
+
(固化于 init 时,via `lib/detect-language.ts:detectExistingLanguage`; default `"en"` when no
|
|
15
|
+
CJK signal is detected in README + docs/; may resolve to `"match-existing"`,
|
|
16
|
+
`"zh-CN"`, `"en"`, or `"zh-CN-hybrid"`). All user-facing text in the
|
|
17
|
+
following 5 categories MUST be rendered in the resolved language:
|
|
18
|
+
|
|
19
|
+
1. **Roll-up templates** — the `# Archive Review — N candidates` batch
|
|
20
|
+
review block (one per candidate) AND any final session summary the
|
|
21
|
+
skill emits after Phase 4 completes. zh-CN ↔ en mirror.
|
|
22
|
+
2. **Errors / Preconditions warnings** — abort + gate-fail messages (e.g.
|
|
23
|
+
the "没有触发归档信号…" trigger-miss and the "本次会话为常规执行…"
|
|
24
|
+
viability-gate-FAIL message). zh-CN ↔ en mirror.
|
|
25
|
+
3. **Confirmation prompts** — the per-candidate `Confirm? (Y to accept,
|
|
26
|
+
edit … inline, N to skip)` line in the batch review template. zh-CN
|
|
27
|
+
↔ en mirror.
|
|
28
|
+
4. **Dry-run table headers** — v2.0.0-rc.27 TASK-007 added a dry-run
|
|
29
|
+
override path (see Phase 4.5 "dry-run") so users can preview the
|
|
30
|
+
archive proposal without writing pending entries. The dry-run summary
|
|
31
|
+
header and per-candidate preview labels MUST be bilingualized per
|
|
32
|
+
this policy. zh-CN ↔ en mirror.
|
|
33
|
+
5. **AskUserQuestion** — `header` + `question` fields (NOT `options[]`).
|
|
34
|
+
zh-CN ↔ en mirror. fabric-archive itself does not surface
|
|
35
|
+
AskUserQuestion in the current contract (Phase 3 batch review is a
|
|
36
|
+
single markdown screen, not a structured question), but if a future
|
|
37
|
+
version adds one — e.g. to confirm layer flip — this rule applies.
|
|
38
|
+
|
|
39
|
+
Rendering rule:
|
|
40
|
+
|
|
41
|
+
- `fabric_language === "zh-CN"` → emit the zh-CN variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
42
|
+
- `fabric_language === "en"` → emit the en variant; pure monolingual, no language mixing inside a single user-facing block.
|
|
43
|
+
- `fabric_language === "zh-CN-hybrid"` → emit Chinese narrative prose with English technical terms preserved. Protected tokens (always EN): MCP tool names (e.g. `fab_get_knowledge_sections`), CLI command names (e.g. `fabric install`), file paths, technical concepts (`Skill`, `SessionStart`, `hook`, `MCP`, `revision_hash`, `pending`, `proven`, `verified`, `draft`).
|
|
44
|
+
- `fabric_language === "match-existing"` or any other value → emit the en variant; pure monolingual.
|
|
45
|
+
|
|
46
|
+
Protected tokens (`fab_extract_knowledge`, `relevance_scope`,
|
|
47
|
+
`relevance_paths`, `narrow`, `broad`, `source_sessions`, `proposed_reason`,
|
|
48
|
+
`session_context`, `intent_clues`, `tech_stack`, `impact`, `must_read_if`,
|
|
49
|
+
`pending_path`, `layer`, `team`, `personal`,
|
|
50
|
+
`knowledge_scope_degraded`, `MUST`, `NEVER`, `.fabric/knowledge/`, the verbatim
|
|
51
|
+
`强 team` / `强 personal` / `默认 team` heuristic block, etc.) are NEVER
|
|
52
|
+
translated — they appear verbatim in both language variants. The
|
|
53
|
+
bilingualization scope is prose ONLY.
|
|
54
|
+
|
|
55
|
+
### AskUserQuestion i18n Policy (value vs label)
|
|
56
|
+
|
|
57
|
+
When a skill (this one or any sibling skill the user is composing with)
|
|
58
|
+
issues an `AskUserQuestion`, the `header` and `question` strings are
|
|
59
|
+
user-facing prose → translated per `fabric_language`. The `options[]`
|
|
60
|
+
array entries (e.g. `["approve", "reject", "modify", "defer", "skip"]` in
|
|
61
|
+
fabric-review, or `["team", "personal"]` for a layer-flip target) are
|
|
62
|
+
**routing keys** consumed by the skill state machine — they MUST remain
|
|
63
|
+
English regardless of `fabric_language`.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// EN (fabric_language === "en")
|
|
67
|
+
AskUserQuestion({
|
|
68
|
+
header: "Layer-flip target",
|
|
69
|
+
question: "Move '{title}' to which layer? (current: {current_layer})",
|
|
70
|
+
options: ["team", "personal"]
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// zh-CN (fabric_language === "zh-CN")
|
|
74
|
+
AskUserQuestion({
|
|
75
|
+
header: "Layer 切换目标",
|
|
76
|
+
question: "将 '{title}' 切换到哪一层?(当前: {current_layer})",
|
|
77
|
+
options: ["team", "personal"] // 不翻译 — routing key
|
|
78
|
+
})
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Rationale: localizing routing keys would force every routing branch to
|
|
82
|
+
dual-string match (e.g. `if (choice === "team" || choice === "团队")`),
|
|
83
|
+
which doubles the surface area for protected-token regressions and breaks
|
|
84
|
+
the option-list invariants that downstream tooling depends on. Keeping
|
|
85
|
+
`options[]` English-only is contract-locked across all three skills.
|
|
86
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Phase 0 — Range Resolution (ref)
|
|
2
|
+
|
|
3
|
+
> **Loaded on demand.** SKILL.md hot path retains the Phase 0 intro, the Confidence decision rule, and Step 1 (invocation context inspection). This file holds Steps 2-6 (parsing tables, session_id resolution algorithm, AskUserQuestion fallback, carry-forward contract) + worked examples. Read when entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND the user prompt likely carries a range hint that needs parsing.
|
|
4
|
+
|
|
5
|
+
## Step 2 — Time-window parsing
|
|
6
|
+
|
|
7
|
+
Match the user prompt against the following bilingual patterns (case-insensitive substring match, leftmost-longest wins). The matched span yields a `[ts_start, ts_end]` pair in Unix milliseconds. `now` = the skill invocation timestamp.
|
|
8
|
+
|
|
9
|
+
### zh-CN pattern table
|
|
10
|
+
|
|
11
|
+
| Pattern | ts_start | ts_end |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `今日` / `今天` | `floor(now, day)` (本地时区 00:00) | `now` |
|
|
14
|
+
| `上周` / `过去一周` | `now - 7d` | `now` |
|
|
15
|
+
| `过去 N 天` / `近 N 天` (N ∈ 1..30) | `now - N*24h` | `now` |
|
|
16
|
+
| `自上次归档` / `自上次 archive` | tail-scan events.jsonl → most recent `knowledge_proposed.ts` (fallback `events[0].ts`) | `now` |
|
|
17
|
+
|
|
18
|
+
### en pattern table
|
|
19
|
+
|
|
20
|
+
| Pattern | ts_start | ts_end |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `today` | `floor(now, day)` (local TZ 00:00) | `now` |
|
|
23
|
+
| `last week` / `past week` | `now - 7d` | `now` |
|
|
24
|
+
| `past N days` / `last N days` (N ∈ 1..30) | `now - N*24h` | `now` |
|
|
25
|
+
| `since last archive` / `since last archived` | tail-scan events.jsonl → most recent `knowledge_proposed.ts` (fallback `events[0].ts`) | `now` |
|
|
26
|
+
|
|
27
|
+
Notes:
|
|
28
|
+
|
|
29
|
+
- Patterns are non-exclusive — if the prompt matches multiple (e.g. "今日 cite policy"), apply time-window THEN topic-keyword as AND.
|
|
30
|
+
- Numeric N must parse as a positive integer ≤ 30; reject anything else as parse-miss.
|
|
31
|
+
- All other date phrasings (specific dates like `5月10日`, relative phrasings like `三天前下午`) are NOT handled here — emit parse-miss and let Step 5 fallback collect a structured answer.
|
|
32
|
+
|
|
33
|
+
## Step 3 — Topic-keyword extraction
|
|
34
|
+
|
|
35
|
+
After time-window matching (or alongside it when both apply), extract content keywords from the prompt:
|
|
36
|
+
|
|
37
|
+
1. Strip recognised time-window tokens (e.g. remove `今日` / `last week` from the residual prompt).
|
|
38
|
+
2. Tokenize residual on whitespace + CJK boundary. Combine adjacent CJK characters into one token; split en words on spaces.
|
|
39
|
+
3. Filter **stop-words**: skill control verbs (`archive`, `归档`, `下`, `的`), articles / particles (`the`, `a`, `an`, `了`, `吧`), pronouns (`it`, `this`, `that`, `这个`, `那个`), and 1-character en tokens.
|
|
40
|
+
4. Retain **2-5 word tokens** (or 1-token CJK content words ≥ 2 chars like `rc.20`, `cite`). Cap at 8 keywords; drop weaker (later-position) ones.
|
|
41
|
+
|
|
42
|
+
The retained set is `topic_keywords[]`. Empty set = no keyword filter.
|
|
43
|
+
|
|
44
|
+
## Step 4 — session_id resolution algorithm
|
|
45
|
+
|
|
46
|
+
Given `time_window = [ts_start, ts_end] | null` and `topic_keywords[] | []`:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Step a — Read events.jsonl tail (last 500 events) via `Bash: tail -n 500
|
|
50
|
+
.fabric/events.jsonl`. ENOENT → empty list (no resolution possible
|
|
51
|
+
→ emit parse-miss → Step 5 fallback).
|
|
52
|
+
|
|
53
|
+
Step b — Per distinct session_id present in the tail, compute:
|
|
54
|
+
ts_min = min(ts) over events with this session_id
|
|
55
|
+
ts_max = max(ts) over events with this session_id
|
|
56
|
+
digest_path = .fabric/.cache/session-digests/<session_id>.md
|
|
57
|
+
digest_body = Read(digest_path) if exists, else ""
|
|
58
|
+
|
|
59
|
+
Step c — TIME-WINDOW FILTER (skip when time_window is null):
|
|
60
|
+
Keep session_id IFF [ts_min, ts_max] intersects [ts_start, ts_end]
|
|
61
|
+
(i.e. ts_max >= ts_start AND ts_min <= ts_end).
|
|
62
|
+
Multiple time intervals are OR'd within the time-window filter
|
|
63
|
+
category (none currently supported; reserved for future ranges).
|
|
64
|
+
|
|
65
|
+
Step d — TOPIC-KEYWORD FILTER (skip when topic_keywords is empty):
|
|
66
|
+
Keep session_id IFF digest_body (case-insensitive) contains
|
|
67
|
+
AT LEAST ONE keyword from topic_keywords[].
|
|
68
|
+
Multiple keywords are OR'd within the keyword filter category.
|
|
69
|
+
|
|
70
|
+
Step e — AND across filter categories:
|
|
71
|
+
A session must pass BOTH filters when BOTH are present.
|
|
72
|
+
Pass either filter alone when only one is present.
|
|
73
|
+
Pass-through (all sessions) when neither is present.
|
|
74
|
+
|
|
75
|
+
Step f — Result: distinct session_id[] (preserve event-order); if empty AND
|
|
76
|
+
a parse hit was claimed → degrade to Step 5 fallback (user wanted a
|
|
77
|
+
range that resolved to zero sessions).
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Step 5 — AskUserQuestion fallback (E2 / E4 only)
|
|
81
|
+
|
|
82
|
+
When Step 2/3 emit parse-miss OR Step 4 resolves to zero sessions AND the invocation type permits prompting (E2 user-active or E4 user回溯-active — NEVER E1 hook / E3 AI-self / E5 cron), surface a structured question. UX i18n Policy class 5 applies: `header` + `question` translate per `fabric_language`; `options[]` routing keys stay English.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
AskUserQuestion({
|
|
86
|
+
header: "Archive range", // zh-CN: "归档范围"
|
|
87
|
+
question:
|
|
88
|
+
"Which session range should this archive cover? " +
|
|
89
|
+
"(today = current calendar day; last-week = past 7 days; " +
|
|
90
|
+
"since-last-archive = newer than last knowledge_proposed event; " +
|
|
91
|
+
"custom = type a free-form range)",
|
|
92
|
+
options: ["today", "last-week", "since-last-archive", "custom"]
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Routing:
|
|
97
|
+
|
|
98
|
+
| Choice | Action |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `today` | Re-enter Step 2 with synthetic prompt `今日` / `today` (per `fabric_language`); resolve session_ids; proceed to Phase 0.5. |
|
|
101
|
+
| `last-week` | Re-enter Step 2 with synthetic prompt `上周` / `last week`; proceed to Phase 0.5. |
|
|
102
|
+
| `since-last-archive` | Re-enter Step 2 with synthetic prompt `自上次归档` / `since last archive`; proceed to Phase 0.5. |
|
|
103
|
+
| `custom` | Surface a one-line text prompt to the user ("type a range, e.g. 'rc.20', 'past 3 days', '上周 cite policy'"). Re-enter Phase 0 Step 1 with the user-typed sub-prompt. Loop max 1 time — second parse-miss falls through to `range = "all"` with a warning. |
|
|
104
|
+
|
|
105
|
+
## Step 6 — Carry-forward contract
|
|
106
|
+
|
|
107
|
+
Phase 0 produces ONE of:
|
|
108
|
+
|
|
109
|
+
- `session_id[]` (non-empty array of distinct session_ids) — passed to Phase 1 as the explicit scope filter; Phase 1 skips its own anchor-walk and uses this list directly.
|
|
110
|
+
- `"all"` (sentinel string) — no range hint detected; Phase 1 falls back to the legacy anchor-walk behaviour ("all distinct sessions since last `knowledge_proposed`").
|
|
111
|
+
|
|
112
|
+
NEVER pass an empty `session_id[]` forward — that case must degrade to Step 5 fallback (or, when fallback is forbidden by invocation type, to `"all"` with a one-line stderr warning).
|
|
113
|
+
|
|
114
|
+
## Worked examples
|
|
115
|
+
|
|
116
|
+
### Example A — time-only: `今日复盘`
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
Step 1: prompt = "今日复盘"; user_invocation_type = E2.
|
|
120
|
+
Step 2: matches `今日` → time_window = [floor(now, day), now].
|
|
121
|
+
Step 3: residual "复盘" survives stop-word filter → topic_keywords = ["复盘"].
|
|
122
|
+
(Edge case: the residual content word may also filter; if 复盘 is
|
|
123
|
+
in the stop list it becomes []. Treat as topic-keyword empty.)
|
|
124
|
+
Step 4: tail-scan events.jsonl; keep sessions whose [ts_min, ts_max]
|
|
125
|
+
intersects today's window. Say 3 sessions match.
|
|
126
|
+
Step 5: skipped (resolution succeeded).
|
|
127
|
+
Step 6: emit session_id[] = ["sess-a", "sess-b", "sess-c"] → Phase 0.5.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Example B — keyword-only: `rc.20 的归档下`
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
Step 1: prompt = "rc.20 的归档下"; user_invocation_type = E2.
|
|
134
|
+
Step 2: no time pattern matches → time_window = null.
|
|
135
|
+
Step 3: strip "归档"/"下"/"的" stop-words → topic_keywords = ["rc.20"].
|
|
136
|
+
Step 4: tail-scan events.jsonl; for each session_id, Read its digest;
|
|
137
|
+
keep those whose digest body matches /rc\.20/i. Say 2 sessions
|
|
138
|
+
match (one was the rc.20 grilling session, one had a tangential
|
|
139
|
+
mention).
|
|
140
|
+
Step 5: skipped.
|
|
141
|
+
Step 6: emit session_id[] = ["sess-x", "sess-y"] → Phase 0.5.
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Example C — combined: `上周 rc.20`
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
Step 1: prompt = "上周 rc.20"; user_invocation_type = E4.
|
|
148
|
+
Step 2: matches `上周` → time_window = [now - 7d, now].
|
|
149
|
+
Step 3: strip "上周" → topic_keywords = ["rc.20"].
|
|
150
|
+
Step 4: AND filter — keep sessions whose [ts_min, ts_max] intersects last
|
|
151
|
+
week AND whose digest matches /rc\.20/i. Say 1 session matches.
|
|
152
|
+
Step 5: skipped.
|
|
153
|
+
Step 6: emit session_id[] = ["sess-z"] → Phase 0.5.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
If Example C had resolved to zero sessions (e.g. user types `上周 rc.99`), Step 4 would degrade into Step 5 — surfacing AskUserQuestion since E4 permits prompting.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Phase 1.5 — First-run Onboard Phase (ref)
|
|
2
|
+
|
|
3
|
+
> **Loaded on demand.** SKILL.md hot path only runs this when entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback} AND `fabric onboard-coverage --json` reports `missing.length > 0`. For E1/E3/E5 entries OR fully-covered workspaces, this entire phase is skipped — no reason to load.
|
|
4
|
+
|
|
5
|
+
## Phase 1.5 — First-run Onboard Phase
|
|
6
|
+
|
|
7
|
+
#### Phase 1.5 Trigger Gate (rc.25 — entry-context aware)
|
|
8
|
+
|
|
9
|
+
Before running ANY of the onboard coverage steps below, evaluate the
|
|
10
|
+
**entry-context gate**. Onboard slot collection is an interactive,
|
|
11
|
+
one-time project-tone capture flow that REQUIRES live user dialogue.
|
|
12
|
+
Non-user-active entries (hook / AI self-trigger / cron) either interrupt
|
|
13
|
+
the user mid-work or run unattended where dialogue is impossible, so
|
|
14
|
+
they MUST skip Phase 1.5 entirely and fall through to Phase 0.
|
|
15
|
+
|
|
16
|
+
Read `context.entry_point` — already determined in **Phase 0 Range
|
|
17
|
+
Resolution** (see TASK-04 / Phase 0 section above). The 5-entry model
|
|
18
|
+
is the canonical taxonomy for this gate.
|
|
19
|
+
|
|
20
|
+
##### Entry-context detection rules
|
|
21
|
+
|
|
22
|
+
| Entry | Symbol | Detection rule (LLM-native, evaluated at skill entry) |
|
|
23
|
+
|-------|--------|-------------------------------------------------------|
|
|
24
|
+
| **E1** | `hook_passive` | stdout JSON `{decision:'block', ...}` from `archive-hint.cjs` detected at skill entry (the Stop-hook reminder path). |
|
|
25
|
+
| **E2** | `explicit_user_invoke` | User prompt is a direct invocation: `fabric archive` / `/fabric-archive` / `archive what we just did` / `归档一下` / similar imperative. |
|
|
26
|
+
| **E3** | `ai_self_trigger` | AI internal marker `self-archive policy triggered by signal: <X>` present (substring match on the verbatim prefix `self-archive policy triggered by signal` per AGENTS.md self-archive policy section; `<X>` is the signal name. v2.0.0-rc.37 NEW-2 simplified the AGENTS.md taxonomy to 2 categories: `User-driven normative` / `Wrong-turn-and-revert`. Back-compat: legacy 4-state names (`Normative` / `Decision confirmation` / `Explicit dismissal`) still route correctly because the substring gate only matches the verbatim prefix and treats any text after `signal:` as the signal label.) |
|
|
27
|
+
| **E4** | `user_range_rollback` | Prompt contains a **range hint** (parsed in Phase 0 — e.g. `今日` / `上周` / `rc.20`) AND the user is invoking. Sub-mode of E2. |
|
|
28
|
+
| **E5** | `cron` | Prompt contains literal `今日复盘` / `daily recap` / `daily-archive` AND no human is present (running under `/loop`, OS cron, or scheduled trigger). |
|
|
29
|
+
|
|
30
|
+
##### Gate decision
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
IF context.entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback}:
|
|
34
|
+
→ gate = PROCEED # user is live, dialogue is possible
|
|
35
|
+
→ continue to Step 1 (Check coverage) below
|
|
36
|
+
ELSE (E1_hook_passive | E3_ai_self_trigger | E5_cron):
|
|
37
|
+
→ gate = SKIP # no live user, onboard prompting would misfire
|
|
38
|
+
→ emit one-line log: "Phase 1.5 skipped (entry=<E1|E3|E5>, no live user)"
|
|
39
|
+
→ proceed directly to Phase 2
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
##### Rationale
|
|
43
|
+
|
|
44
|
+
Onboard slot collection is a one-time project-tone capture flow that
|
|
45
|
+
requires user dialogue. Non-user-active entries (hook / AI / cron)
|
|
46
|
+
interrupt the user mid-work or run unattended where dialogue is
|
|
47
|
+
impossible, so they MUST skip Phase 1.5. The S5 slot semantics
|
|
48
|
+
(`tech-stack-decision`, `architecture-pattern`, ...) are user-validated
|
|
49
|
+
baselines — populating them from a hook fire-and-forget or a cron daily
|
|
50
|
+
recap would defeat the purpose of capturing _user-confirmed_ project
|
|
51
|
+
tone.
|
|
52
|
+
|
|
53
|
+
##### Tradeoff (documented in CHANGELOG)
|
|
54
|
+
|
|
55
|
+
A first-time user whose ONLY invocations ever come via hook (never an
|
|
56
|
+
explicit `/fabric-archive`) will not see the onboard prompt; the 5
|
|
57
|
+
onboard slots remain empty. Mitigation: documentation tells users to
|
|
58
|
+
run an explicit `fabric archive` at least once to populate the onboard
|
|
59
|
+
baseline.
|
|
60
|
+
|
|
61
|
+
##### Worked example
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
$ /loop 24h /fabric-archive 今日复盘
|
|
65
|
+
→ cron context, no live user
|
|
66
|
+
→ Phase 0 detects literal "今日复盘" + no-human marker
|
|
67
|
+
→ context.entry_point = E5_cron
|
|
68
|
+
→ Phase 1.5 Trigger Gate evaluates: E5 ∉ {E2, E4} → SKIP
|
|
69
|
+
→ emit log "Phase 1.5 skipped (entry=E5, no live user)"
|
|
70
|
+
→ proceed directly to Phase 2 (collect candidates for daily window)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Contrast with E2:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
$ /fabric-archive
|
|
77
|
+
→ user typed explicit invocation
|
|
78
|
+
→ Phase 0: context.entry_point = E2_explicit_user_invoke
|
|
79
|
+
→ Phase 1.5 Trigger Gate evaluates: E2 ∈ {E2, E4} → PROCEED
|
|
80
|
+
→ run Step 1 (Check coverage) below
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
After F8a removed the auto-`fabric scan` baseline pipeline, a freshly installed
|
|
86
|
+
Fabric workspace ships with an EMPTY `.fabric/knowledge/` tree. Five fixed
|
|
87
|
+
**S5 onboard slots** capture the "project tone" baseline that the AI needs
|
|
88
|
+
for high-quality plan_context retrieval from day one:
|
|
89
|
+
|
|
90
|
+
- `tech-stack-decision` — primary languages / frameworks / runtime stack
|
|
91
|
+
- `architecture-pattern` — module layout, service boundaries, layering rules
|
|
92
|
+
- `code-style-tone` — naming / formatting / idiom conventions the project enforces
|
|
93
|
+
- `build-system-idiom` — build tool quirks, scripts, deploy pipeline shape
|
|
94
|
+
- `domain-vocabulary` — business / product terminology that names code entities
|
|
95
|
+
|
|
96
|
+
This phase runs ONCE per archive-skill invocation, BEFORE Phase 2 evidence
|
|
97
|
+
gathering, so coverage state is fresh for the session.
|
|
98
|
+
|
|
99
|
+
#### Step 1 — Check coverage
|
|
100
|
+
|
|
101
|
+
Invoke `fabric onboard-coverage --json` and parse the JSON payload:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
fabric onboard-coverage --json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Expected shape:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"filled": { "tech-stack-decision": ["KT-DEC-0012"], ... },
|
|
112
|
+
"missing": ["architecture-pattern", "code-style-tone"],
|
|
113
|
+
"opted_out": ["domain-vocabulary"],
|
|
114
|
+
"total": 5
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Step 2 — Decide
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
IF missing.length === 0:
|
|
122
|
+
→ skip Phase 1.5 entirely; proceed to Phase 0.
|
|
123
|
+
ELSE:
|
|
124
|
+
→ ask the user how to handle the missing slots (Step 3).
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Step 3 — Prompt user
|
|
128
|
+
|
|
129
|
+
Present a single roll-up listing each missing slot. UX i18n Policy class 5
|
|
130
|
+
applies: the `header` + `question` strings are translated per
|
|
131
|
+
`fabric_language`; the `options[]` routing keys stay English.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
AskUserQuestion({
|
|
135
|
+
header: "Onboard coverage", // zh-CN: "首装基调覆盖"
|
|
136
|
+
question:
|
|
137
|
+
"KB is missing the following project-tone slots: " +
|
|
138
|
+
missing.join(", ") +
|
|
139
|
+
". Tour the project and propose pending entries for each?",
|
|
140
|
+
options: ["fill-all", "fill-each", "dismiss-all", "skip"]
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`fab_extract_knowledge` is called with `onboard_slot: <slot>` set so each
|
|
145
|
+
proposed entry counts toward coverage once approved via fab_review.
|
|
146
|
+
|
|
147
|
+
| User choice | Action |
|
|
148
|
+
|----------------|--------|
|
|
149
|
+
| `fill-all` | For EACH slot in `missing`, run Step 4 (Tour-and-propose). All proposals share session_id; one batch review at the end (Phase 3). |
|
|
150
|
+
| `fill-each` | Loop slot-by-slot through `missing`. Per slot: ask user `confirm | dismiss | skip` (per-slot AskUserQuestion); `confirm` → run Step 4; `dismiss` → `fabric config dismiss-slot <slot>`; `skip` → leave for next archive run. |
|
|
151
|
+
| `dismiss-all` | For EACH slot in `missing`, invoke `Bash("fabric config dismiss-slot <slot>")`. Print a one-line confirmation each. Skip to Phase 0. |
|
|
152
|
+
| `skip` | No-op. Slots remain in `missing` for the next archive run. Skip to Phase 0. |
|
|
153
|
+
|
|
154
|
+
#### Step 4 — Tour-and-propose (per-slot)
|
|
155
|
+
|
|
156
|
+
For each slot to fill, the LLM independently sources slot-specific evidence
|
|
157
|
+
from the project (no user prompt — this is a Read-only tour):
|
|
158
|
+
|
|
159
|
+
| Slot | Source files (LLM should Read these) |
|
|
160
|
+
|--------------------------|---------------------------------------|
|
|
161
|
+
| `tech-stack-decision` | `package.json` (+ lockfile), `pyproject.toml` / `Cargo.toml` / `go.mod`, `tsconfig.json`, root README |
|
|
162
|
+
| `architecture-pattern` | Top-level dir tree (`ls -F`), 1-2 entry-point files (`src/index.ts`, `main.go`, etc.), framework-config files (`next.config`, `vite.config`, `astro.config`) |
|
|
163
|
+
| `code-style-tone` | `.editorconfig`, `prettier.config.*`, `eslint.config.*`, `biome.*`, `.prettierrc*`, framework lint config, 2-3 representative source files for naming-pattern inference |
|
|
164
|
+
| `build-system-idiom` | `package.json` `scripts` block, `Makefile`, `taskfile.yaml`, CI yml (`.github/workflows/*.yml`), Dockerfile if present |
|
|
165
|
+
| `domain-vocabulary` | README, `docs/*.md`, top-level `src/` directory names (often domain-aligned), public API entry types |
|
|
166
|
+
|
|
167
|
+
After Read-ing the slot-specific sources, classify the observation:
|
|
168
|
+
|
|
169
|
+
- `tech-stack-decision` → type=`decisions`, `proposed_reason=decision-confirmation`
|
|
170
|
+
- `architecture-pattern` → type=`models`, `proposed_reason=new-dependency-or-pattern`
|
|
171
|
+
- `code-style-tone` → type=`guidelines`, `proposed_reason=explicit-user-mark` (the project ITSELF is the mark)
|
|
172
|
+
- `build-system-idiom` → type=`processes`, `proposed_reason=new-dependency-or-pattern`
|
|
173
|
+
- `domain-vocabulary` → type=`models`, `proposed_reason=new-dependency-or-pattern`
|
|
174
|
+
|
|
175
|
+
Call `fab_extract_knowledge` with the inferred fields PLUS `onboard_slot:
|
|
176
|
+
<slot>`. The pending file's frontmatter will carry the slot label, and the
|
|
177
|
+
next `fabric onboard-coverage` run will see the slot as filled (once approved
|
|
178
|
+
via fab_review).
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
mcp__fabric__fab_extract_knowledge({
|
|
184
|
+
source_sessions: ["<current-session-id>"],
|
|
185
|
+
recent_paths: ["package.json", "tsconfig.json"],
|
|
186
|
+
user_messages_summary: "Project uses TypeScript + pnpm workspace + Vitest. Node 20 LTS target. ESM-only.",
|
|
187
|
+
type: "decisions",
|
|
188
|
+
slug: "primary-tech-stack",
|
|
189
|
+
layer: "team",
|
|
190
|
+
relevance_scope: "broad", // tech stack applies everywhere
|
|
191
|
+
relevance_paths: [],
|
|
192
|
+
proposed_reason: "decision-confirmation",
|
|
193
|
+
session_context:
|
|
194
|
+
"Session goal: capture onboard tech-stack baseline.\nTurning point: read package.json + tsconfig.json + pnpm-workspace.yaml; stack confirmed.",
|
|
195
|
+
onboard_slot: "tech-stack-decision", // ← claims the slot
|
|
196
|
+
tech_stack: ["typescript", "nodejs", "pnpm", "vitest"]
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### Onboard phase constraints (DO NOT TRANSLATE)
|
|
201
|
+
|
|
202
|
+
- MUST run BEFORE Phase 2 evidence gathering — onboard is a separate flow,
|
|
203
|
+
not interleaved with session-archive candidates.
|
|
204
|
+
- MUST call `fabric onboard-coverage --json` before deciding; never assume
|
|
205
|
+
coverage state.
|
|
206
|
+
- NEVER fill a slot that is in `opted_out` — `fabric onboard-coverage` already
|
|
207
|
+
excludes those from `missing`, but the Skill MUST NOT re-propose them
|
|
208
|
+
even if the user asks "fill all of them" — the dismiss is intentional.
|
|
209
|
+
- NEVER prompt the user when `missing.length === 0` — silent skip.
|
|
210
|
+
- NEVER set `onboard_slot` on a regular session-archive candidate in
|
|
211
|
+
Phase 4 — that field is RESERVED for the onboard phase. Mixing the
|
|
212
|
+
two would let session-archive proposals masquerade as onboard
|
|
213
|
+
coverage and let any random pending file claim a slot.
|
|
214
|
+
- MUST emit `onboard_slot: <slot>` verbatim — the slot name is one of
|
|
215
|
+
the locked S5 strings (tech-stack-decision / architecture-pattern /
|
|
216
|
+
code-style-tone / build-system-idiom / domain-vocabulary). The
|
|
217
|
+
fab_extract_knowledge schema enum will reject anything else.
|
|
218
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Phase 1 — Collect Cross-Session Digests (ref)
|
|
2
|
+
|
|
3
|
+
> **Loaded on demand.** SKILL.md hot path retains Phase 1's purpose statement + 5-step summary + graceful-degradation note. This file holds Steps 1-5 detailed implementation (events.jsonl tail-scan, anchor-walk, digest load, rc.25 TASK-05 ledger filter algorithm + constants + worked examples, cross-session context build).
|
|
4
|
+
|
|
5
|
+
> **v2.0.0-rc.37 NEW-9 — Steps 1-4.5 moved server-side.** The deterministic part of this algorithm (events.jsonl tail-scan, anchor-find, session forward-collect, and the Step 4.5 outcome-ledger filter state machine) now runs in the server and is exposed as the `fab_archive_scan` MCP tool — call it instead of hand-running `tail`/grep. The tool returns the already-filtered `session_ids[]` + `anchor_ts` + `covered_through_ts` + `already_proposed_keys[]`. Steps 1-4.5 below remain as the AUTHORITATIVE SPEC of what the server computes (and the contract tests pin it); the Skill no longer executes them by hand. Step 5 (digest load + cross-session context stitch) stays LLM-side per Boundary B.
|
|
6
|
+
|
|
7
|
+
## Step 1 — Read events.jsonl tail
|
|
8
|
+
|
|
9
|
+
Use `Bash` with `tail -n 200 .fabric/events.jsonl` (tolerate ENOENT — empty ledger is a normal first-run state).
|
|
10
|
+
|
|
11
|
+
## Step 2 — Find the anchor
|
|
12
|
+
|
|
13
|
+
Walk the tail backwards to locate the most recent `knowledge_proposed` event (`event_type === "knowledge_proposed"`). The anchor's `ts` becomes the lower bound for digest selection. If NO anchor exists, treat all digests in the cache as in-scope.
|
|
14
|
+
|
|
15
|
+
## Step 3 — Collect session_ids since anchor
|
|
16
|
+
|
|
17
|
+
Scan the tail forward from the anchor and collect every distinct `session_id` field that appears on any event newer than the anchor. Distinct ordering preserved.
|
|
18
|
+
|
|
19
|
+
## Step 4 — Load digests
|
|
20
|
+
|
|
21
|
+
For each collected `session_id`, read `.fabric/.cache/session-digests/<session_id>.md`. Missing digest files degrade silently (the digest write was best-effort, so a Stop hook crash can produce a session_id without a digest). Cap the loaded digest set at `archive_digest_max_sessions` most-recent sessions (config-resolved, default 10) to bound LLM context (~50KB worst-case at default).
|
|
22
|
+
|
|
23
|
+
## Step 4.5 — Filter via session_archive_attempted ledger (rc.25 TASK-05)
|
|
24
|
+
|
|
25
|
+
Before Step 5 builds the cross-session context, drop sessions that the outcome ledger says we should not re-scan. For each `session_id` collected in Steps 1-3, scan `.fabric/events.jsonl` for events where `event_type === "session_archive_attempted"` AND `session_id` matches, keep the most-recent one by `ts`, and apply this state machine:
|
|
26
|
+
|
|
27
|
+
- **(a) Look up the most recent `session_archive_attempted`** event for this `session_id` (none found → fall through to (e)).
|
|
28
|
+
- **(b) `outcome === "user_dismissed"` → drop (permanent skip).** The user explicitly rejected this session's candidates; never auto-re-scan it. Respect the dismissal forever — re-scanning would re-propose the same content the user already declined.
|
|
29
|
+
- **(c) `(nowMs - attempted_event.ts) < ANTI_LOOP_HOURS * 3_600_000` → drop (cooldown skip).** Anti-loop window: even if outcome is otherwise re-scannable, never re-scan a session within 12 hours of the last attempt. Aligns 心智 with the Stop-hook cooldown so a single user does not see the same session repeatedly within one work day.
|
|
30
|
+
- **(d) `covered_through_ts` present → check for high-value signal in `ts > covered_through_ts` events for this `session_id`.** Tail-scan `events.jsonl` for events newer than the watermark whose `session_id` matches. A session passes this gate iff at least ONE of:
|
|
31
|
+
- ≥1 event with `event_type ∈ HIGH_VALUE_EVENT_TYPES` (`knowledge_context_planned`, `edit_paths_recorded`), OR
|
|
32
|
+
- the latest `assistant_turn_observed` event body contains ≥1 of `NORMATIVE_KEYWORDS` (substring match, case-insensitive for English entries).
|
|
33
|
+
|
|
34
|
+
No high-value signal → drop (no new content worth re-scanning, even though the cooldown has expired). Has signal → keep for re-scan.
|
|
35
|
+
- **(e) Never attempted (no `session_archive_attempted` event found for this `session_id`) → keep.** First-time scan; nothing to filter against.
|
|
36
|
+
- **(f) Cross-session pending dedupe** (operates on candidate observations, not on `session_id` filter): gather all `knowledge_proposed_ids` from `session_archive_attempted` events with `outcome === "proposed"` across ALL sessions in the recent window (NOT just the current candidate session). This builds a global set of idempotency keys already proposed by prior archive runs but not yet reviewed by the user (`.fabric/knowledge/pending/` may still contain them). When classifying new observations in Phase 3, drop any candidate whose computed `idempotency_key` matches an id already in this set — it was already proposed by an earlier archive run, the user just hasn't reviewed it yet, so re-proposing would duplicate pending entries and inflate `candidates_proposed` counts. Per Phase 4.5 dedupe consumer of `knowledge_proposed_ids`.
|
|
37
|
+
|
|
38
|
+
The resulting filtered `session_id[]` proceeds into Step 5's digest concatenation. Sessions filtered out in this step do NOT contribute to `### Cross-session digest`, are NOT included in `source_sessions` on any fab_extract_knowledge call, and are NOT referenced in `session_context` bodies.
|
|
39
|
+
|
|
40
|
+
### Constants (rc.25 — verbatim)
|
|
41
|
+
|
|
42
|
+
- `ANTI_LOOP_HOURS = 12` — cooldown window in hours between consecutive re-scans of the same `session_id`. Rationale: 心智对齐 hook cooldown (`stop_hook_cooldown_hours = 12`); identical mental model avoids user confusion when a session shows up in both hook reminders and archive re-scan candidates.
|
|
43
|
+
- `HIGH_VALUE_EVENT_TYPES = ['knowledge_context_planned', 'edit_paths_recorded']` — event types that count as "new substantive activity worth re-scanning" past `covered_through_ts`. Chat accumulation (`assistant_turn_observed` alone) does NOT count — it would let mere conversation noise trigger re-scans.
|
|
44
|
+
- `NORMATIVE_KEYWORDS = ['以后','always','never','from now on','下次','记一下','永远不要']` — substring patterns scanned against the latest `assistant_turn_observed` body for the session. Mixed CN/EN to cover bilingual users. If any keyword hits, the session is flagged as having high-value chat-only signal even without code edits.
|
|
45
|
+
|
|
46
|
+
### Worked examples
|
|
47
|
+
|
|
48
|
+
- **Session X (user_dismissed)** — last `session_archive_attempted` ts = 3 days ago, outcome = `user_dismissed`. Rule (b) fires → permanent skip. Session X is dropped even if 50 new `knowledge_context_planned` events have accumulated since.
|
|
49
|
+
- **Session Y (proposed 6h ago)** — last `session_archive_attempted` ts = 6h ago, outcome = `proposed`. Rule (c) fires: 6h < 12h cooldown window → drop (cooldown skip). Y becomes eligible again after the 12h window closes, provided high-value signal accumulates by then.
|
|
50
|
+
- **Session Z (viability_failed 14h ago + 3 new plan_context)** — last `session_archive_attempted` ts = 14h ago, outcome = `viability_failed`, `covered_through_ts` = T₀. Rules (b)(c) pass. Rule (d) tail-scans for `session_id === Z AND ts > T₀`: finds 3 `knowledge_context_planned` events. HIGH_VALUE_EVENT_TYPES match → keep Z for re-scan. The previous viability failure does not block a re-scan once new substantive activity has accumulated.
|
|
51
|
+
|
|
52
|
+
## Step 5 — Build cross-session context
|
|
53
|
+
|
|
54
|
+
Concatenate the loaded digests into a single `### Cross-session digest` block to carry into Phase 2.5 + Phase 1. Use this block to:
|
|
55
|
+
|
|
56
|
+
- Detect session-spanning patterns (e.g. a discussion that started in session A and continued in session B).
|
|
57
|
+
- Populate the `source_sessions` array on every fab_extract_knowledge call — the array form (T5) replaces the legacy `source_session` string.
|
|
58
|
+
- Inform the `session_context` blob written to each pending entry's body (3-5 lines summarizing goal + key turning point, per T6).
|
|
59
|
+
|
|
60
|
+
## Graceful degradation
|
|
61
|
+
|
|
62
|
+
If `.fabric/.cache/session-digests/` is missing entirely, this phase reports an empty context and Phase 2 falls back to the single-session behaviour. Tests that synthesize events.jsonl without populating the digest cache continue to work. If `session_archive_attempted` events are missing entirely (pre-rc.25 ledger or rotation has trimmed older events), treat all sessions as never-attempted (current default behavior) — Step 4.5 rule (e) applies uniformly, so the filter degrades to the legacy "scan everything since anchor" semantics without raising errors.
|