@fenglimg/fabric-cli 2.2.0-rc.9 → 2.3.0-rc.1
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/README.md +2 -2
- package/dist/audit-PURSJJFH.js +734 -0
- package/dist/{chunk-YM4XATJF.js → chunk-722JU5BP.js} +2 -0
- package/dist/{chunk-QPAW6IYT.js → chunk-7V4XMLQ2.js} +3 -3
- package/dist/{chunk-7ZDXBOOU.js → chunk-ACSMNX3V.js} +44 -128
- package/dist/{chunk-PTGQAZEW.js → chunk-GGDVZCD6.js} +2 -4
- package/dist/{chunk-EOT63RDH.js → chunk-I5F5BHWI.js} +9 -0
- package/dist/chunk-PP7QVRXH.js +565 -0
- package/dist/chunk-SL77FXX7.js +54 -0
- package/dist/{chunk-3D7B2UAZ.js → chunk-VQKXTMWH.js} +44 -4
- package/dist/doctor-S6KPGS35.js +27 -0
- package/dist/index.js +91 -81
- package/dist/{info-7FKBTMVO.js → info-NJEY26H6.js} +91 -46
- package/dist/{context-7NUKXDB6.js → inspect-5YZMJPFM.js} +11 -11
- package/dist/{install-v2-I6PJ6IFT.js → install-v2-KGIDII4H.js} +163 -364
- package/dist/{plan-context-hint-G75R4P4J.js → plan-context-hint-5TNGH3R4.js} +1 -1
- package/dist/{store-HOCORVL3.js → store-GF4SFBMJ.js} +155 -57
- package/dist/{sync-DT5UJMMR.js → sync-3XCIRDPK.js} +3 -4
- package/dist/{uninstall-IFN2KYBK.js → uninstall-BG4ML4FC.js} +39 -10
- package/package.json +3 -7
- package/templates/hooks/cite-policy-evict.cjs +1 -1
- package/templates/hooks/configs/claude-code.json +1 -5
- package/templates/hooks/configs/codex-hooks.json +1 -5
- package/templates/hooks/fabric-hint.cjs +346 -138
- package/templates/hooks/knowledge-hint-broad.cjs +265 -75
- package/templates/hooks/knowledge-hint-narrow.cjs +3 -3
- package/templates/hooks/knowledge-pretooluse.cjs +111 -0
- package/templates/hooks/lib/banner-i18n.cjs +31 -12
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +1 -1
- package/templates/hooks/lib/event-writer.cjs +79 -0
- package/templates/hooks/lib/nudge-policy.cjs +11 -0
- package/templates/hooks/lib/theme.cjs +62 -0
- package/templates/hooks/post-tooluse-mutation.cjs +28 -39
- package/templates/skills/fabric-archive/SKILL.md +43 -12
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/i18n-policy.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +5 -5
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +2 -2
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-6-related-edges.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-3-classify.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +1 -1
- package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +6 -5
- package/templates/skills/{fabric-import/ref/checkpoint-state.md → fabric-archive/ref/source-checkpoint.md} +3 -3
- package/templates/skills/{fabric-import/ref/phase-3-dedup.md → fabric-archive/ref/source-dedup.md} +4 -4
- package/templates/skills/{fabric-import/ref/phase-2-mining.md → fabric-archive/ref/source-mining.md} +20 -20
- package/templates/skills/{fabric-import/ref/output-contract.md → fabric-archive/ref/source-output-contract.md} +3 -3
- package/templates/skills/{fabric-import/ref/state-recovery.md → fabric-archive/ref/source-state-recovery.md} +2 -2
- package/templates/skills/{fabric-import/ref/worked-examples.md → fabric-archive/ref/source-worked-examples.md} +10 -10
- package/templates/skills/fabric-archive/ref/worked-examples.md +3 -3
- package/templates/skills/fabric-review/SKILL.md +28 -15
- package/templates/skills/fabric-review/ref/cite-contract.md +2 -2
- package/templates/skills/fabric-review/ref/modify-flow.md +13 -1
- package/templates/skills/fabric-review/ref/per-mode-flows.md +5 -5
- package/templates/skills/fabric-review/ref/relate-mode.md +33 -0
- package/templates/skills/fabric-review/ref/retire-mode.md +47 -0
- package/templates/skills/fabric-review/ref/semantic-check.md +1 -1
- package/templates/skills/fabric-review/ref/worked-examples.md +5 -5
- package/templates/skills/fabric-store/SKILL.md +12 -27
- package/templates/skills/fabric-sync/SKILL.md +16 -35
- package/templates/skills/lib/shared-policy.md +6 -4
- package/dist/chunk-27HK6H5Y.js +0 -69
- package/dist/chunk-E7HJUU34.js +0 -1096
- package/dist/chunk-NLNH64A3.js +0 -43
- package/dist/chunk-QFIVFZRH.js +0 -13
- package/dist/doctor-MDTZWKBK.js +0 -24
- package/dist/metrics-HMFH4YHK.js +0 -135
- package/dist/scope-explain-HLJZ2M33.js +0 -48
- package/dist/status-4R3TM4FJ.js +0 -37
- package/dist/whoami-ITGEFWH4.js +0 -49
- package/templates/skills/fabric/SKILL.md +0 -100
- package/templates/skills/fabric-audit/SKILL.md +0 -63
- package/templates/skills/fabric-connect/SKILL.md +0 -48
- package/templates/skills/fabric-import/SKILL.md +0 -151
- package/templates/skills/fabric-import/ref/i18n-policy.md +0 -78
|
@@ -6,7 +6,11 @@ const { dirname, join } = require("node:path");
|
|
|
6
6
|
// ledgers (events.jsonl, metrics.jsonl). Under multi-window concurrency a bare
|
|
7
7
|
// appendFileSync can interleave a partial write; route through the advisory-lock
|
|
8
8
|
// primitive (drop-on-contention, best-effort — matches injection-log).
|
|
9
|
+
// ux-w2-9: events.jsonl writes go through the single guarded event-writer
|
|
10
|
+
// (envelope stamp + event_type guard); metrics.jsonl stays on the raw locked
|
|
11
|
+
// primitive (it is not a schema-governed event ledger).
|
|
9
12
|
const { appendLockedLine } = require("./lib/injection-log.cjs");
|
|
13
|
+
const { appendEvent } = require("./lib/event-writer.cjs");
|
|
10
14
|
|
|
11
15
|
// v2.0.0-rc.7 T5: session-digest writer. Best-effort (never blocks Stop hook
|
|
12
16
|
// on failure — see contract in lib/session-digest-writer.cjs).
|
|
@@ -248,12 +252,18 @@ const ARCHIVE_NORMATIVE_KEYWORDS = [
|
|
|
248
252
|
// (D6): a workspace that crossed the edit threshold but produced no high-value
|
|
249
253
|
// signal stays quiet. watermarkTs null (never archived) → treat all events as
|
|
250
254
|
// past-watermark (a never-archived repo with any edit signal is worth nudging).
|
|
251
|
-
|
|
255
|
+
// crack 1: optional `sessionId` scopes the probe to ONE session's events so a
|
|
256
|
+
// neighbour window's high-value work (past the same global watermark) cannot
|
|
257
|
+
// keep THIS session's archive nudge alive (or, in the backlog scan, attribute a
|
|
258
|
+
// neighbour's signal to a dead session). Omitted → workspace-wide (legacy).
|
|
259
|
+
function hasHighValueArchiveSignal(events, watermarkTs, sessionId) {
|
|
252
260
|
if (!Array.isArray(events)) return false;
|
|
253
261
|
const wm = typeof watermarkTs === "number" ? watermarkTs : 0;
|
|
262
|
+
const scoped = typeof sessionId === "string" && sessionId.length > 0;
|
|
254
263
|
let latestTurn = null;
|
|
255
264
|
for (const e of events) {
|
|
256
265
|
if (!e || typeof e.ts !== "number" || e.ts <= wm) continue;
|
|
266
|
+
if (scoped && e.session_id !== sessionId) continue;
|
|
257
267
|
if (typeof e.event_type === "string" && ARCHIVE_HIGH_VALUE_EVENT_TYPES.has(e.event_type)) {
|
|
258
268
|
return true;
|
|
259
269
|
}
|
|
@@ -505,6 +515,138 @@ function countEditsSince(projectRoot, anchorTs) {
|
|
|
505
515
|
return count;
|
|
506
516
|
}
|
|
507
517
|
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// Two-lane archive strategy (crack 1 + 2).
|
|
520
|
+
//
|
|
521
|
+
// In-session lane (crack 1): the archive nudge's edit trigger counts ONLY the
|
|
522
|
+
// current session's `file_mutated` events since the current session's OWN
|
|
523
|
+
// archive watermark — a neighbour window archiving (which moves the GLOBAL
|
|
524
|
+
// `knowledge_proposed` anchor) must never zero THIS window's unarchived work.
|
|
525
|
+
// We read the event ledger (file_mutated carries session_id, written by
|
|
526
|
+
// post-tooluse-mutation.cjs; session_archive_attempted carries
|
|
527
|
+
// covered_through_ts), NOT the session-blind `.fabric/.cache/edit-counter`
|
|
528
|
+
// sidecar — that stays for the activity-overview DISPLAY line only.
|
|
529
|
+
//
|
|
530
|
+
// Cross-session lane (crack 2): `countBacklogSessions` is the safety net that
|
|
531
|
+
// replaces the old global-24h timer (which any neighbour's archive reset, so a
|
|
532
|
+
// low-signal "dead" session was orphaned forever). It reads events.jsonl
|
|
533
|
+
// directly — never the resolved-bindings snapshot (KT-PIT-0017/0019 stale
|
|
534
|
+
// projection class).
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
|
|
537
|
+
// rc cross-session backlog constants. ANTI_LOOP mirrors archive-scan.ts.
|
|
538
|
+
const ARCHIVE_BACKLOG_ANTI_LOOP_HOURS = 12;
|
|
539
|
+
const DEFAULT_ARCHIVE_BACKLOG_SESSION_COUNT = 2;
|
|
540
|
+
const DEFAULT_ARCHIVE_BACKLOG_IDLE_HOURS = 24;
|
|
541
|
+
|
|
542
|
+
// Latest session_archive_attempted.covered_through_ts for this session, else null.
|
|
543
|
+
function sessionArchiveWatermark(events, sessionId) {
|
|
544
|
+
if (!Array.isArray(events) || typeof sessionId !== "string" || sessionId.length === 0) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
let wm = null;
|
|
548
|
+
for (const ev of events) {
|
|
549
|
+
if (!ev || ev.session_id !== sessionId) continue;
|
|
550
|
+
if (ev.event_type !== "session_archive_attempted") continue;
|
|
551
|
+
if (typeof ev.covered_through_ts !== "number") continue;
|
|
552
|
+
if (wm === null || ev.covered_through_ts > wm) wm = ev.covered_through_ts;
|
|
553
|
+
}
|
|
554
|
+
return wm;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Earliest event ts carrying this session_id, else null.
|
|
558
|
+
function sessionFirstActivityTs(events, sessionId) {
|
|
559
|
+
if (!Array.isArray(events) || typeof sessionId !== "string" || sessionId.length === 0) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
let first = null;
|
|
563
|
+
for (const ev of events) {
|
|
564
|
+
if (!ev || ev.session_id !== sessionId || typeof ev.ts !== "number") continue;
|
|
565
|
+
if (first === null || ev.ts < first) first = ev.ts;
|
|
566
|
+
}
|
|
567
|
+
return first;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Per-session archive anchor: this session's own last archive watermark, else
|
|
571
|
+
// its first activity ts. null only when the session has zero ledger presence.
|
|
572
|
+
function sessionAnchorTs(events, sessionId) {
|
|
573
|
+
const wm = sessionArchiveWatermark(events, sessionId);
|
|
574
|
+
if (wm !== null) return wm;
|
|
575
|
+
return sessionFirstActivityTs(events, sessionId);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Count this session's `file_mutated` events strictly after the anchor (anchor
|
|
579
|
+
// null → count all of the session's mutations). Replaces the session-blind
|
|
580
|
+
// countEditsSince(edit-counter) for the archive TRIGGER (crack 1).
|
|
581
|
+
function countSessionMutationsSince(events, sessionId, anchorTs) {
|
|
582
|
+
if (!Array.isArray(events) || typeof sessionId !== "string" || sessionId.length === 0) {
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
585
|
+
let count = 0;
|
|
586
|
+
for (const ev of events) {
|
|
587
|
+
if (!ev || ev.session_id !== sessionId) continue;
|
|
588
|
+
if (ev.event_type !== "file_mutated" || typeof ev.ts !== "number") continue;
|
|
589
|
+
if (anchorTs === null || ev.ts > anchorTs) count += 1;
|
|
590
|
+
}
|
|
591
|
+
return count;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Cross-session safety net (crack 2). Counts DEAD sessions (carry a
|
|
595
|
+
// `session_ended` marker OR have been idle beyond idleHours) — OTHER than the
|
|
596
|
+
// current one — that hold unarchived high-value work and are NOT
|
|
597
|
+
// `user_dismissed` / inside the 12h anti-loop cooldown. This is the per-session
|
|
598
|
+
// replacement for the global-24h archive timer: it is NOT reset by any
|
|
599
|
+
// neighbour's archive, so a low-signal session that simply ended is no longer
|
|
600
|
+
// orphaned. Mirrors archive-scan.ts's outcome-filter semantics.
|
|
601
|
+
function countBacklogSessions(events, nowMs, currentSessionId, idleHours) {
|
|
602
|
+
if (!Array.isArray(events)) return 0;
|
|
603
|
+
const idleMs =
|
|
604
|
+
(typeof idleHours === "number" && idleHours > 0 ? idleHours : DEFAULT_ARCHIVE_BACKLOG_IDLE_HOURS) *
|
|
605
|
+
MS_PER_HOUR;
|
|
606
|
+
const lastActivity = new Map(); // sid -> max ts
|
|
607
|
+
const ended = new Set(); // sid with a session_ended marker
|
|
608
|
+
const lastAttempt = new Map(); // sid -> latest session_archive_attempted event
|
|
609
|
+
const sessions = new Set();
|
|
610
|
+
for (const ev of events) {
|
|
611
|
+
if (!ev || typeof ev.session_id !== "string" || ev.session_id.length === 0) continue;
|
|
612
|
+
const sid = ev.session_id;
|
|
613
|
+
sessions.add(sid);
|
|
614
|
+
if (typeof ev.ts === "number") {
|
|
615
|
+
const prev = lastActivity.get(sid);
|
|
616
|
+
if (prev === undefined || ev.ts > prev) lastActivity.set(sid, ev.ts);
|
|
617
|
+
}
|
|
618
|
+
if (ev.event_type === "session_ended") ended.add(sid);
|
|
619
|
+
if (ev.event_type === "session_archive_attempted" && typeof ev.ts === "number") {
|
|
620
|
+
const prior = lastAttempt.get(sid);
|
|
621
|
+
if (!prior || (typeof prior.ts === "number" && ev.ts > prior.ts)) lastAttempt.set(sid, ev);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
let count = 0;
|
|
625
|
+
for (const sid of sessions) {
|
|
626
|
+
if (sid === currentSessionId) continue; // live lane handles the current session
|
|
627
|
+
const last = lastActivity.get(sid);
|
|
628
|
+
const isDead = ended.has(sid) || (typeof last === "number" && nowMs - last >= idleMs);
|
|
629
|
+
if (!isDead) continue;
|
|
630
|
+
const attempt = lastAttempt.get(sid);
|
|
631
|
+
if (attempt && attempt.outcome === "user_dismissed") continue; // respect dismissal
|
|
632
|
+
if (
|
|
633
|
+
attempt &&
|
|
634
|
+
typeof attempt.ts === "number" &&
|
|
635
|
+
nowMs - attempt.ts < ARCHIVE_BACKLOG_ANTI_LOOP_HOURS * MS_PER_HOUR
|
|
636
|
+
) {
|
|
637
|
+
continue; // inside anti-loop cooldown
|
|
638
|
+
}
|
|
639
|
+
// Probe high-value work since the session's OWN archive watermark — null
|
|
640
|
+
// (never archived) means probe the whole session (wm→0), so a high-value
|
|
641
|
+
// signal that was the session's first event still counts. Using the
|
|
642
|
+
// first-activity anchor here would wrongly exclude it (strict `> anchor`).
|
|
643
|
+
const wm = sessionArchiveWatermark(events, sid);
|
|
644
|
+
if (!hasHighValueArchiveSignal(events, wm, sid)) continue; // no unarchived high-value work
|
|
645
|
+
count += 1;
|
|
646
|
+
}
|
|
647
|
+
return count;
|
|
648
|
+
}
|
|
649
|
+
|
|
508
650
|
// ---------------------------------------------------------------------------
|
|
509
651
|
// Observability grill (a + Q4): session-activity status breadcrumb.
|
|
510
652
|
//
|
|
@@ -830,16 +972,19 @@ function readArchiveEditThreshold(projectRoot) {
|
|
|
830
972
|
* Review wins over import because pending overflow is a sharper backlog signal
|
|
831
973
|
* than a sparse corpus.
|
|
832
974
|
*
|
|
833
|
-
* The `editCounterStats` parameter is the
|
|
834
|
-
*
|
|
835
|
-
* {
|
|
836
|
-
*
|
|
837
|
-
*
|
|
975
|
+
* The `editCounterStats` parameter is the per-session edit view (crack 1)
|
|
976
|
+
* computed in main() from file_mutated events:
|
|
977
|
+
* { editsSinceArchive: number, threshold: number, anchorPresent: boolean }
|
|
978
|
+
* The `backlogStats` parameter (crack 2) is the cross-session view:
|
|
979
|
+
* { deadSessionCount: number, threshold: number }
|
|
980
|
+
* Both default to a no-trigger shape when omitted (back-compat for callers
|
|
981
|
+
* pre-dating the two-lane split).
|
|
838
982
|
*
|
|
839
|
-
* Returns one of:
|
|
840
|
-
* - { decision: '
|
|
841
|
-
* - { decision: '
|
|
842
|
-
* - { decision: '
|
|
983
|
+
* Returns one of (ux-w0-3: `decision: 'soft'` — a reminder, never a gate):
|
|
984
|
+
* - { decision: 'soft', reason, signal: 'archive', recommended_skill: 'fabric-archive' }
|
|
985
|
+
* - { decision: 'soft', reason, signal: 'archive_backlog', recommended_skill: 'fabric-archive' }
|
|
986
|
+
* - { decision: 'soft', reason, signal: 'review', recommended_skill: 'fabric-review' }
|
|
987
|
+
* - { decision: 'soft', reason, signal: 'import', recommended_skill: 'fabric-import' }
|
|
843
988
|
* - null on no trigger
|
|
844
989
|
*/
|
|
845
990
|
// rc.7 T7: thresholds is the externalized-config view passed in by main().
|
|
@@ -847,21 +992,31 @@ function readArchiveEditThreshold(projectRoot) {
|
|
|
847
992
|
// without touching the filesystem. Omitting the arg falls back to documented
|
|
848
993
|
// defaults so existing in-process callers (tests that pre-date T7) still
|
|
849
994
|
// pass without modification — they implicitly exercise the default path.
|
|
850
|
-
function decide(events, now, pendingStats, underseedStats, editCounterStats, thresholds, banner, importInFlight) {
|
|
995
|
+
function decide(events, now, pendingStats, underseedStats, editCounterStats, thresholds, banner, importInFlight, backlogStats) {
|
|
851
996
|
const nowMs = now instanceof Date ? now.getTime() : Number(now) || Date.now();
|
|
852
997
|
const stats = pendingStats || { count: 0, oldestAgeMs: null };
|
|
853
998
|
const underseed =
|
|
854
999
|
underseedStats || { nodeCount: 0, threshold: DEFAULT_UNDERSEED_NODE_THRESHOLD };
|
|
1000
|
+
// crack 1: per-session edit view. `editsSinceArchive` = current session's
|
|
1001
|
+
// file_mutated count since its own archive anchor; `anchorPresent` = the
|
|
1002
|
+
// session has any ledger activity (the trigger gate, replacing the old
|
|
1003
|
+
// "global knowledge_proposed exists" gate).
|
|
855
1004
|
const editStats =
|
|
856
1005
|
editCounterStats || {
|
|
857
|
-
|
|
1006
|
+
editsSinceArchive: 0,
|
|
858
1007
|
threshold: DEFAULT_ARCHIVE_EDIT_THRESHOLD,
|
|
1008
|
+
anchorPresent: false,
|
|
1009
|
+
};
|
|
1010
|
+
// crack 2: cross-session backlog view (dead sessions with unarchived work).
|
|
1011
|
+
const backlog =
|
|
1012
|
+
backlogStats || {
|
|
1013
|
+
deadSessionCount: 0,
|
|
1014
|
+
threshold: DEFAULT_ARCHIVE_BACKLOG_SESSION_COUNT,
|
|
859
1015
|
};
|
|
860
1016
|
const cfg = thresholds || {};
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
: DEFAULT_ARCHIVE_HINT_HOURS;
|
|
1017
|
+
// crack 2: the global archive_hint_hours timer is retired (the cross-session
|
|
1018
|
+
// case is now the archive_backlog signal). cfg.archiveHintHours is still
|
|
1019
|
+
// accepted on the thresholds bag for back-compat but no longer drives Signal A.
|
|
865
1020
|
const reviewHintPendingCount =
|
|
866
1021
|
typeof cfg.reviewHintPendingCount === "number" && cfg.reviewHintPendingCount > 0
|
|
867
1022
|
? cfg.reviewHintPendingCount
|
|
@@ -875,10 +1030,18 @@ function decide(events, now, pendingStats, underseedStats, editCounterStats, thr
|
|
|
875
1030
|
// byte-identical Chinese output. main() always supplies the resolved variant.
|
|
876
1031
|
const variant = typeof cfg.variant === "string" ? cfg.variant : "zh-CN";
|
|
877
1032
|
|
|
878
|
-
// ---- Archive signal (
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
//
|
|
1033
|
+
// ---- Archive signal (crack 1 — per-session edit count) -------------------
|
|
1034
|
+
// In-session lane: nudge when THIS session has accumulated >= threshold file
|
|
1035
|
+
// mutations since its OWN archive anchor (computed per-session in main() from
|
|
1036
|
+
// file_mutated events — `editStats.editsSinceArchive`). The old global
|
|
1037
|
+
// 24h-OR-N-edits trigger is retired: the hours branch became the
|
|
1038
|
+
// archive_backlog signal below (crack 2), and the edit count is now
|
|
1039
|
+
// session-scoped so a neighbour window's archive can't zero this window's
|
|
1040
|
+
// work. `anchorPresent` gates the trigger (a session with zero ledger
|
|
1041
|
+
// activity has nothing to count).
|
|
1042
|
+
//
|
|
1043
|
+
// `lastProposedTs` / `hoursElapsed` are still derived here for the IMPORT
|
|
1044
|
+
// signal's "no knowledge_proposed in last 24h" guard further down.
|
|
882
1045
|
let lastProposedTs = null;
|
|
883
1046
|
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
884
1047
|
const ev = events[i];
|
|
@@ -887,63 +1050,27 @@ function decide(events, now, pendingStats, underseedStats, editCounterStats, thr
|
|
|
887
1050
|
break;
|
|
888
1051
|
}
|
|
889
1052
|
}
|
|
890
|
-
|
|
891
1053
|
const hoursElapsed =
|
|
892
1054
|
lastProposedTs === null ? null : (nowMs - lastProposedTs) / MS_PER_HOUR;
|
|
893
1055
|
|
|
894
|
-
const triggerByHours =
|
|
895
|
-
hoursElapsed !== null && hoursElapsed >= archiveHintHours;
|
|
896
1056
|
const triggerByEdits =
|
|
897
|
-
|
|
898
|
-
editStats.
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
if (
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
// tests. When omitted (legacy callers / tests pre-T4) the overview
|
|
915
|
-
// line is skipped — the banner remains valid 3-or-2 lines depending
|
|
916
|
-
// on data availability.
|
|
917
|
-
//
|
|
918
|
-
// Substring contract preserved for existing tests:
|
|
919
|
-
// - "<hoursElapsed.toFixed(1)>h" (e.g. "25.0h")
|
|
920
|
-
// - "<editCount> 次编辑"
|
|
921
|
-
// - "阈值 <N>"
|
|
922
|
-
// - "fabric-archive"
|
|
923
|
-
// v2.0.0-rc.27 TASK-005 (audit §2.17): parts now assembled per-variant
|
|
924
|
-
// via banner-i18n's archivePartsHours / archivePartsEdits so en mode
|
|
925
|
-
// gets fully-English fragments instead of mixed-language output. zh-CN
|
|
926
|
-
// / zh-CN-hybrid still render the original substring contract verbatim.
|
|
927
|
-
const parts = [];
|
|
928
|
-
if (triggerByHours) {
|
|
929
|
-
parts.push(
|
|
930
|
-
renderBanner("archivePartsHours", variant, {
|
|
931
|
-
hoursFixed: hoursElapsed.toFixed(1),
|
|
932
|
-
threshold: archiveHintHours,
|
|
933
|
-
}),
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
if (triggerByEdits) {
|
|
937
|
-
parts.push(
|
|
938
|
-
renderBanner("archivePartsEdits", variant, {
|
|
939
|
-
count: editStats.editsSinceLastProposed,
|
|
940
|
-
threshold: editStats.threshold,
|
|
941
|
-
}),
|
|
942
|
-
);
|
|
943
|
-
}
|
|
944
|
-
// rc.16 TASK-002: 5-banner i18n via lib/banner-i18n.cjs. Substring
|
|
945
|
-
// contracts ('25.0h', '阈值 N', 'fabric-archive') preserved by the lib's
|
|
946
|
-
// zh-CN templates — see lib header for the full contract.
|
|
1057
|
+
editStats.anchorPresent === true &&
|
|
1058
|
+
typeof editStats.editsSinceArchive === "number" &&
|
|
1059
|
+
editStats.editsSinceArchive >= editStats.threshold;
|
|
1060
|
+
|
|
1061
|
+
// PRECEDENCE: in-session archive wins over backlog/review/import — recent
|
|
1062
|
+
// local work is the most actionable reminder.
|
|
1063
|
+
if (triggerByEdits) {
|
|
1064
|
+
// 人-first banner: edit-count fragment only (the hours fragment retired with
|
|
1065
|
+
// the global timer). Substring contracts ('次编辑', '阈值 N', 'fabric-archive')
|
|
1066
|
+
// preserved by banner-i18n's zh-CN templates. The activity overview line is
|
|
1067
|
+
// injected by main() via `banner` so decide() stays pure / filesystem-free.
|
|
1068
|
+
const parts = [
|
|
1069
|
+
renderBanner("archivePartsEdits", variant, {
|
|
1070
|
+
count: editStats.editsSinceArchive,
|
|
1071
|
+
threshold: editStats.threshold,
|
|
1072
|
+
}),
|
|
1073
|
+
];
|
|
947
1074
|
const line1 = renderBanner("archiveLine1", variant, { parts: parts.join(" / ") });
|
|
948
1075
|
const activity = banner && typeof banner.activityOverview === "string"
|
|
949
1076
|
? banner.activityOverview
|
|
@@ -954,15 +1081,35 @@ function decide(events, now, pendingStats, underseedStats, editCounterStats, thr
|
|
|
954
1081
|
const line3 = renderBanner("archiveCta", variant, {});
|
|
955
1082
|
const reason = [line1, line2, line3].filter((l) => l.length > 0).join("\n");
|
|
956
1083
|
return {
|
|
957
|
-
decision: "
|
|
1084
|
+
decision: "soft",
|
|
958
1085
|
reason,
|
|
959
1086
|
signal: "archive",
|
|
960
1087
|
recommended_skill: "fabric-archive",
|
|
961
1088
|
// v2.1 NEW-N-3: surface the firing sub-signal's numbers for the
|
|
962
|
-
// hook_signal_emitted ledger row main() writes.
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1089
|
+
// hook_signal_emitted ledger row main() writes.
|
|
1090
|
+
threshold: editStats.threshold,
|
|
1091
|
+
actual_value: editStats.editsSinceArchive,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// ---- Archive backlog signal (crack 2 — cross-session safety net) ---------
|
|
1096
|
+
// Fires when N+ DEAD sessions (session_ended / idle) carry unarchived
|
|
1097
|
+
// high-value work — the per-session replacement for the old global-24h timer
|
|
1098
|
+
// (which any neighbour's archive reset, orphaning low-signal ended sessions).
|
|
1099
|
+
// KT-DEC-0007: a soft reminder, never a gate. Ranked AFTER in-session archive
|
|
1100
|
+
// but BEFORE review/import: losing knowledge from an ended session is a
|
|
1101
|
+
// sharper signal than a review/import backlog.
|
|
1102
|
+
if (backlog.threshold > 0 && backlog.deadSessionCount >= backlog.threshold) {
|
|
1103
|
+
const line1 = renderBanner("backlogLine1", variant, { count: backlog.deadSessionCount });
|
|
1104
|
+
const line2 = renderBanner("backlogCta", variant, {});
|
|
1105
|
+
const reason = `${line1}\n${line2}`;
|
|
1106
|
+
return {
|
|
1107
|
+
decision: "soft",
|
|
1108
|
+
reason,
|
|
1109
|
+
signal: "archive_backlog",
|
|
1110
|
+
recommended_skill: "fabric-archive",
|
|
1111
|
+
threshold: backlog.threshold,
|
|
1112
|
+
actual_value: backlog.deadSessionCount,
|
|
966
1113
|
};
|
|
967
1114
|
}
|
|
968
1115
|
|
|
@@ -995,7 +1142,7 @@ function decide(events, now, pendingStats, underseedStats, editCounterStats, thr
|
|
|
995
1142
|
const line2 = renderBanner("reviewCta", variant, {});
|
|
996
1143
|
const reason = `${line1}\n${line2}`;
|
|
997
1144
|
return {
|
|
998
|
-
decision: "
|
|
1145
|
+
decision: "soft",
|
|
999
1146
|
reason,
|
|
1000
1147
|
signal: "review",
|
|
1001
1148
|
recommended_skill: "fabric-review",
|
|
@@ -1053,10 +1200,11 @@ function decide(events, now, pendingStats, underseedStats, editCounterStats, thr
|
|
|
1053
1200
|
const line2 = renderBanner("importCta", variant, {});
|
|
1054
1201
|
const reason = `${line1}\n${line2}`;
|
|
1055
1202
|
return {
|
|
1056
|
-
decision: "
|
|
1203
|
+
decision: "soft",
|
|
1057
1204
|
reason,
|
|
1058
1205
|
signal: "import",
|
|
1059
|
-
|
|
1206
|
+
// W3-C: fabric-import folded into fabric-archive `source` mode.
|
|
1207
|
+
recommended_skill: "fabric-archive",
|
|
1060
1208
|
// v2.1 NEW-N-3: underseed corpus trigger — node-count vs threshold. The
|
|
1061
1209
|
// "import" signal collapses to schema signal_type "other" in main().
|
|
1062
1210
|
threshold: underseed.threshold,
|
|
@@ -1121,6 +1269,23 @@ function readMaintenanceHintCooldownDays(projectRoot) {
|
|
|
1121
1269
|
);
|
|
1122
1270
|
}
|
|
1123
1271
|
|
|
1272
|
+
// crack 2: cross-session backlog signal thresholds.
|
|
1273
|
+
function readArchiveBacklogSessionCount(projectRoot) {
|
|
1274
|
+
return _readConfigNumber(
|
|
1275
|
+
projectRoot,
|
|
1276
|
+
"archive_backlog_session_count",
|
|
1277
|
+
DEFAULT_ARCHIVE_BACKLOG_SESSION_COUNT,
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function readArchiveBacklogIdleHours(projectRoot) {
|
|
1282
|
+
return _readConfigNumber(
|
|
1283
|
+
projectRoot,
|
|
1284
|
+
"archive_backlog_idle_hours",
|
|
1285
|
+
DEFAULT_ARCHIVE_BACKLOG_IDLE_HOURS,
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1124
1289
|
/**
|
|
1125
1290
|
* Resolve the cooldown setting from .fabric/fabric-config.json
|
|
1126
1291
|
* (archive_hint_cooldown_hours), falling back to DEFAULT_COOLDOWN_HOURS.
|
|
@@ -1231,7 +1396,7 @@ function writeShownCache(projectRoot, cache, sessionId) {
|
|
|
1231
1396
|
// precedence model — KT-DEC-0007 anti-nag spirit).
|
|
1232
1397
|
// -----------------------------------------------------------------------------
|
|
1233
1398
|
|
|
1234
|
-
const DISMISSABLE_SIGNALS = ["archive", "review", "import", "maintenance"];
|
|
1399
|
+
const DISMISSABLE_SIGNALS = ["archive", "archive_backlog", "review", "import", "maintenance"];
|
|
1235
1400
|
|
|
1236
1401
|
function sessionDismissFileName(sessionId) {
|
|
1237
1402
|
const safe = String(sessionId || "anonymous").replace(/[^A-Za-z0-9_.-]/g, "-");
|
|
@@ -1365,8 +1530,8 @@ function writeMaintenanceLastEmit(projectRoot, nowMs, sessionId) {
|
|
|
1365
1530
|
* the previous Signal-D emit. Tracked via dedicated sidecar
|
|
1366
1531
|
* `.fabric/.cache/maintenance-hint-last-emit`.
|
|
1367
1532
|
*
|
|
1368
|
-
* Returns one of:
|
|
1369
|
-
* - { decision: '
|
|
1533
|
+
* Returns one of (ux-w0-3: `decision: 'soft'` — a reminder, never a gate):
|
|
1534
|
+
* - { decision: 'soft', reason, signal: 'maintenance', recommended_skill: null }
|
|
1370
1535
|
* - null on no trigger
|
|
1371
1536
|
*
|
|
1372
1537
|
* `recommended_skill` is intentionally null — the maintenance prompt
|
|
@@ -1431,7 +1596,7 @@ function evaluateMaintenanceSignal(events, now, canonicalCount, lastEmitMs, thre
|
|
|
1431
1596
|
const reason = `${line1}\n${line2}`;
|
|
1432
1597
|
|
|
1433
1598
|
return {
|
|
1434
|
-
decision: "
|
|
1599
|
+
decision: "soft",
|
|
1435
1600
|
reason,
|
|
1436
1601
|
signal: "maintenance",
|
|
1437
1602
|
// CLI recommendation rather than Skill — doctor is a CLI surface.
|
|
@@ -1521,7 +1686,7 @@ function emitGraphEdgeCandidateBestEffort(cwd, events, sessionId) {
|
|
|
1521
1686
|
};
|
|
1522
1687
|
if (store !== undefined) event.store = store;
|
|
1523
1688
|
if (typeof sessionId === "string" && sessionId.length > 0) event.session_id = sessionId;
|
|
1524
|
-
|
|
1689
|
+
appendEvent(fabricDir, event);
|
|
1525
1690
|
|
|
1526
1691
|
// Record the de-dup marker (best-effort; atomic when state-store lib loaded).
|
|
1527
1692
|
try {
|
|
@@ -1583,7 +1748,7 @@ function emitSignalFiredEvent(cwd, sessionId, result) {
|
|
|
1583
1748
|
fired: true,
|
|
1584
1749
|
};
|
|
1585
1750
|
if (typeof sessionId === "string" && sessionId.length > 0) event.session_id = sessionId;
|
|
1586
|
-
|
|
1751
|
+
appendEvent(fabricDir, event);
|
|
1587
1752
|
} catch {
|
|
1588
1753
|
// best-effort telemetry — never block the hook
|
|
1589
1754
|
}
|
|
@@ -1736,7 +1901,6 @@ function extractAndWriteAssistantTurnsBestEffort(cwd, stdinPayload) {
|
|
|
1736
1901
|
// writer applies the same guard via its own internal check.
|
|
1737
1902
|
return;
|
|
1738
1903
|
}
|
|
1739
|
-
const ledgerPath = join(fabricDir, EVENT_LEDGER_FILE);
|
|
1740
1904
|
const client = detectClient();
|
|
1741
1905
|
let randomUUID;
|
|
1742
1906
|
try {
|
|
@@ -1792,7 +1956,7 @@ function extractAndWriteAssistantTurnsBestEffort(cwd, stdinPayload) {
|
|
|
1792
1956
|
timestamp: new Date().toISOString(),
|
|
1793
1957
|
};
|
|
1794
1958
|
if (client !== undefined) event.client = client;
|
|
1795
|
-
|
|
1959
|
+
appendEvent(fabricDir, event);
|
|
1796
1960
|
} catch {
|
|
1797
1961
|
// Per-turn failure must not abort the remaining turns; the Stop hook
|
|
1798
1962
|
// contract is "never block on hook failure". Best-effort continues.
|
|
@@ -2060,6 +2224,45 @@ function writeSessionDigestBestEffort(projectRoot, stdinPayload) {
|
|
|
2060
2224
|
}
|
|
2061
2225
|
}
|
|
2062
2226
|
|
|
2227
|
+
// ux-w0-3 (KT-DEC-0007): the SINGLE soft-emit path for EVERY Stop-hook signal
|
|
2228
|
+
// (archive / archive_backlog / review / import / maintenance). A nudge is a
|
|
2229
|
+
// reminder layer, NEVER a gate — so no signal ever emits a blocking decision.
|
|
2230
|
+
// The AI channel always carries the reason (flow ⊥ observation, D3); the human
|
|
2231
|
+
// systemMessage is gated by nudge_mode (D4/D5), with high-value signals
|
|
2232
|
+
// (knowledge-loss: archive / archive_backlog) surfacing at lower volumes.
|
|
2233
|
+
// Mutates `result` (strips telemetry-only threshold/actual_value, like the prior
|
|
2234
|
+
// inline paths). When the client adapter is unavailable, falls back to a plain
|
|
2235
|
+
// non-blocking JSON payload (decision stays "soft", never blocking).
|
|
2236
|
+
function emitSoftSignal(out, result, cwd, highValue) {
|
|
2237
|
+
const reasonText = typeof result.reason === "string" ? result.reason : "";
|
|
2238
|
+
delete result.threshold;
|
|
2239
|
+
delete result.actual_value;
|
|
2240
|
+
const client =
|
|
2241
|
+
clientAdapter && typeof clientAdapter.detectClient === "function"
|
|
2242
|
+
? clientAdapter.detectClient(__dirname)
|
|
2243
|
+
: undefined;
|
|
2244
|
+
// Known client (cc / codex): emit the dual-sink envelope on stdout —
|
|
2245
|
+
// additionalContext(AI, always) + systemMessage(human, gated by nudge_mode).
|
|
2246
|
+
if (client && clientAdapter && typeof clientAdapter.emitDualSink === "function") {
|
|
2247
|
+
const humanGate =
|
|
2248
|
+
nudgePolicy !== null
|
|
2249
|
+
? nudgePolicy.resolveHumanSink(cwd, "stop", { highValue })
|
|
2250
|
+
: { emitHuman: true };
|
|
2251
|
+
clientAdapter.emitDualSink(
|
|
2252
|
+
{ human: humanGate.emitHuman ? reasonText : null, ai: reasonText },
|
|
2253
|
+
{ client, eventName: "Stop", streams: { stdout: out } },
|
|
2254
|
+
);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
// Unknown client / no adapter → emit the reason as a plain, non-blocking
|
|
2258
|
+
// payload on stdout. `result.decision` is "soft" (non-blocking), so this is
|
|
2259
|
+
// a reminder, not a gate (KT-DEC-0007).
|
|
2260
|
+
out.write(JSON.stringify(result));
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// High-value (knowledge-loss) signals surface at lower nudge_mode volumes.
|
|
2264
|
+
const HIGH_VALUE_SIGNALS = new Set(["archive", "archive_backlog"]);
|
|
2265
|
+
|
|
2063
2266
|
/**
|
|
2064
2267
|
* Main entry — invoked both as a CLI (require.main === module) and in-process by tests.
|
|
2065
2268
|
*
|
|
@@ -2123,24 +2326,41 @@ function main(env, stdio) {
|
|
|
2123
2326
|
// ts to anchor the count; rather than rescanning events here, we mirror
|
|
2124
2327
|
// decide()'s scan locally to keep the helper pure. The threshold comes
|
|
2125
2328
|
// from fabric-config.json (archive_edit_threshold, default 20).
|
|
2329
|
+
// crack 1: per-session edit view. anchor = THIS session's own last archive
|
|
2330
|
+
// watermark (session_archive_attempted.covered_through_ts) else its first
|
|
2331
|
+
// ledger activity; count = THIS session's file_mutated events since anchor.
|
|
2332
|
+
// Reads the event ledger, NOT the session-blind edit-counter sidecar.
|
|
2126
2333
|
let editCounterStats;
|
|
2127
2334
|
try {
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
const ev = events[i];
|
|
2131
|
-
if (ev && ev.event_type === EVENT_TYPE_PROPOSED && typeof ev.ts === "number") {
|
|
2132
|
-
anchorTs = ev.ts;
|
|
2133
|
-
break;
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2335
|
+
const sid = resolveHookSessionId(stdinPayload);
|
|
2336
|
+
const anchorTs = sessionAnchorTs(events, sid);
|
|
2136
2337
|
editCounterStats = {
|
|
2137
|
-
|
|
2338
|
+
editsSinceArchive: countSessionMutationsSince(events, sid, anchorTs),
|
|
2138
2339
|
threshold: readArchiveEditThreshold(cwd),
|
|
2340
|
+
anchorPresent: anchorTs !== null,
|
|
2139
2341
|
};
|
|
2140
2342
|
} catch {
|
|
2141
2343
|
editCounterStats = {
|
|
2142
|
-
|
|
2344
|
+
editsSinceArchive: 0,
|
|
2143
2345
|
threshold: DEFAULT_ARCHIVE_EDIT_THRESHOLD,
|
|
2346
|
+
anchorPresent: false,
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// crack 2: cross-session backlog view — count DEAD sessions (other than the
|
|
2351
|
+
// current one) carrying unarchived high-value work. Drives the
|
|
2352
|
+
// archive_backlog signal that replaces the retired global-24h timer.
|
|
2353
|
+
let backlogStats;
|
|
2354
|
+
try {
|
|
2355
|
+
const sid = resolveHookSessionId(stdinPayload);
|
|
2356
|
+
backlogStats = {
|
|
2357
|
+
deadSessionCount: countBacklogSessions(events, nowMs, sid, readArchiveBacklogIdleHours(cwd)),
|
|
2358
|
+
threshold: readArchiveBacklogSessionCount(cwd),
|
|
2359
|
+
};
|
|
2360
|
+
} catch {
|
|
2361
|
+
backlogStats = {
|
|
2362
|
+
deadSessionCount: 0,
|
|
2363
|
+
threshold: DEFAULT_ARCHIVE_BACKLOG_SESSION_COUNT,
|
|
2144
2364
|
};
|
|
2145
2365
|
}
|
|
2146
2366
|
|
|
@@ -2223,6 +2443,7 @@ function main(env, stdio) {
|
|
|
2223
2443
|
thresholds,
|
|
2224
2444
|
{ activityOverview },
|
|
2225
2445
|
importInFlight,
|
|
2446
|
+
backlogStats,
|
|
2226
2447
|
);
|
|
2227
2448
|
|
|
2228
2449
|
// v2.0.0-rc.7 T10: Signal D — maintenance hint. Evaluated AFTER A/B/C
|
|
@@ -2259,22 +2480,18 @@ function main(env, stdio) {
|
|
|
2259
2480
|
return;
|
|
2260
2481
|
}
|
|
2261
2482
|
|
|
2262
|
-
// v2.2 dual-sink (Goal A / D6): VALUE-GATE the archive nudge.
|
|
2263
|
-
// edit
|
|
2264
|
-
//
|
|
2265
|
-
//
|
|
2266
|
-
//
|
|
2267
|
-
//
|
|
2483
|
+
// v2.2 dual-sink (Goal A / D6): VALUE-GATE the in-session archive nudge. The
|
|
2484
|
+
// edit trigger is the CHECK cadence; the nudge only fires when a high-value
|
|
2485
|
+
// signal accrued since the last archive (decouples check from disturb).
|
|
2486
|
+
// crack 1: re-anchored PER SESSION (watermark = this session's own anchor,
|
|
2487
|
+
// probe scoped to this session) so a neighbour window's high-value work past
|
|
2488
|
+
// the same global watermark can't keep — or suppress — THIS window's nudge.
|
|
2489
|
+
// archive_backlog already incorporates high-value in its count, so it is not
|
|
2490
|
+
// re-gated here. Other signals (review/import/maintenance) are unaffected.
|
|
2268
2491
|
if (result.signal === "archive") {
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
if (ev && ev.event_type === EVENT_TYPE_PROPOSED && typeof ev.ts === "number") {
|
|
2273
|
-
watermarkTs = ev.ts;
|
|
2274
|
-
break;
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
if (!hasHighValueArchiveSignal(events, watermarkTs)) {
|
|
2492
|
+
const sid = resolveHookSessionId(stdinPayload);
|
|
2493
|
+
const watermarkTs = sessionAnchorTs(events, sid);
|
|
2494
|
+
if (!hasHighValueArchiveSignal(events, watermarkTs, sid)) {
|
|
2278
2495
|
return; // no high-value candidate → stay quiet (D6 value-gate)
|
|
2279
2496
|
}
|
|
2280
2497
|
}
|
|
@@ -2318,9 +2535,7 @@ function main(env, stdio) {
|
|
|
2318
2535
|
// uses hours, so we branch here to avoid mixing semantics.
|
|
2319
2536
|
if (result.signal === "maintenance") {
|
|
2320
2537
|
emitSignalFiredEvent(cwd, sessionId, result);
|
|
2321
|
-
|
|
2322
|
-
delete result.actual_value;
|
|
2323
|
-
out.write(JSON.stringify(result));
|
|
2538
|
+
emitSoftSignal(out, result, cwd, HIGH_VALUE_SIGNALS.has(result.signal));
|
|
2324
2539
|
writeMaintenanceLastEmit(cwd, nowMs, resolveHookSessionId(stdinPayload));
|
|
2325
2540
|
return;
|
|
2326
2541
|
}
|
|
@@ -2342,27 +2557,12 @@ function main(env, stdio) {
|
|
|
2342
2557
|
}
|
|
2343
2558
|
|
|
2344
2559
|
emitSignalFiredEvent(cwd, sessionId, result);
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
//
|
|
2349
|
-
//
|
|
2350
|
-
|
|
2351
|
-
// it (flow ⊥ observation). Missing it is backstopped by the SessionEnd marker
|
|
2352
|
-
// + cross-session debt (D3). Review/import keep the decision:block contract
|
|
2353
|
-
// (out of Goal A scope; KT-DEC-0007 nudge semantics unchanged for them).
|
|
2354
|
-
if (result.signal === "archive" && clientAdapter && typeof clientAdapter.emitDualSink === "function") {
|
|
2355
|
-
const humanGate =
|
|
2356
|
-
nudgePolicy !== null
|
|
2357
|
-
? nudgePolicy.resolveHumanSink(cwd, "stop", { highValue: true })
|
|
2358
|
-
: { emitHuman: true };
|
|
2359
|
-
clientAdapter.emitDualSink(
|
|
2360
|
-
{ human: humanGate.emitHuman ? reasonText : null, ai: reasonText },
|
|
2361
|
-
{ client: clientAdapter.detectClient(__dirname), eventName: "Stop", streams: { stdout: out } },
|
|
2362
|
-
);
|
|
2363
|
-
} else {
|
|
2364
|
-
out.write(JSON.stringify(result));
|
|
2365
|
-
}
|
|
2560
|
+
// ux-w0-3 (KT-DEC-0007): EVERY A/B/C signal (archive / archive_backlog /
|
|
2561
|
+
// review / import) emits SOFT via the shared path — additionalContext(AI) +
|
|
2562
|
+
// nudge_mode-gated systemMessage(human), NEVER decision:block. Previously
|
|
2563
|
+
// only `archive` was soft and review/import blocked; the block contract is
|
|
2564
|
+
// retired (a nudge is a reminder layer, never a gate).
|
|
2565
|
+
emitSoftSignal(out, result, cwd, HIGH_VALUE_SIGNALS.has(result.signal));
|
|
2366
2566
|
cache[result.signal] = nowMs;
|
|
2367
2567
|
writeShownCache(cwd, cache, resolveHookSessionId(stdinPayload));
|
|
2368
2568
|
} catch {
|
|
@@ -2385,6 +2585,14 @@ module.exports = {
|
|
|
2385
2585
|
// for unit testing of the truth table).
|
|
2386
2586
|
isImportInFlight,
|
|
2387
2587
|
decide,
|
|
2588
|
+
// crack 1 + 2: two-lane archive strategy helpers (exported for unit testing).
|
|
2589
|
+
sessionArchiveWatermark,
|
|
2590
|
+
sessionFirstActivityTs,
|
|
2591
|
+
sessionAnchorTs,
|
|
2592
|
+
countSessionMutationsSince,
|
|
2593
|
+
countBacklogSessions,
|
|
2594
|
+
readArchiveBacklogSessionCount,
|
|
2595
|
+
readArchiveBacklogIdleHours,
|
|
2388
2596
|
readCooldownHours,
|
|
2389
2597
|
readUnderseedThreshold,
|
|
2390
2598
|
readArchiveEditThreshold,
|