@fenglimg/fabric-cli 2.0.0-rc.22 → 2.0.0-rc.25

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.
@@ -23,13 +23,220 @@ If none of the above hold, stop the skill immediately and tell the user (UX i18n
23
23
 
24
24
  This skill is `Check-not-Ask`, not a preference interview:
25
25
 
26
+ - **Phase 0.4 (rc.23 F8c) first-run onboard phase** — checks S5 onboard-slot coverage; if unclaimed slots remain, prompts user to fill / dismiss / skip before proceeding to normal archive flow
26
27
  - Phase 0 proactively gathers candidate evidence from the session
27
28
  - Phase 0.5 viability gate aborts the skill if the session lacks any archive-signal (anti-archive guard)
28
29
  - Phase 1 classifies / layers / slugs each candidate and presents one batch review for user correction
29
30
  - Phase 1.5 assigns `relevance_scope=narrow|broad` and derives `relevance_paths` from edit history (rc.5 single-signal source)
30
31
  - Phase 2 calls `fab_extract_knowledge` once per confirmed candidate
31
32
 
32
- ## 执行流程 (5 Phase / 1 User Review Round)
33
+ ## 执行流程 (6 Phase / 1 User Review Round)
34
+
35
+ ### Phase -0.5 — Range Resolution (rc.25 E4 Entry Foundation)
36
+
37
+ When the skill is invoked, the user's prompt may carry an explicit range hint —
38
+ a time window (`今日` / `last week`), a topic keyword (`rc.20`, `cite policy`),
39
+ or a literal session_id reference. This phase parses those hints and resolves
40
+ them to a concrete `session_id[]` set that constrains Phase 0.0 cross-session
41
+ digest collection. **Falls through silently** when no hint is detected — Phase
42
+ 0.0 then sees the legacy "all distinct sessions since last anchor" behaviour.
43
+
44
+ This is the foundation of the **E4 (user-language range selection) entry
45
+ point** per rc.25 Q3.3. AI (Claude/Codex) interprets the rules below at runtime
46
+ — there is no parser code; the LLM IS the parser. Time-window patterns +
47
+ keyword extraction are LLM-native tasks; an `AskUserQuestion` fallback covers
48
+ the low-confidence case.
49
+
50
+ #### Step 1 — Invocation context inspection
51
+
52
+ Read three sources to determine whether a range hint is present:
53
+
54
+ | Source | Inspection | Yields |
55
+ |---|---|---|
56
+ | User prompt text (the natural-language string that triggered the skill) | Free-form parse for time words + topic keywords + literal `session_id=...` | Candidate `time_window`, `topic_keywords[]`, `explicit_session_ids[]` |
57
+ | Hook-context-marker (only when entry = E1 hook-triggered) | Already-parsed `{count, hours_since_last, sessions_since_last_proposed}` block emitted by archive-hint.cjs | Optional default scope = "since last archive" |
58
+ | User invocation type | E1 / E2 / E3 / E4 / E5 (per rc.25 5-entry model) | Decides whether to fall back to `AskUserQuestion` (E2/E4 only) |
59
+
60
+ If NONE of the three yields a usable hint AND `user_invocation_type ∉ {E2, E4}`,
61
+ fall through directly to Phase 0.6 with `range = "all"` sentinel (legacy
62
+ behaviour). E2 / E4 with no hint → proceed to Step 5 fallback.
63
+
64
+ #### Step 2 — Time-window parsing
65
+
66
+ Match the user prompt against the following bilingual patterns (case-insensitive
67
+ substring match, leftmost-longest wins). The matched span yields a
68
+ `[ts_start, ts_end]` pair in Unix milliseconds. `now` = the skill invocation
69
+ timestamp.
70
+
71
+ zh-CN pattern table:
72
+
73
+ | Pattern | ts_start | ts_end |
74
+ |---|---|---|
75
+ | `今日` / `今天` | `floor(now, day)` (本地时区 00:00) | `now` |
76
+ | `上周` / `过去一周` | `now - 7d` | `now` |
77
+ | `过去 N 天` / `近 N 天` (N ∈ 1..30) | `now - N*24h` | `now` |
78
+ | `自上次归档` / `自上次 archive` | tail-scan events.jsonl → most recent `knowledge_proposed.ts` (fallback `events[0].ts`) | `now` |
79
+
80
+ en pattern table:
81
+
82
+ | Pattern | ts_start | ts_end |
83
+ |---|---|---|
84
+ | `today` | `floor(now, day)` (local TZ 00:00) | `now` |
85
+ | `last week` / `past week` | `now - 7d` | `now` |
86
+ | `past N days` / `last N days` (N ∈ 1..30) | `now - N*24h` | `now` |
87
+ | `since last archive` / `since last archived` | tail-scan events.jsonl → most recent `knowledge_proposed.ts` (fallback `events[0].ts`) | `now` |
88
+
89
+ Notes:
90
+
91
+ - Patterns are non-exclusive — if the prompt matches multiple (e.g. "今日 cite policy"),
92
+ apply time-window THEN topic-keyword as AND.
93
+ - Numeric N must parse as a positive integer ≤ 30; reject anything else as parse-miss.
94
+ - All other date phrasings (specific dates like `5月10日`, relative phrasings
95
+ like `三天前下午`) are NOT handled here — emit parse-miss and let Step 5
96
+ fallback collect a structured answer.
97
+
98
+ #### Step 3 — Topic-keyword extraction
99
+
100
+ After time-window matching (or alongside it when both apply), extract content
101
+ keywords from the prompt:
102
+
103
+ 1. Strip recognised time-window tokens (e.g. remove `今日` / `last week` from
104
+ the residual prompt).
105
+ 2. Tokenize residual on whitespace + CJK boundary. Combine adjacent CJK
106
+ characters into one token; split en words on spaces.
107
+ 3. Filter **stop-words**: skill control verbs (`archive`, `归档`, `下`, `的`),
108
+ articles / particles (`the`, `a`, `an`, `了`, `吧`), pronouns (`it`, `this`,
109
+ `that`, `这个`, `那个`), and 1-character en tokens.
110
+ 4. Retain **2-5 word tokens** (or 1-token CJK content words ≥ 2 chars like
111
+ `rc.20`, `cite`). Cap at 8 keywords; drop weaker (later-position) ones.
112
+
113
+ The retained set is `topic_keywords[]`. Empty set = no keyword filter.
114
+
115
+ #### Step 4 — session_id resolution algorithm
116
+
117
+ Given `time_window = [ts_start, ts_end] | null` and `topic_keywords[] | []`:
118
+
119
+ ```
120
+ Step a — Read events.jsonl tail (last 500 events) via `Bash: tail -n 500
121
+ .fabric/events.jsonl`. ENOENT → empty list (no resolution possible
122
+ → emit parse-miss → Step 5 fallback).
123
+
124
+ Step b — Per distinct session_id present in the tail, compute:
125
+ ts_min = min(ts) over events with this session_id
126
+ ts_max = max(ts) over events with this session_id
127
+ digest_path = .fabric/.cache/session-digests/<session_id>.md
128
+ digest_body = Read(digest_path) if exists, else ""
129
+
130
+ Step c — TIME-WINDOW FILTER (skip when time_window is null):
131
+ Keep session_id IFF [ts_min, ts_max] intersects [ts_start, ts_end]
132
+ (i.e. ts_max >= ts_start AND ts_min <= ts_end).
133
+ Multiple time intervals are OR'd within the time-window filter
134
+ category (none currently supported; reserved for future ranges).
135
+
136
+ Step d — TOPIC-KEYWORD FILTER (skip when topic_keywords is empty):
137
+ Keep session_id IFF digest_body (case-insensitive) contains
138
+ AT LEAST ONE keyword from topic_keywords[].
139
+ Multiple keywords are OR'd within the keyword filter category.
140
+
141
+ Step e — AND across filter categories:
142
+ A session must pass BOTH filters when BOTH are present.
143
+ Pass either filter alone when only one is present.
144
+ Pass-through (all sessions) when neither is present.
145
+
146
+ Step f — Result: distinct session_id[] (preserve event-order); if empty AND
147
+ a parse hit was claimed → degrade to Step 5 fallback (user wanted a
148
+ range that resolved to zero sessions).
149
+ ```
150
+
151
+ #### Step 5 — AskUserQuestion fallback (E2 / E4 only)
152
+
153
+ When Step 2/3 emit parse-miss OR Step 4 resolves to zero sessions AND the
154
+ invocation type permits prompting (E2 user-active or E4 user回溯-active —
155
+ NEVER E1 hook / E3 AI-self / E5 cron), surface a structured question. UX i18n
156
+ Policy class 5 applies: `header` + `question` translate per `fabric_language`;
157
+ `options[]` routing keys stay English.
158
+
159
+ ```ts
160
+ AskUserQuestion({
161
+ header: "Archive range", // zh-CN: "归档范围"
162
+ question:
163
+ "Which session range should this archive cover? " +
164
+ "(today = current calendar day; last-week = past 7 days; " +
165
+ "since-last-archive = newer than last knowledge_proposed event; " +
166
+ "custom = type a free-form range)",
167
+ options: ["today", "last-week", "since-last-archive", "custom"]
168
+ })
169
+ ```
170
+
171
+ Routing:
172
+
173
+ | Choice | Action |
174
+ |---|---|
175
+ | `today` | Re-enter Step 2 with synthetic prompt `今日` / `today` (per `fabric_language`); resolve session_ids; proceed to Phase 0.6. |
176
+ | `last-week` | Re-enter Step 2 with synthetic prompt `上周` / `last week`; proceed to Phase 0.6. |
177
+ | `since-last-archive` | Re-enter Step 2 with synthetic prompt `自上次归档` / `since last archive`; proceed to Phase 0.6. |
178
+ | `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.5 Step 1 with the user-typed sub-prompt. Loop max 1 time — second parse-miss falls through to `range = "all"` with a warning. |
179
+
180
+ #### Step 6 — Carry-forward contract
181
+
182
+ Phase -0.5 produces ONE of:
183
+
184
+ - `session_id[]` (non-empty array of distinct session_ids) — passed to Phase
185
+ 0.0 as the explicit scope filter; Phase 0.0 skips its own anchor-walk and
186
+ uses this list directly.
187
+ - `"all"` (sentinel string) — no range hint detected; Phase 0.0 falls back to
188
+ the legacy anchor-walk behaviour ("all distinct sessions since last
189
+ `knowledge_proposed`").
190
+
191
+ NEVER pass an empty `session_id[]` forward — that case must degrade to Step 5
192
+ fallback (or, when fallback is forbidden by invocation type, to `"all"` with
193
+ a one-line stderr warning).
194
+
195
+ #### Worked examples
196
+
197
+ **Example A — time-only: `今日复盘`**
198
+
199
+ ```
200
+ Step 1: prompt = "今日复盘"; user_invocation_type = E2.
201
+ Step 2: matches `今日` → time_window = [floor(now, day), now].
202
+ Step 3: residual "复盘" survives stop-word filter → topic_keywords = ["复盘"].
203
+ (Edge case: the residual content word may also filter; if 复盘 is
204
+ in the stop list it becomes []. Treat as topic-keyword empty.)
205
+ Step 4: tail-scan events.jsonl; keep sessions whose [ts_min, ts_max]
206
+ intersects today's window. Say 3 sessions match.
207
+ Step 5: skipped (resolution succeeded).
208
+ Step 6: emit session_id[] = ["sess-a", "sess-b", "sess-c"] → Phase 0.6.
209
+ ```
210
+
211
+ **Example B — keyword-only: `rc.20 的归档下`**
212
+
213
+ ```
214
+ Step 1: prompt = "rc.20 的归档下"; user_invocation_type = E2.
215
+ Step 2: no time pattern matches → time_window = null.
216
+ Step 3: strip "归档"/"下"/"的" stop-words → topic_keywords = ["rc.20"].
217
+ Step 4: tail-scan events.jsonl; for each session_id, Read its digest;
218
+ keep those whose digest body matches /rc\.20/i. Say 2 sessions
219
+ match (one was the rc.20 grilling session, one had a tangential
220
+ mention).
221
+ Step 5: skipped.
222
+ Step 6: emit session_id[] = ["sess-x", "sess-y"] → Phase 0.6.
223
+ ```
224
+
225
+ **Example C — combined: `上周 rc.20`**
226
+
227
+ ```
228
+ Step 1: prompt = "上周 rc.20"; user_invocation_type = E4.
229
+ Step 2: matches `上周` → time_window = [now - 7d, now].
230
+ Step 3: strip "上周" → topic_keywords = ["rc.20"].
231
+ Step 4: AND filter — keep sessions whose [ts_min, ts_max] intersects last
232
+ week AND whose digest matches /rc\.20/i. Say 1 session matches.
233
+ Step 5: skipped.
234
+ Step 6: emit session_id[] = ["sess-z"] → Phase 0.6.
235
+ ```
236
+
237
+ If Example C had resolved to zero sessions (e.g. user types `上周 rc.99`),
238
+ Step 4 would degrade into Step 5 — surfacing AskUserQuestion since E4 permits
239
+ prompting.
33
240
 
34
241
  ### Phase 0.6 — Config Load
35
242
 
@@ -48,7 +255,7 @@ If `.fabric/fabric-config.json` is missing or unreadable, use defaults silently.
48
255
  ### UX i18n Policy (5-class bilingualization)
49
256
 
50
257
  The skill consults `fabric_language` from `.fabric/fabric-config.json`
51
- (固化于 init 时,via `scan.ts:detectExistingLanguage`; default `"en"` when no
258
+ (固化于 init 时,via `lib/detect-language.ts:detectExistingLanguage`; default `"en"` when no
52
259
  CJK signal is detected in README + docs/; may resolve to `"match-existing"`,
53
260
  `"zh-CN"`, `"en"`, or `"zh-CN-hybrid"`). All user-facing text in the
54
261
  following 5 categories MUST be rendered in the resolved language:
@@ -81,7 +288,8 @@ Rendering rule:
81
288
 
82
289
  Protected tokens (`fab_extract_knowledge`, `relevance_scope`,
83
290
  `relevance_paths`, `narrow`, `broad`, `source_sessions`, `proposed_reason`,
84
- `session_context`, `pending_path`, `layer`, `team`, `personal`,
291
+ `session_context`, `intent_clues`, `tech_stack`, `impact`, `must_read_if`,
292
+ `pending_path`, `layer`, `team`, `personal`,
85
293
  `knowledge_scope_degraded`, `MUST`, `NEVER`, `.fabric/knowledge/`, the verbatim
86
294
  `强 team` / `强 personal` / `默认 team` heuristic block, etc.) are NEVER
87
295
  translated — they appear verbatim in both language variants. The
@@ -143,6 +351,94 @@ messages + edit_paths + 1-line title), so this phase is a tail-scan + read.
143
351
  can produce a session_id without a digest). Cap the loaded digest set at
144
352
  `archive_digest_max_sessions` most-recent sessions (config-resolved, default
145
353
  10) to bound LLM context (~50KB worst-case at default).
354
+ 4.5. **Filter via session_archive_attempted ledger (rc.25 TASK-05).** Before
355
+ step 5 builds the cross-session context, drop sessions that the outcome
356
+ ledger says we should not re-scan. For each `session_id` collected in
357
+ steps 1-3, scan `.fabric/events.jsonl` for events where
358
+ `event_type === "session_archive_attempted"` AND `session_id` matches,
359
+ keep the most-recent one by `ts`, and apply this state machine:
360
+
361
+ - **(a) Look up the most recent `session_archive_attempted`** event for
362
+ this `session_id` (none found → fall through to (e)).
363
+ - **(b) `outcome === "user_dismissed"` → drop (permanent skip).** The
364
+ user explicitly rejected this session's candidates; never auto-re-scan
365
+ it. Respect the dismissal forever — re-scanning would re-propose the
366
+ same content the user already declined.
367
+ - **(c) `(nowMs - attempted_event.ts) < ANTI_LOOP_HOURS * 3_600_000` →
368
+ drop (cooldown skip).** Anti-loop window: even if outcome is otherwise
369
+ re-scannable, never re-scan a session within 12 hours of the last
370
+ attempt. Aligns 心智 with the Stop-hook cooldown so a single user does
371
+ not see the same session repeatedly within one work day.
372
+ - **(d) `covered_through_ts` present → check for high-value signal in
373
+ `ts > covered_through_ts` events for this `session_id`.** Tail-scan
374
+ `events.jsonl` for events newer than the watermark whose
375
+ `session_id` matches. A session passes this gate iff at least ONE of:
376
+ - ≥1 event with `event_type ∈ HIGH_VALUE_EVENT_TYPES`
377
+ (`knowledge_context_planned`, `edit_paths_recorded`), OR
378
+ - the latest `assistant_turn_observed` event body contains ≥1 of
379
+ `NORMATIVE_KEYWORDS` (substring match, case-insensitive for
380
+ English entries).
381
+
382
+ No high-value signal → drop (no new content worth re-scanning, even
383
+ though the cooldown has expired). Has signal → keep for re-scan.
384
+ - **(e) Never attempted (no `session_archive_attempted` event found for
385
+ this `session_id`) → keep.** First-time scan; nothing to filter
386
+ against.
387
+ - **(f) Cross-session pending dedupe** (operates on candidate
388
+ observations, not on `session_id` filter): gather all
389
+ `knowledge_proposed_ids` from `session_archive_attempted` events with
390
+ `outcome === "proposed"` across ALL sessions in the recent window
391
+ (NOT just the current candidate session). This builds a global set of
392
+ idempotency keys already proposed by prior archive runs but not yet
393
+ reviewed by the user (`.fabric/knowledge/pending/` may still contain
394
+ them). When classifying new observations in Phase 1, drop any
395
+ candidate whose computed `idempotency_key` matches an id already in
396
+ this set — it was already proposed by an earlier archive run, the
397
+ user just hasn't reviewed it yet, so re-proposing would duplicate
398
+ pending entries and inflate `candidates_proposed` counts. Per Phase
399
+ 2.5 line 1112 — this is the dedupe consumer of `knowledge_proposed_ids`.
400
+
401
+ The resulting filtered `session_id[]` proceeds into step 5's digest
402
+ concatenation. Sessions filtered out in this step do NOT contribute to
403
+ `### Cross-session digest`, are NOT included in `source_sessions` on any
404
+ fab_extract_knowledge call, and are NOT referenced in `session_context`
405
+ bodies.
406
+
407
+ **Constants (rc.25 — verbatim):**
408
+
409
+ - `ANTI_LOOP_HOURS = 12` — cooldown window in hours between consecutive
410
+ re-scans of the same `session_id`. Rationale: 心智对齐 hook cooldown
411
+ (`stop_hook_cooldown_hours = 12`); identical mental model avoids user
412
+ confusion when a session shows up in both hook reminders and
413
+ archive re-scan candidates.
414
+ - `HIGH_VALUE_EVENT_TYPES = ['knowledge_context_planned', 'edit_paths_recorded']`
415
+ — event types that count as "new substantive activity worth
416
+ re-scanning" past `covered_through_ts`. Chat accumulation
417
+ (`assistant_turn_observed` alone) does NOT count — it would let mere
418
+ conversation noise trigger re-scans.
419
+ - `NORMATIVE_KEYWORDS = ['以后','always','never','from now on','下次','记一下','永远不要']`
420
+ — substring patterns scanned against the latest
421
+ `assistant_turn_observed` body for the session. Mixed CN/EN to cover
422
+ bilingual users. If any keyword hits, the session is flagged as
423
+ having high-value chat-only signal even without code edits.
424
+
425
+ **Worked examples:**
426
+
427
+ - **Session X (user_dismissed)** — last `session_archive_attempted` ts
428
+ = 3 days ago, outcome = `user_dismissed`. Rule (b) fires → permanent
429
+ skip. Session X is dropped even if 50 new `knowledge_context_planned`
430
+ events have accumulated since.
431
+ - **Session Y (proposed 6h ago)** — last `session_archive_attempted`
432
+ ts = 6h ago, outcome = `proposed`. Rule (c) fires: 6h < 12h cooldown
433
+ window → drop (cooldown skip). Y becomes eligible again after the
434
+ 12h window closes, provided high-value signal accumulates by then.
435
+ - **Session Z (viability_failed 14h ago + 3 new plan_context)** — last
436
+ `session_archive_attempted` ts = 14h ago, outcome = `viability_failed`,
437
+ `covered_through_ts` = T₀. Rules (b)(c) pass. Rule (d) tail-scans for
438
+ `session_id === Z AND ts > T₀`: finds 3 `knowledge_context_planned`
439
+ events. HIGH_VALUE_EVENT_TYPES match → keep Z for re-scan. The
440
+ previous viability failure does not block a re-scan once new
441
+ substantive activity has accumulated.
146
442
  5. **Build cross-session context.** Concatenate the loaded digests into a
147
443
  single `### Cross-session digest` block to carry into Phase 0.5 + Phase 1.
148
444
  Use this block to:
@@ -156,7 +452,225 @@ messages + edit_paths + 1-line title), so this phase is a tail-scan + read.
156
452
  Graceful degradation: if `.fabric/.cache/session-digests/` is missing
157
453
  entirely, this phase reports an empty context and Phase 0 falls back to the
158
454
  single-session behaviour. Tests that synthesize events.jsonl without
159
- populating the digest cache continue to work.
455
+ populating the digest cache continue to work. If `session_archive_attempted`
456
+ events are missing entirely (pre-rc.25 ledger or rotation has trimmed older
457
+ events), treat all sessions as never-attempted (current default behavior) —
458
+ step 4.5 rule (e) applies uniformly, so the filter degrades to the legacy
459
+ "scan everything since anchor" semantics without raising errors.
460
+
461
+ ### Phase 0.4 — First-run Onboard Phase (rc.23 F8c)
462
+
463
+ #### Phase 0.4 Trigger Gate (rc.25 — entry-context aware)
464
+
465
+ Before running ANY of the onboard coverage steps below, evaluate the
466
+ **entry-context gate**. Onboard slot collection is an interactive,
467
+ one-time project-tone capture flow that REQUIRES live user dialogue.
468
+ Non-user-active entries (hook / AI self-trigger / cron) either interrupt
469
+ the user mid-work or run unattended where dialogue is impossible, so
470
+ they MUST skip Phase 0.4 entirely and fall through to Phase 0.
471
+
472
+ Read `context.entry_point` — already determined in **Phase -0.5 Range
473
+ Resolution** (see TASK-04 / Phase -0.5 section above). The 5-entry model
474
+ is the canonical taxonomy for this gate.
475
+
476
+ ##### Entry-context detection rules
477
+
478
+ | Entry | Symbol | Detection rule (LLM-native, evaluated at skill entry) |
479
+ |-------|--------|-------------------------------------------------------|
480
+ | **E1** | `hook_passive` | stdout JSON `{decision:'block', ...}` from `archive-hint.cjs` detected at skill entry (the Stop-hook reminder path). |
481
+ | **E2** | `explicit_user_invoke` | User prompt is a direct invocation: `fabric archive` / `/fabric-archive` / `archive what we just did` / `归档一下` / similar imperative. |
482
+ | **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`; `<X>` is one of the 4 self-trigger signals from AGENTS.md E3 section: `Normative` / `Wrong-turn-and-revert` / `Decision confirmation` / `Explicit dismissal`). |
483
+ | **E4** | `user_range_rollback` | Prompt contains a **range hint** (parsed in Phase -0.5 — e.g. `今日` / `上周` / `rc.20`) AND the user is invoking. Sub-mode of E2. |
484
+ | **E5** | `cron` | Prompt contains literal `今日复盘` / `daily recap` / `daily-archive` AND no human is present (running under `/loop`, OS cron, or scheduled trigger). |
485
+
486
+ ##### Gate decision
487
+
488
+ ```
489
+ IF context.entry_point ∈ {E2_explicit_user_invoke, E4_user_range_rollback}:
490
+ → gate = PROCEED # user is live, dialogue is possible
491
+ → continue to Step 1 (Check coverage) below
492
+ ELSE (E1_hook_passive | E3_ai_self_trigger | E5_cron):
493
+ → gate = SKIP # no live user, onboard prompting would misfire
494
+ → emit one-line log: "Phase 0.4 skipped (entry=<E1|E3|E5>, no live user)"
495
+ → proceed directly to Phase 0
496
+ ```
497
+
498
+ ##### Rationale
499
+
500
+ Onboard slot collection is a one-time project-tone capture flow that
501
+ requires user dialogue. Non-user-active entries (hook / AI / cron)
502
+ interrupt the user mid-work or run unattended where dialogue is
503
+ impossible, so they MUST skip Phase 0.4. The S5 slot semantics
504
+ (`tech-stack-decision`, `architecture-pattern`, ...) are user-validated
505
+ baselines — populating them from a hook fire-and-forget or a cron daily
506
+ recap would defeat the purpose of capturing _user-confirmed_ project
507
+ tone.
508
+
509
+ ##### Tradeoff (documented in CHANGELOG)
510
+
511
+ A first-time user whose ONLY invocations ever come via hook (never an
512
+ explicit `/fabric-archive`) will not see the onboard prompt; the 5
513
+ onboard slots remain empty. Mitigation: documentation tells users to
514
+ run an explicit `fab archive` at least once to populate the onboard
515
+ baseline.
516
+
517
+ ##### Worked example
518
+
519
+ ```
520
+ $ /loop 24h /fabric-archive 今日复盘
521
+ → cron context, no live user
522
+ → Phase -0.5 detects literal "今日复盘" + no-human marker
523
+ → context.entry_point = E5_cron
524
+ → Phase 0.4 Trigger Gate evaluates: E5 ∉ {E2, E4} → SKIP
525
+ → emit log "Phase 0.4 skipped (entry=E5, no live user)"
526
+ → proceed directly to Phase 0 (collect candidates for daily window)
527
+ ```
528
+
529
+ Contrast with E2:
530
+
531
+ ```
532
+ $ /fabric-archive
533
+ → user typed explicit invocation
534
+ → Phase -0.5: context.entry_point = E2_explicit_user_invoke
535
+ → Phase 0.4 Trigger Gate evaluates: E2 ∈ {E2, E4} → PROCEED
536
+ → run Step 1 (Check coverage) below
537
+ ```
538
+
539
+ ---
540
+
541
+ After F8a removed the auto-`fab scan` baseline pipeline, a freshly installed
542
+ Fabric workspace ships with an EMPTY `.fabric/knowledge/` tree. Five fixed
543
+ **S5 onboard slots** capture the "project tone" baseline that the AI needs
544
+ for high-quality plan_context retrieval from day one:
545
+
546
+ - `tech-stack-decision` — primary languages / frameworks / runtime stack
547
+ - `architecture-pattern` — module layout, service boundaries, layering rules
548
+ - `code-style-tone` — naming / formatting / idiom conventions the project enforces
549
+ - `build-system-idiom` — build tool quirks, scripts, deploy pipeline shape
550
+ - `domain-vocabulary` — business / product terminology that names code entities
551
+
552
+ This phase runs ONCE per archive-skill invocation, BEFORE Phase 0 evidence
553
+ gathering, so coverage state is fresh for the session.
554
+
555
+ #### Step 1 — Check coverage
556
+
557
+ Invoke `fab onboard-coverage --json` and parse the JSON payload:
558
+
559
+ ```bash
560
+ fab onboard-coverage --json
561
+ ```
562
+
563
+ Expected shape:
564
+
565
+ ```json
566
+ {
567
+ "filled": { "tech-stack-decision": ["KT-DEC-0012"], ... },
568
+ "missing": ["architecture-pattern", "code-style-tone"],
569
+ "opted_out": ["domain-vocabulary"],
570
+ "total": 5
571
+ }
572
+ ```
573
+
574
+ #### Step 2 — Decide
575
+
576
+ ```
577
+ IF missing.length === 0:
578
+ → skip Phase 0.4 entirely; proceed to Phase 0.
579
+ ELSE:
580
+ → ask the user how to handle the missing slots (Step 3).
581
+ ```
582
+
583
+ #### Step 3 — Prompt user
584
+
585
+ Present a single roll-up listing each missing slot. UX i18n Policy class 5
586
+ applies: the `header` + `question` strings are translated per
587
+ `fabric_language`; the `options[]` routing keys stay English.
588
+
589
+ ```ts
590
+ AskUserQuestion({
591
+ header: "Onboard coverage", // zh-CN: "首装基调覆盖"
592
+ question:
593
+ "KB is missing the following project-tone slots: " +
594
+ missing.join(", ") +
595
+ ". Tour the project and propose pending entries for each?",
596
+ options: ["fill-all", "fill-each", "dismiss-all", "skip"]
597
+ })
598
+ ```
599
+
600
+ `fab_extract_knowledge` is called with `onboard_slot: <slot>` set so each
601
+ proposed entry counts toward coverage once approved via fab_review.
602
+
603
+ | User choice | Action |
604
+ |----------------|--------|
605
+ | `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 1). |
606
+ | `fill-each` | Loop slot-by-slot through `missing`. Per slot: ask user `confirm | dismiss | skip` (per-slot AskUserQuestion); `confirm` → run Step 4; `dismiss` → `fab config dismiss-slot <slot>`; `skip` → leave for next archive run. |
607
+ | `dismiss-all` | For EACH slot in `missing`, invoke `Bash("fab config dismiss-slot <slot>")`. Print a one-line confirmation each. Skip to Phase 0. |
608
+ | `skip` | No-op. Slots remain in `missing` for the next archive run. Skip to Phase 0. |
609
+
610
+ #### Step 4 — Tour-and-propose (per-slot)
611
+
612
+ For each slot to fill, the LLM independently sources slot-specific evidence
613
+ from the project (no user prompt — this is a Read-only tour):
614
+
615
+ | Slot | Source files (LLM should Read these) |
616
+ |--------------------------|---------------------------------------|
617
+ | `tech-stack-decision` | `package.json` (+ lockfile), `pyproject.toml` / `Cargo.toml` / `go.mod`, `tsconfig.json`, root README |
618
+ | `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`) |
619
+ | `code-style-tone` | `.editorconfig`, `prettier.config.*`, `eslint.config.*`, `biome.*`, `.prettierrc*`, framework lint config, 2-3 representative source files for naming-pattern inference |
620
+ | `build-system-idiom` | `package.json` `scripts` block, `Makefile`, `taskfile.yaml`, CI yml (`.github/workflows/*.yml`), Dockerfile if present |
621
+ | `domain-vocabulary` | README, `docs/*.md`, top-level `src/` directory names (often domain-aligned), public API entry types |
622
+
623
+ After Read-ing the slot-specific sources, classify the observation:
624
+
625
+ - `tech-stack-decision` → type=`decisions`, `proposed_reason=decision-confirmation`
626
+ - `architecture-pattern` → type=`models`, `proposed_reason=new-dependency-or-pattern`
627
+ - `code-style-tone` → type=`guidelines`, `proposed_reason=explicit-user-mark` (the project ITSELF is the mark)
628
+ - `build-system-idiom` → type=`processes`, `proposed_reason=new-dependency-or-pattern`
629
+ - `domain-vocabulary` → type=`models`, `proposed_reason=new-dependency-or-pattern`
630
+
631
+ Call `fab_extract_knowledge` with the inferred fields PLUS `onboard_slot:
632
+ <slot>`. The pending file's frontmatter will carry the slot label, and the
633
+ next `fab onboard-coverage` run will see the slot as filled (once approved
634
+ via fab_review).
635
+
636
+ Example:
637
+
638
+ ```ts
639
+ mcp__fabric__fab_extract_knowledge({
640
+ source_sessions: ["<current-session-id>"],
641
+ recent_paths: ["package.json", "tsconfig.json"],
642
+ user_messages_summary: "Project uses TypeScript + pnpm workspace + Vitest. Node 20 LTS target. ESM-only.",
643
+ type: "decisions",
644
+ slug: "primary-tech-stack",
645
+ layer: "team",
646
+ relevance_scope: "broad", // tech stack applies everywhere
647
+ relevance_paths: [],
648
+ proposed_reason: "decision-confirmation",
649
+ session_context:
650
+ "Session goal: capture onboard tech-stack baseline.\nTurning point: read package.json + tsconfig.json + pnpm-workspace.yaml; stack confirmed.",
651
+ onboard_slot: "tech-stack-decision", // ← claims the slot
652
+ tech_stack: ["typescript", "nodejs", "pnpm", "vitest"]
653
+ })
654
+ ```
655
+
656
+ #### Onboard phase constraints (DO NOT TRANSLATE)
657
+
658
+ - MUST run BEFORE Phase 0 evidence gathering — onboard is a separate flow,
659
+ not interleaved with session-archive candidates.
660
+ - MUST call `fab onboard-coverage --json` before deciding; never assume
661
+ coverage state.
662
+ - NEVER fill a slot that is in `opted_out` — `fab onboard-coverage` already
663
+ excludes those from `missing`, but the Skill MUST NOT re-propose them
664
+ even if the user asks "fill all of them" — the dismiss is intentional.
665
+ - NEVER prompt the user when `missing.length === 0` — silent skip.
666
+ - NEVER set `onboard_slot` on a regular session-archive candidate in
667
+ Phase 2 — that field is RESERVED for the onboard phase. Mixing the
668
+ two would let session-archive proposals masquerade as onboard
669
+ coverage and let any random pending file claim a slot.
670
+ - MUST emit `onboard_slot: <slot>` verbatim — the slot name is one of
671
+ the locked S5 strings (tech-stack-decision / architecture-pattern /
672
+ code-style-tone / build-system-idiom / domain-vocabulary). The
673
+ fab_extract_knowledge schema enum will reject anything else.
160
674
 
161
675
  ### Phase 0 — Collect Candidates
162
676
 
@@ -214,7 +728,27 @@ ELSE:
214
728
 
215
729
  #### On gate FAIL
216
730
 
217
- Stop the skill with the gate-FAIL message (UX i18n Policy class 2 — errors/preconditions; render per `fabric_language`):
731
+ Branching by `entry_point` (resolved at Phase -0.5):
732
+
733
+ ```
734
+ IF entry_point ∈ {E1_hook, E3_ai_self_trigger, E5_cron}:
735
+ → SILENT-SKIP path: do NOT emit the gate-FAIL message; do NOT trigger AskUserQuestion.
736
+ → Still write ONE `session_archive_attempted` event per session in scope
737
+ with outcome='skipped_no_signal' (see Phase 2.5 for the emission contract).
738
+ → Exit the skill silently. Rationale: hook / AI self-trigger / cron are
739
+ non-user-active contexts — a verbose message there is pure noise.
740
+ ELSE (entry_point ∈ {E2_explicit, E4_user_range}):
741
+ → User-active path: render the gate-FAIL message below (UX i18n Policy
742
+ class 2 — errors/preconditions; render per `fabric_language`).
743
+ → Still write ONE `session_archive_attempted` event per session in scope
744
+ with outcome='viability_failed' (see Phase 2.5 Outcome Decision Matrix
745
+ row 2 — user-active gate failure populates `viability_failed`, NOT
746
+ `skipped_no_signal` which is reserved for the SILENT-SKIP branch
747
+ above).
748
+ → Exit the skill.
749
+ ```
750
+
751
+ For the user-active branch (E2 / E4), the gate-FAIL message variants are:
218
752
 
219
753
  zh-CN variant:
220
754
 
@@ -228,7 +762,7 @@ en variant:
228
762
  Current session is routine execution; no new knowledge to archive (gate=<reason>). To force-archive, explicitly invoke fabric-archive.
229
763
  ```
230
764
 
231
- Optionally append a one-line event to `.fabric/events.jsonl` of shape `{"ts":"...","kind":"knowledge_archive_aborted","reason":"<reason>","session":"<id>"}` if the events ledger is writable; otherwise just log to stderr. Do NOT proceed to Phase 1, do NOT call any MCP tool.
765
+ In BOTH branches: do NOT proceed to Phase 1, do NOT call any MCP tool. The legacy `knowledge_archive_aborted` event line (`{"ts":"...","kind":"knowledge_archive_aborted","reason":"<reason>","session":"<id>"}`) MAY be appended in addition to the mandatory Phase 2.5 `session_archive_attempted` event they serve different audit purposes (legacy abort reason vs new outcome state machine) and the two coexist during the rc.25 transition window.
232
766
 
233
767
  ##### events.jsonl Constraint Note
234
768
 
@@ -490,10 +1024,30 @@ mcp__fabric__fab_extract_knowledge({
490
1024
  | "new-dependency-or-pattern" // new dep/lib/abstraction introduced
491
1025
  | "dismissal-with-reason", // user rejected approach AND said why
492
1026
  session_context: "<3-5 line markdown: session goal + key turning point>",
1027
+ // v2.0.0-rc.23 TASK-006 (a-C1): four OPTIONAL structured triage fields.
1028
+ // Lift implicit signals out of `## Session context` prose so future-self
1029
+ // reviewers / plan-context retrievers can triage relevance from
1030
+ // frontmatter alone, without re-reading the body. Omit any field the
1031
+ // skill cannot infer cleanly — guessing is worse than omitting.
1032
+ intent_clues: ["<short trigger>", "<negative trigger e.g. 'NOT for X'>"], // when this rule applies / when NOT
1033
+ tech_stack: ["<lang/framework>", "..."], // inferred from recent_paths (see table below)
1034
+ impact: ["<consequence of ignoring>"], // why future-self should care
1035
+ must_read_if: "<one-line strong trigger>" // single condition; if it holds, the entry is required reading
493
1036
  // tags? — NOT in current schema; reserved for future
494
1037
  })
495
1038
  ```
496
1039
 
1040
+ ##### C1 triage-field inference table
1041
+
1042
+ | Field | Inference source | Skip when |
1043
+ |----------------|----------------------------------------------------------------------------------|------------------------------------|
1044
+ | `intent_clues` | Pull from `session_context` turning point + negative phrasing in the transcript ("not for", "don't do X when") | No clear trigger phrasing surfaced |
1045
+ | `tech_stack` | Map `recent_paths` extensions: `.ts`→`typescript`, `.tsx`→`typescript`+`react`, `.go`→`go`, `package.json`→`nodejs`, `pyproject.toml`→`python`, `Cargo.toml`→`rust`. Add framework markers from path heuristics (`cocos`→`cocos-creator`, `next.config`→`nextjs`) | Rule is stack-agnostic |
1046
+ | `impact` | Pull from the diagnostic-loop body — "wasted 30 min", "production outage", "silent data loss" | No observable consequence stated |
1047
+ | `must_read_if` | Strongest single trigger from the worth-archive signal: a file path, a routine, a recurring condition; ≤160 chars | No single dominant trigger fits |
1048
+
1049
+ All four fields are STRICTLY OPTIONAL. The schema accepts the call without any of them — omit rather than guess. None of the four participate in the idempotency_key hash (server formula at `extract-knowledge.ts:100-106` is frozen to `{source_session, type, slug}`), so partial-vs-full fill of these fields on the same triple is safe.
1050
+
497
1051
  The Skill infers `proposed_reason` from the classification + viability-gate
498
1052
  signal that fired:
499
1053
 
@@ -537,6 +1091,85 @@ If the skill needs to record a genuinely separate observation in the same sessio
537
1091
  - Same first session but different tail sessions → evidence-merge into the SAME pending file; tail `session_id`s are NOT recorded as independent evidence keys.
538
1092
  - The formula is intentionally stable across the rc.5 → rc.7 migration; adding or removing tail entries does NOT change the idempotency key, preserving rc.5 single-session compat.
539
1093
 
1094
+ ### Phase 2.5 — Persist Archive Attempt
1095
+
1096
+ MANDATORY closing step on every skill invocation — runs AFTER Phase 2 (success path) AND on every early-exit path (Phase 0.0 dropped-all, Phase 0.5 gate-FAIL silent-skip or user-active, Phase 1 batch user-dismissed). Drives the Q3.4 outcome state machine + cross-session digest rescan filter.
1097
+
1098
+ #### What to emit
1099
+
1100
+ For EACH `session_id` in the run's scope (multi-session E4 runs emit MULTIPLE events — one per session_id; single-session E1/E2/E3/E5 runs emit ONE event), append ONE `session_archive_attempted` line to `.fabric/events.jsonl`:
1101
+
1102
+ ```jsonc
1103
+ {
1104
+ "kind": "fabric-event",
1105
+ "id": "<uuid or ts-derived>",
1106
+ "ts": <epoch ms>,
1107
+ "schema_version": 1,
1108
+ "session_id": "<the session this event pertains to>",
1109
+ "event_type": "session_archive_attempted",
1110
+ "outcome": "proposed" | "viability_failed" | "user_dismissed" | "skipped_no_signal",
1111
+ "covered_through_ts": <max event ts scanned for this session>,
1112
+ "candidates_proposed": <integer, default 0>,
1113
+ "knowledge_proposed_ids": ["<idempotency_key_1>", "..."] // default []
1114
+ }
1115
+ ```
1116
+
1117
+ #### Outcome decision matrix
1118
+
1119
+ | Skill terminal state | outcome | candidates_proposed | knowledge_proposed_ids |
1120
+ |----------------------------------------------------------------------|----------------------|---------------------|-------------------------------------------------|
1121
+ | Phase 2 wrote ≥ 1 pending entry | `proposed` | N (count written) | `[idempotency_key_1, idempotency_key_2, ...]` (from each fab_extract_knowledge response) |
1122
+ | Phase 0.5 viability_failed AND entry_point ∈ {E2_explicit, E4_user_range} AND user saw + accepted the gate-FAIL message | `viability_failed` | 0 | `[]` |
1123
+ | Phase 1 batch review — user dismissed ALL presented candidates | `user_dismissed` | 0 | `[]` |
1124
+ | Phase 0.0 filter dropped every session in scope OR Phase 0.5 silent-skip path (E1_hook / E3_ai_self_trigger / E5_cron) | `skipped_no_signal` | 0 | `[]` |
1125
+
1126
+ Rationale highlights:
1127
+ - `user_dismissed` is the ONLY outcome that suppresses future auto-rescan (respects user decision per Q3.4).
1128
+ - `proposed` populates `knowledge_proposed_ids` so the cross-session digest in Phase 0.0 can dedupe future runs against already-proposed entries.
1129
+ - `viability_failed` vs `skipped_no_signal` distinguishes "user was prompted but the gate stopped us" from "we never bothered the user" — both allow rescan but the doctor history report differentiates them.
1130
+
1131
+ #### covered_through_ts watermark
1132
+
1133
+ ```
1134
+ covered_through_ts = max(events_in_scope[*].ts)
1135
+ ```
1136
+
1137
+ where `events_in_scope` is the set of events the skill actually examined for THAT session_id (Phase 0 + Phase 0.0 digest input). On rescan, Phase 0.0 compares the current `max(ts)` against this stored watermark — only sessions with new events past the watermark are eligible candidates.
1138
+
1139
+ #### Multi-session emission rule
1140
+
1141
+ When the run scope spans multiple session_ids (E4 user-range with `--since` / topic-keyword matching multiple sessions), emit ONE `session_archive_attempted` event PER session_id. Each event's `covered_through_ts` is computed against that session's own event subset. The `knowledge_proposed_ids` for a multi-session `proposed` run lists ALL idempotency_keys produced by the run; ledger consumers that want per-session breakdown should join against `source_sessions` on each pending entry.
1142
+
1143
+ #### Append pattern (Bash echo, 4KB-safe, fail-tolerant)
1144
+
1145
+ Reuse the Phase 0.5 `events.jsonl Constraint Note` pattern: single-line JSON ≤ 4KB, no embedded newlines. Best-effort write — if the append fails (disk full, permission denied, race), the skill MUST still exit successfully. Log the failure to stderr only; do NOT surface it to the user. Rationale: a missing `session_archive_attempted` event degrades gracefully — the next Phase 0.0 digest treats the session as "never archived" and re-evaluates it, which is the safe-default behavior.
1146
+
1147
+ ```bash
1148
+ # Pseudo — actual implementation uses the same pattern as the legacy
1149
+ # knowledge_archive_aborted emit at the end of Phase 0.5.
1150
+ echo '{"kind":"fabric-event","id":"...","ts":..., "schema_version":1, "session_id":"...", "event_type":"session_archive_attempted","outcome":"...","covered_through_ts":...,"candidates_proposed":0,"knowledge_proposed_ids":[]}' >> .fabric/events.jsonl
1151
+ ```
1152
+
1153
+ The per-field caps from Phase 0.5's constraint note carry over: `knowledge_proposed_ids` capped at 20 entries (drop tail with `...` marker in `id` field if truncated); other fields are bounded by schema.
1154
+
1155
+ #### Worked example: E5 cron silent-skip
1156
+
1157
+ Setup: An OS cron job runs `fabric-archive` at 03:00 daily for the "today" range (E5 entry_point). Today's session was routine config edits — no archive signals fire.
1158
+
1159
+ Trace:
1160
+ 1. Phase -0.5 resolves `entry_point=E5_cron`, range = "today" → 1 session_id in scope.
1161
+ 2. Phase 0.0 digest collects events for that session_id; nothing dropped.
1162
+ 3. Phase 0.4 onboard is skipped (E5 is not E2).
1163
+ 4. Phase 0.5 viability gate runs — `archive_signals_hit=0` → `gate=FAIL (reason=no_signal)`.
1164
+ 5. `entry_point=E5_cron` ∈ {E1, E3, E5} → SILENT-SKIP branch. No message rendered.
1165
+ 6. Phase 2.5 (mandatory) appends ONE event:
1166
+ ```
1167
+ {"kind":"fabric-event","id":"...","ts":<now>,"schema_version":1,"session_id":"<today-session-id>","event_type":"session_archive_attempted","outcome":"skipped_no_signal","covered_through_ts":<max ts of today's events>,"candidates_proposed":0,"knowledge_proposed_ids":[]}
1168
+ ```
1169
+ 7. Skill exits silently. Cron output is empty.
1170
+
1171
+ Next day's cron rescan: Phase 0.0 sees `covered_through_ts < max(ts of session's new events)` → session is rescan-eligible → loop continues without `user_dismissed` block.
1172
+
540
1173
  ## Hard Rules (DO NOT TRANSLATE) — DISPLAY / WRITE Split
541
1174
 
542
1175
  ### DISPLAY Rules
@@ -563,7 +1196,7 @@ If the skill needs to record a genuinely separate observation in the same sessio
563
1196
  - NEVER use multi-signal sources for relevance_paths in rc.5 — `edit_paths` is the SOLE source. `read_paths`, body regex, and symbol extraction are reserved for rc.7+.
564
1197
  - NEVER batch multiple candidates into a single fab_extract_knowledge call; one call per candidate.
565
1198
  - NEVER paraphrase the verbatim layer heuristic block above — the Chinese text is contract-locked.
566
- - MUST preserve protected tokens exactly: `stable_id`, `knowledge_proposed`, `knowledge_archive_aborted`, `knowledge_scope_degraded`, `.fabric/knowledge/pending/`, `fab_extract_knowledge`, `relevance_paths`, `relevance_scope`, `narrow`, `broad`, `edit_paths`, `source_sessions`, `proposed_reason`, `session_context`, `pending_path`, `layer`, `team`, `personal`, `MUST`, `NEVER`, `强 team`, `强 personal`, `默认 team`.
1199
+ - MUST preserve protected tokens exactly: `stable_id`, `knowledge_proposed`, `knowledge_archive_aborted`, `knowledge_scope_degraded`, `.fabric/knowledge/pending/`, `fab_extract_knowledge`, `relevance_paths`, `relevance_scope`, `narrow`, `broad`, `edit_paths`, `source_sessions`, `proposed_reason`, `session_context`, `intent_clues`, `tech_stack`, `impact`, `must_read_if`, `pending_path`, `layer`, `team`, `personal`, `MUST`, `NEVER`, `强 team`, `强 personal`, `默认 team`.
567
1200
 
568
1201
  ## Worked Examples
569
1202
 
@@ -638,3 +1271,59 @@ mcp__fabric__fab_extract_knowledge({
638
1271
  ```
639
1272
 
640
1273
  Layer = personal (跨项目通用 + 工具/编辑器偏好 signals dominate; no 强 team signal applies). Scope = broad with `relevance_paths=[]` (personal layer ALWAYS forces broad — paths don't generalize across projects per Phase 1.5 special case).
1274
+
1275
+ ## E5 周期触发 (Scheduled Daily Recap)
1276
+
1277
+ ### Overview
1278
+
1279
+ `今日复盘` = E5 entry point. Default scope = today. Falls back to historical scan if today yields no candidates (silent-skip per Phase 2.5).
1280
+
1281
+ E5 是 5 入口模型中唯一由 OS 调度器或 Claude Code `/loop` 周期触发的入口形态。fab 端**零代码**——不提供 `fab schedule` 子命令,亦不内嵌 daemon。用户基于自己的执行环境二选一接入: `/loop`(Claude Code 原生,推荐) 或 OS cron(跨平台 fallback)。
1282
+
1283
+ ### /loop sample (primary path for Claude Code)
1284
+
1285
+ ```
1286
+ /loop /fabric-archive 今日复盘 --cron "0 23 * * *"
1287
+ ```
1288
+
1289
+ 每晚 23:00 在当前 Claude Code session 中触发 fabric-archive skill,scope = today。`/loop` 复用现有 Claude session 鉴权,无需独立 token。
1290
+
1291
+ ### OS cron sample (cross-platform alternative)
1292
+
1293
+ ```
1294
+ # crontab -e
1295
+ 0 23 * * * cd /path/to/project && claude code -p "/fabric-archive 今日复盘" 2>&1 >> /var/log/fabric-daily-recap.log
1296
+ ```
1297
+
1298
+ 适用于:
1299
+ - 非 Claude Code 环境(纯 server / CI 节点)
1300
+ - 希望脱离 /loop session 生命周期独立运行的场景
1301
+ - 已有 cron / launchd 调度基础设施的团队
1302
+
1303
+ macOS 用户可改用 `launchd` plist;Linux 用户直接 `crontab -e`。命令需自行确保 `claude code` CLI 已安装且鉴权可用。
1304
+
1305
+ ### E5 prompt parse contract
1306
+
1307
+ 当用户或 cron 以 `今日复盘` / `daily recap` 字面短语触发 fabric-archive 时,skill 应按以下契约处理:
1308
+
1309
+ - **Phase -0.5 Range Resolution**: 识别 `今日复盘` / `daily recap` 为 magic phrase, 直接设置 `time_window = today` (00:00 local timezone → current ts), 无需 AskUserQuestion 兜底。
1310
+ - **Phase 0.4 Onboard Coverage**: 跳过 (entry_point = E5_cron, 非 E2_explicit, 不弹 onboard 弹问)。
1311
+ - **Phase 2.5 Persist Archive Attempt**: 始终写入 `session_archive_attempted` event。当今日无 archive 信号触发 viability gate FAIL 时,走 silent-skip 路径(outcome = `skipped_no_signal`),skill 静默退出,cron 日志为空。
1312
+
1313
+ ### Trade-off table (/loop vs OS cron)
1314
+
1315
+ | 维度 | /loop | OS cron |
1316
+ |---|---|---|
1317
+ | 鉴权 | 复用 Claude session | 独立 token / login |
1318
+ | 跨平台 | Claude Code 全平台一致 | macOS launchd / Linux cron 不同 |
1319
+ | Token 成本 | 累积 (长 session) | 每 tick fresh, 无累积 |
1320
+ | 调试 | Claude UI 可见 | 日志文件 |
1321
+
1322
+ ### Failure modes
1323
+
1324
+ - **/loop session crash**: 归档暂停,用户需重启 `/loop`。无自动恢复机制——`/loop` 与 Claude Code session 生命周期绑定。
1325
+ - **OS cron**: 自带恢复(下一个 tick 重新启动),但需独立 `claude code` CLI 安装与鉴权;鉴权 token 过期时 cron job 会静默失败,需人工 `claude login` 重置。
1326
+
1327
+ ### NOT in scope
1328
+
1329
+ - fab CLI does NOT provide a cron helper command — keeps fab focused on archive/install, not scheduling infrastructure. 用户自行管理调度器是显式设计决策(参见 Q3.1 入口集 + cli-design 原则"能交互别做 flag, 不做调度器")。