create-claude-cabinet 0.51.0 → 0.53.0
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 +35 -28
- package/lib/cli.js +2 -2
- package/lib/modules.js +5 -4
- package/package.json +1 -1
- package/templates/CLAUDE.md +196 -31
- package/templates/EXTENSIONS.md +7 -0
- package/templates/README.md +3 -3
- package/templates/briefing/_briefing-template.md +1 -1
- package/templates/briefing/_briefing-work-tracking-template.md +3 -3
- package/templates/cabinet/_cabinet-member-template.md +10 -7
- package/templates/cabinet/memory-lifecycle-contract.md +16 -0
- package/templates/cabinet/pib-db-access.md +1 -1
- package/templates/cabinet/pib-db-triggers.md +20 -16
- package/templates/cabinet/watchtower-contracts.md +321 -10
- package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +2 -2
- package/templates/memory/patterns/_pattern-template.md +1 -1
- package/templates/mux/bin/mux +5 -0
- package/templates/mux/config/mux-server.py +7 -5
- package/templates/rules/enforcement-pipeline.md +2 -2
- package/templates/rules/memory-capture.md +2 -2
- package/templates/scripts/__tests__/absorption-no-prune.test.mjs +89 -0
- package/templates/scripts/__tests__/assessment-recalibration.test.mjs +0 -2
- package/templates/scripts/__tests__/batch-disposition.test.mjs +7 -9
- package/templates/scripts/__tests__/category-expiry.test.mjs +130 -0
- package/templates/scripts/__tests__/detector-registry.test.mjs +41 -7
- package/templates/scripts/__tests__/draft-surfacing.test.mjs +46 -162
- package/templates/scripts/__tests__/extraction-classification.test.mjs +306 -0
- package/templates/scripts/__tests__/fixtures/read-pass-2026-07-14.json +325 -0
- package/templates/scripts/__tests__/fold-similarity.test.mjs +290 -0
- package/templates/scripts/__tests__/friction-actionability.test.mjs +180 -0
- package/templates/scripts/__tests__/hold-verb.test.mjs +149 -0
- package/templates/scripts/__tests__/memory-budget-debt.test.mjs +264 -0
- package/templates/scripts/__tests__/reach1-inbox-relations.test.mjs +274 -0
- package/templates/scripts/__tests__/reach2-significance.test.mjs +300 -0
- package/templates/scripts/__tests__/read-pass-fixture.test.mjs +71 -0
- package/templates/scripts/__tests__/ring2-pattern-gate.test.mjs +315 -0
- package/templates/scripts/__tests__/ring2-roster-review.test.mjs +75 -16
- package/templates/scripts/__tests__/ring3-completion-filter.test.mjs +167 -105
- package/templates/scripts/__tests__/ring3-outage-backfill.test.mjs +156 -0
- package/templates/scripts/__tests__/roster-ambient-render.test.mjs +132 -0
- package/templates/scripts/__tests__/routine-dispatch.test.mjs +29 -1
- package/templates/scripts/__tests__/routine-moment-expiry.test.mjs +192 -0
- package/templates/scripts/memory-consolidation-scan.mjs +149 -0
- package/templates/scripts/pib-db-mcp-server.mjs +2 -2
- package/templates/scripts/skill-validator.sh +7 -4
- package/templates/scripts/watchtower-build-context.mjs +33 -1
- package/templates/scripts/watchtower-lib.mjs +225 -6
- package/templates/scripts/watchtower-queue.mjs +245 -59
- package/templates/scripts/watchtower-ring1.mjs +194 -2
- package/templates/scripts/watchtower-ring2.mjs +488 -190
- package/templates/scripts/watchtower-ring3-close.mjs +994 -115
- package/templates/scripts/watchtower-routines.mjs +107 -1
- package/templates/skills/audit/SKILL.md +5 -5
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +35 -2
- package/templates/skills/cabinet-anti-confirmation/SKILL.md +24 -0
- package/templates/skills/cabinet-cc-health/SKILL.md +22 -0
- package/templates/skills/cabinet-data-integrity/SKILL.md +19 -0
- package/templates/skills/cabinet-debugger/SKILL.md +36 -0
- package/templates/skills/cabinet-deployment/SKILL.md +22 -0
- package/templates/skills/cabinet-elegance/SKILL.md +12 -0
- package/templates/skills/cabinet-goal-alignment/SKILL.md +11 -0
- package/templates/skills/cabinet-historian/SKILL.md +33 -0
- package/templates/skills/cabinet-organized-mind/SKILL.md +11 -0
- package/templates/skills/cabinet-process-therapist/SKILL.md +132 -6
- package/templates/skills/cabinet-qa/SKILL.md +63 -0
- package/templates/skills/cabinet-roster-check/SKILL.md +12 -0
- package/templates/skills/cabinet-system-advocate/SKILL.md +2 -2
- package/templates/skills/cabinet-workflow-cop/SKILL.md +65 -3
- package/templates/skills/cc-feedback/SKILL.md +4 -4
- package/templates/skills/cc-remember/SKILL.md +4 -4
- package/templates/skills/close/SKILL.md +63 -9
- package/templates/skills/debrief/SKILL.md +14 -8
- package/templates/skills/debrief/phases/qa-handoff-sweep.md +12 -9
- package/templates/skills/dx-feedback/SKILL.md +1 -1
- package/templates/skills/inbox/SKILL.md +85 -16
- package/templates/skills/memory/SKILL.md +1 -1
- package/templates/skills/onboard/SKILL.md +42 -33
- package/templates/skills/onboard/phases/detect-state.md +3 -2
- package/templates/skills/onboard/phases/generate-briefing.md +3 -2
- package/templates/skills/onboard/phases/generate-session-loop.md +70 -37
- package/templates/skills/onboard/phases/interview.md +17 -12
- package/templates/skills/onboard/phases/modularity-menu.md +33 -19
- package/templates/skills/onboard/phases/options.md +3 -1
- package/templates/skills/onboard/phases/post-onboard-audit.md +17 -11
- package/templates/skills/onboard/phases/summary.md +15 -12
- package/templates/skills/onboard/phases/work-tracking.md +14 -8
- package/templates/skills/orient/SKILL.md +7 -5
- package/templates/skills/pulse/SKILL.md +23 -18
- package/templates/skills/seed/phases/build-member.md +5 -3
- package/templates/skills/session-handoff/SKILL.md +72 -45
- package/templates/skills/spring-clean/SKILL.md +15 -13
- package/templates/skills/triage-audit/SKILL.md +3 -3
|
@@ -62,7 +62,7 @@ every session:
|
|
|
62
62
|
When deferring an item that waits on a specific identifiable
|
|
63
63
|
condition — a dependency landing, a stack decision finalizing, a
|
|
64
64
|
referenced file appearing — use `pib_defer_with_trigger` instead of
|
|
65
|
-
plain `status='deferred'`.
|
|
65
|
+
plain `status='deferred'`. The session loop re-evaluates each trigger every
|
|
66
66
|
session and surfaces items whose conditions have become true. See
|
|
67
67
|
[pib-db-triggers.md](pib-db-triggers.md) for the full convention:
|
|
68
68
|
result vocabulary, cascade semantics for projects, migration
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pib-db Deferred Triggers
|
|
2
2
|
|
|
3
3
|
How to defer work items with structured return conditions and how
|
|
4
|
-
|
|
4
|
+
the session loop re-evaluates them each session.
|
|
5
5
|
|
|
6
6
|
## Purpose
|
|
7
7
|
|
|
@@ -13,8 +13,8 @@ or a `status='deferred'` row and sits there forever. Nobody re-reads
|
|
|
13
13
|
|
|
14
14
|
Deferred triggers turn those items into structurally-encoded work:
|
|
15
15
|
each one carries a natural-language condition describing what would
|
|
16
|
-
have to change to reactivate it.
|
|
17
|
-
and evaluates the condition against the current session's context.
|
|
16
|
+
have to change to reactivate it. The session loop surfaces them every
|
|
17
|
+
session and evaluates the condition against the current session's context.
|
|
18
18
|
The item sits in the queue, not on a reminder list.
|
|
19
19
|
|
|
20
20
|
## Schema
|
|
@@ -110,29 +110,33 @@ The cascade:
|
|
|
110
110
|
trigger is the single source of truth.
|
|
111
111
|
|
|
112
112
|
When the parent reopens (user flips status back to `active`, or
|
|
113
|
-
|
|
113
|
+
the session loop surfaces it as `triggered` and the user accepts):
|
|
114
114
|
|
|
115
115
|
- Children remain `deferred`.
|
|
116
116
|
- The user decides which children to reopen. Reopening everything
|
|
117
117
|
automatically often resurrects stale subgoals that the deferral
|
|
118
118
|
period should have retired.
|
|
119
119
|
|
|
120
|
-
##
|
|
120
|
+
## Session-loop integration
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
Under watchtower (the default loop), Ring 1 surfaces deferred/triggered
|
|
123
|
+
items in the project state file's Standing Issues, and the session — or
|
|
124
|
+
`/briefing` — re-evaluates each trigger against current context. On the
|
|
125
|
+
legacy loop, the `deferred-check.md` orient phase does this, firing after
|
|
126
|
+
`work-scan.md`, before `auto-maintenance.md`.
|
|
124
127
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
Either way, every session:
|
|
129
|
+
|
|
130
|
+
1. Call `pib_list_triggered` (or CLI fallback).
|
|
131
|
+
2. For each item, evaluate the trigger text against the current session's
|
|
132
|
+
context (new dependencies installed? a referenced file now exists? a
|
|
133
|
+
stack decision was finalized?).
|
|
134
|
+
3. Record each evaluation via `pib_mark_trigger_checked`. Prefer
|
|
130
135
|
`needs-info` over guessing `triggered`.
|
|
131
|
-
4. Items that evaluate to `triggered`
|
|
132
|
-
|
|
133
|
-
user decides.
|
|
136
|
+
4. Items that evaluate to `triggered` surface as **Attention Items**. The
|
|
137
|
+
session loop does not auto-reopen — the user decides.
|
|
134
138
|
|
|
135
|
-
Cost control: cap the
|
|
139
|
+
Cost control: cap the evaluation at 30 seconds. If more than 10 items are
|
|
136
140
|
triggered, evaluate only the 5 least-recently-checked.
|
|
137
141
|
|
|
138
142
|
## Migration guarantees
|
|
@@ -208,6 +208,81 @@ mux descriptor; routine items resolve normally (no QA gate, normal
|
|
|
208
208
|
expiry — expiry also clears the descriptor). A pending routine item
|
|
209
209
|
blocks refiring of the same routine until resolved or stale.
|
|
210
210
|
|
|
211
|
+
**Moment expiry for `time-of-day` routines (act:ea23b3a5).** A daily
|
|
212
|
+
routine's value is bound to its day: a morning briefing delivered
|
|
213
|
+
tomorrow is not a briefing. `stale_after_hours` only supersedes at the
|
|
214
|
+
NEXT due firing, which for a daily routine is 24 hours later, so a
|
|
215
|
+
missed firing held a queue slot and a lit desk badge all day. Ring 1's
|
|
216
|
+
tick now supersedes a pending `time-of-day` item once the calendar day
|
|
217
|
+
it was FILED on has ended.
|
|
218
|
+
|
|
219
|
+
End of day, not a fixed number of hours, and the difference is
|
|
220
|
+
measured rather than tasteful: across six weeks the portfolio's one
|
|
221
|
+
time-of-day routine had ten real pickups — eight within four hours and
|
|
222
|
+
two after, at 4.4h and 8.6h. A four-hour window would have destroyed two
|
|
223
|
+
genuine pickups to clear one stale item. End-of-day preserves all ten,
|
|
224
|
+
needs no tuning constant, and matches what a daily routine means to a
|
|
225
|
+
person: you can still want your morning briefing at 4pm; you do not want
|
|
226
|
+
it tomorrow.
|
|
227
|
+
|
|
228
|
+
Three scoping rules, each load-bearing:
|
|
229
|
+
|
|
230
|
+
- **Ring 1's portfolio tick only.** Ring 3 also calls `runRoutinePass`,
|
|
231
|
+
with a single-project `session-close` event. An unscoped sweep there
|
|
232
|
+
would have every session close in any project superseding every OTHER
|
|
233
|
+
project's routine items — and one portfolio sweep per replayed session
|
|
234
|
+
during a `--reprocess-failed` drain.
|
|
235
|
+
- **Classify from the ITEM, not from config.** `dispatchRoutine`
|
|
236
|
+
persists `evidence.trigger` at filing, so a routine whose declaration
|
|
237
|
+
was later removed still classifies. A config lookup would find nothing
|
|
238
|
+
and leave it pending forever.
|
|
239
|
+
- **An unparseable `filed_at` abstains, loudly.** The `stale_after_hours`
|
|
240
|
+
path treats the same input as stale (NaN comparisons fall through to
|
|
241
|
+
supersede); this one refuses and logs. Two mechanisms disagreeing
|
|
242
|
+
silently is worse than one abstaining out loud.
|
|
243
|
+
|
|
244
|
+
Consequences, named rather than discovered later: for a `time-of-day`
|
|
245
|
+
trigger the moment sweep pre-empts the `stale_after_hours` supersede
|
|
246
|
+
(same outcome, more honest reason), a missed briefing is no longer
|
|
247
|
+
re-delivered by the SessionStart missed-routine section on a LATER day,
|
|
248
|
+
and the sweep has no in-flight awareness — a routine picked up by `mux
|
|
249
|
+
qa drain` very close to midnight could be superseded mid-run, which
|
|
250
|
+
end-of-day narrows to minutes where a fixed-hour window would have
|
|
251
|
+
opened it to hours. `interval`, `path-nonempty`, and `session-close`
|
|
252
|
+
routines have no moment and are untouched.
|
|
253
|
+
|
|
254
|
+
## Upstream-Friction Actionability (Phase 2g)
|
|
255
|
+
|
|
256
|
+
A friction report with no party who could act on it is not a report.
|
|
257
|
+
Five of the 2026-07-26 dismissals were observations like "Claude made
|
|
258
|
+
iterative browser tweaks without writing to file" — true, and addressed
|
|
259
|
+
to nobody, aging in the queue for weeks.
|
|
260
|
+
|
|
261
|
+
Phase 2g now classifies each finding into three arms (act:ea23b3a5),
|
|
262
|
+
copying Phase 2f's verification-gate shape (which files a methodology
|
|
263
|
+
capture only when the claimed artifact verifiably exists on disk, after
|
|
264
|
+
11 of 11 unverified captures were dismissed over three weeks):
|
|
265
|
+
|
|
266
|
+
1. **A named route** — `evidence.route.type` from the CLOSED vocabulary
|
|
267
|
+
`cc-feedback | project-tracker | config-change`, plus a non-empty
|
|
268
|
+
`detail` naming who acts and how ⇒ files `upstream-friction`, with the
|
|
269
|
+
route rendered into the summary so the operator sees it without
|
|
270
|
+
opening the evidence.
|
|
271
|
+
2. **Durable, no route** — a platform constraint or environment quirk
|
|
272
|
+
nobody can fix, whose WORKAROUND is worth remembering ⇒ files
|
|
273
|
+
`knowledge-extraction`. This arm is not theory: of the five friction
|
|
274
|
+
items the operator KEPT, four exited as `captured-to-memory`. A
|
|
275
|
+
two-arm gate would have discarded the useful half of the lens.
|
|
276
|
+
3. **Neither** — a transient incident or a general observation about
|
|
277
|
+
Claude's behavior ⇒ files nothing, and is COUNTED. All three counts
|
|
278
|
+
are logged every run; silence would be indistinguishable from a lens
|
|
279
|
+
that never ran, and the drop count is the number this change exists to
|
|
280
|
+
move.
|
|
281
|
+
|
|
282
|
+
An invented route type or an empty `detail` falls to arm 3. The
|
|
283
|
+
vocabulary is closed precisely so "someone should fix this" cannot
|
|
284
|
+
become an escape hatch.
|
|
285
|
+
|
|
211
286
|
## Session Advisor Pass (re-homed standing advisors)
|
|
212
287
|
|
|
213
288
|
The standing session advisors (historian, system-advocate,
|
|
@@ -336,6 +411,125 @@ each owning its tier. Flag: `defaults.recall_canary` (opt-out).
|
|
|
336
411
|
extraction never proposed (M2 under-extraction). The canary states
|
|
337
412
|
this blindness in its own output.
|
|
338
413
|
|
|
414
|
+
## Reach 1 — Pending-Relation Proposals (act:5182beda, N3 of grp:retro-remeaning)
|
|
415
|
+
|
|
416
|
+
The first shippable instance of **retrospective temporality** (`act:ef5a3842`,
|
|
417
|
+
MANIFESTO principle 4): new understanding reaching BACK to recolor what came
|
|
418
|
+
before, at the earliest and safest reach — the still-pending inbox, where
|
|
419
|
+
nothing is believed yet. Every specimen that motivated the program had its
|
|
420
|
+
counterpart sitting pending at the moment the second one was written; Phase
|
|
421
|
+
2d's extraction call now sees that pending pile and can propose a relation.
|
|
422
|
+
|
|
423
|
+
**Mechanism.** `selectNearbyPendingItems` (ring3-close.mjs) shortlists the
|
|
424
|
+
pending inbox drafts most relevant to THIS session's transcript (same
|
|
425
|
+
relevance-scoring recipe as `selectNearbyMemoryTitles`, a SEPARATE corpus
|
|
426
|
+
with its OWN cap, `PENDING_RELATION_CAP` — never a shared budget with
|
|
427
|
+
memory's, or a large pending pile would evict memory titles and degrade the
|
|
428
|
+
M1b novelty gate). The extraction prompt's `PENDING IN INBOX` block is
|
|
429
|
+
injected as its OWN section, distinct from `ALREADY SAVED TO MEMORY`. For
|
|
430
|
+
each item, the model may set `relates_to` (the exact pending title) and
|
|
431
|
+
`relation` (`pairs_with` | `answers` | `contradicts`) — fields the model
|
|
432
|
+
outputs ALONGSIDE, never IN, `covered_by`. This separation is load-bearing:
|
|
433
|
+
`covered_by` stays scoped to memory titles and feeds `modelRescues`/
|
|
434
|
+
`isDuplicate` exactly as before; `relates_to`/`relation` are never read by
|
|
435
|
+
either — naming a pending title in `covered_by` instead would flip the
|
|
436
|
+
rescue gate into a suppressor (a filed-today item wrongly read as "covered
|
|
437
|
+
by," and suppressed, because it topically resembles a merely-PENDING draft
|
|
438
|
+
that isn't believed yet — the exact failure the Meta question/answer
|
|
439
|
+
specimen would have suffered).
|
|
440
|
+
|
|
441
|
+
**Filing.** A relation proposal is its own inbox item, category
|
|
442
|
+
`pending-relation`, filed via `fileRelationProposal` AFTER the newly-
|
|
443
|
+
extracted item's own `createItem` call succeeds (its id isn't known until
|
|
444
|
+
then). Deduped by `evidence.relation_key` (sorted ids + the relation verb) —
|
|
445
|
+
one pending item per (pair, verb), so the same relation proposed from either
|
|
446
|
+
side, or in a later session, never refiles. The proposal never changes
|
|
447
|
+
anything about the item that prompted it: no suppression, no home change, no
|
|
448
|
+
urgency change — it is strictly additive.
|
|
449
|
+
|
|
450
|
+
**The M5 suppression ledger is blind to this channel.** `recordSuppression`
|
|
451
|
+
(the Suppression Ledger above) only sees LEXICAL suppressions — the
|
|
452
|
+
deterministic `isDuplicate` corpus matches. The model's real per-item
|
|
453
|
+
inclusion/omission decision happens INSIDE the extraction call, in a channel
|
|
454
|
+
the ledger cannot observe at all: if the model chose to silently omit an
|
|
455
|
+
item from its output array (rather than including it with `relation: none`),
|
|
456
|
+
no suppression record is ever written, because nothing was suppressed BY A
|
|
457
|
+
CORPUS — it was never proposed. This is why the prompt carries an explicit,
|
|
458
|
+
load-bearing instruction that a pending item is grounds for a RELATION only,
|
|
459
|
+
never grounds to omit or demote anything — the instruction is the entire
|
|
460
|
+
defense, because the suppression ledger cannot audit it.
|
|
461
|
+
|
|
462
|
+
**Retraction.** Registered in `INBOX_DETECTOR_REGISTRY` with a reconciler:
|
|
463
|
+
`autoReconcilePendingRelations` (ring1.mjs, one global pass per tick — purely
|
|
464
|
+
queue-internal, no per-project pib-db access, unlike `completion-review`'s).
|
|
465
|
+
Retracts via `autoReconcileItem` when EITHER cited side (`evidence.from_id`
|
|
466
|
+
/ `evidence.to_id`) leaves pending — a relation between two open questions
|
|
467
|
+
no longer applies once one resolves. Fails toward KEEPING: an id that no
|
|
468
|
+
longer resolves via `getItem` (deleted/migrated) is ambiguous, not evidence
|
|
469
|
+
the relation resolved.
|
|
470
|
+
|
|
471
|
+
## Reach 2 — Significance-Change Proposals (act:bcb7edd4, L1 of grp:retro-remeaning)
|
|
472
|
+
|
|
473
|
+
The second reach of the retrospective-temporality motion (`act:ef5a3842`,
|
|
474
|
+
MANIFESTO principle 4), extending Reach 1's inbox-range reach-back to the
|
|
475
|
+
recently-absorbed thread trail. When Ring 3 closes a session, the new
|
|
476
|
+
understanding it captured reaches BACK into the prior cursor history of the
|
|
477
|
+
threads THIS session touched, and may PROPOSE that an earlier entry now
|
|
478
|
+
means something different: `reframes` (significance change — what looked
|
|
479
|
+
like friction was the seed of the architecture; the class a contradiction
|
|
480
|
+
scan is structurally blind to), `answers` (a real earlier uncertainty is
|
|
481
|
+
settled), or `contradicts`. Category: `significance-change`.
|
|
482
|
+
|
|
483
|
+
**Mechanism.** Inside Phase 2b2, AFTER the cursor appends land: the prior
|
|
484
|
+
trail of each earned thread is snapshotted from disk BEFORE this session's
|
|
485
|
+
entry is appended (last `PRIOR_HISTORY_CAP` entries), and one focused
|
|
486
|
+
follow-up model call (`significanceReachBack`) sees the new understanding
|
|
487
|
+
beside each prior trail. It fires only when at least one earned thread has
|
|
488
|
+
prior history — a routine or first-touch session costs nothing. It cannot
|
|
489
|
+
ride the capture call the way Reach 1 rode extraction: the reach-back
|
|
490
|
+
corpus (which threads matter) IS that call's output. EVENT-DRIVEN by
|
|
491
|
+
construction — never a periodic sweep (the shape the cabinet killed).
|
|
492
|
+
|
|
493
|
+
**Proposals are validated, then durably deduped.** A proposal must cite an
|
|
494
|
+
ACTUAL prior entry — session_id + date checked against the on-disk snapshot
|
|
495
|
+
(`validateRecolorProposals`); a hallucinated anchor drops, logged, never
|
|
496
|
+
filed. Filing (`fileSignificanceProposal`) dedups by
|
|
497
|
+
`evidence.relation_key` (`thread|prior_session|new_session|verb`) across
|
|
498
|
+
ALL queue statuses with NO time window: a dismissed or expired judgment
|
|
499
|
+
about a static corpus never re-nags — not after `RESOLUTION_CORPUS_DAYS`,
|
|
500
|
+
not ever — and a reprocess replay of the same session produces the same key
|
|
501
|
+
and files nothing. (The same unwindowed-exclusion discipline was backported
|
|
502
|
+
to Reach 1's `fileRelationProposal`, whose original pending-only dedup
|
|
503
|
+
left a post-dismissal refile hole.)
|
|
504
|
+
|
|
505
|
+
**The gate is real, not ceremonial.** Both relational categories
|
|
506
|
+
(`significance-change`, `pending-relation`) are in `GATED_CATEGORIES`:
|
|
507
|
+
`applyBatch` structurally refuses them, so each reinterpretation is
|
|
508
|
+
confirmed or dismissed individually — a rubber-stamped batch of re-meanings
|
|
509
|
+
is how a system gaslights its operator. On CONFIRM, /inbox applies the
|
|
510
|
+
recolor via `appendThreadRecolor` (watchtower-lib): an entry in the
|
|
511
|
+
thread's additive `recolor_log` array. The recolor ANNOTATES history — it
|
|
512
|
+
never rewrites `cursor_history`, and no ring ever writes `recolor_log`;
|
|
513
|
+
the rings propose, only the operator's confirmation applies.
|
|
514
|
+
|
|
515
|
+
**Retraction.** `autoReconcileSignificanceItems` (ring1, per tick):
|
|
516
|
+
retracts when the cited thread file is POSITIVELY gone from
|
|
517
|
+
`state/threads/` (dir readable, file absent — a recolor of deleted history
|
|
518
|
+
is moot); keeps on any read error, and keeps EVERYTHING when the dir is
|
|
519
|
+
unreadable (a mount hiccup must not mass-retract).
|
|
520
|
+
|
|
521
|
+
**The honest metric that gates Reach 3.** Every real-work close appends one
|
|
522
|
+
line to `state/significance-ledger.jsonl` (`recordSignificanceEvent`,
|
|
523
|
+
fail-open, same discipline as the suppression ledger): session, threads
|
|
524
|
+
touched, threads with history, entries shown, proposals filed.
|
|
525
|
+
`readSignificanceRate` (watchtower-lib) combines the ledger (denominator,
|
|
526
|
+
replay-deduped by session_id) with the queue's per-category dispositions
|
|
527
|
+
(numerator) into `{sessions, proposals_per_session, keep_ratio, …}` — with
|
|
528
|
+
null ratios when a denominator is zero, never fabricated 0s. This is the
|
|
529
|
+
L2 gate's named reader: Reach 3 (`act:a8b372a7`) is built only when an
|
|
530
|
+
observed significance-change rate on real data warrants it — replacing the
|
|
531
|
+
contradiction-density measurement the plan retired as the wrong instrument.
|
|
532
|
+
|
|
339
533
|
## Detector Symmetry (no detector without a retraction path)
|
|
340
534
|
|
|
341
535
|
The 2026-07-12 inbox drain (893→194 pending) hit the same disease four
|
|
@@ -404,16 +598,133 @@ branch-diverged item for it — the branch stays visible on the attention
|
|
|
404
598
|
line / git-attention sidecar / summary / Standing Issues (the exclusion
|
|
405
599
|
gates filing ONLY), and the consumer overrides the list in config.
|
|
406
600
|
|
|
407
|
-
**Standing-debt aggregation (coverage-warning
|
|
408
|
-
whose finding is a STANDING condition (recurs while a debt
|
|
409
|
-
files ONE item per (project, debt-class) and APPENDS evidence
|
|
410
|
-
pending item
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
601
|
+
**Standing-debt aggregation (coverage-warning, memory-budget):** a
|
|
602
|
+
detector whose finding is a STANDING condition (recurs while a debt
|
|
603
|
+
persists) files ONE item per (project, debt-class) and APPENDS evidence
|
|
604
|
+
to the pending item, carrying `evidence.first_seen` (oldest evidence
|
|
605
|
+
date) so aging reads the debt's true age. Resolving the item closes the
|
|
606
|
+
accumulated view; if the debt persists, the next tick files FRESH —
|
|
607
|
+
post-disposition recurrence is a new signal, not a refile loop
|
|
608
|
+
(act:e888dd63; 49 identical coverage-warning items for one month-old
|
|
609
|
+
debt was the live specimen).
|
|
610
|
+
|
|
611
|
+
Three rules the second adopter (memory-budget, act:ea23b3a5) added, each
|
|
612
|
+
learned from a live failure mode:
|
|
613
|
+
|
|
614
|
+
1. **The occurrence key must be the operator's unit of time, not the
|
|
615
|
+
ring's.** coverage-warning keys on the session; memory-budget keys on
|
|
616
|
+
the CALENDAR DAY, because Ring 2 slow runs ~48 times a day. A per-run
|
|
617
|
+
key makes the replay guard dead code, grows the key list by ~17,500
|
|
618
|
+
entries a year inside one JSON file, renders a count no human can
|
|
619
|
+
read, and — because the backlog-rot reader weighs standing debt by
|
|
620
|
+
that count — poisons the `keeping` / `losing` / `not-consumed` verdict
|
|
621
|
+
`/inbox` and `/briefing` lead with.
|
|
622
|
+
2. **Reuse an evidence spelling the readers already know.** The rot
|
|
623
|
+
weigher (`itemSessionWeight`, watchtower-inbox-assessment) accepts
|
|
624
|
+
`session_count` / `session_ids` / `sessions` and silently weighs
|
|
625
|
+
everything else as 1. A new adopter that invents a fifth spelling
|
|
626
|
+
reintroduces the blind spot the fourth was added to fix. memory-budget
|
|
627
|
+
writes `session_count`.
|
|
628
|
+
3. **Prefer `annotateItemEvidence` over a direct item write, and do not
|
|
629
|
+
bump `filed_at` without a reason.** The append gets a fresh read, a
|
|
630
|
+
pending fence checked at write time, and a deep-equal skip for free.
|
|
631
|
+
Bumping `filed_at` has three side effects worth choosing
|
|
632
|
+
deliberately: it strips an `[AGING]` prefix the escalation pass added
|
|
633
|
+
(and prevents its return), it re-escalates urgency the decay pass
|
|
634
|
+
de-escalated, and — since `listPending` sorts by `filed_at` — it pins
|
|
635
|
+
the item to the top of every pending list permanently. coverage-warning
|
|
636
|
+
bumps on purpose (its expiry clock should measure the last
|
|
637
|
+
recurrence); memory-budget does not.
|
|
638
|
+
|
|
639
|
+
**What does NOT aggregate.** `doc-drift` carries a per-claim,
|
|
640
|
+
mutually-exclusive fix-doc / fix-code decision, so N claims folded into
|
|
641
|
+
one item pose a question the operator cannot answer, and any reconciler
|
|
642
|
+
over the aggregate could only fire when every claim cleared.
|
|
643
|
+
`raised-unhandled` is free text: recognizing "this loose end restates
|
|
644
|
+
that one" needs a similarity matcher, and `proposeFolds`'
|
|
645
|
+
`overlapCoefficient` at `FOLD_SIMILARITY_THRESHOLD = 0.22` folds two
|
|
646
|
+
three-token titles that share ONE word — calibrated for advisory
|
|
647
|
+
retrieval, not for a destructive merge. Both instead route through Ring
|
|
648
|
+
2's `runDraftAnnotationSweep`, which annotates both sides with
|
|
649
|
+
`possible_duplicate_of` and demotes them out of batch sign-off. **A
|
|
650
|
+
lexical matcher may propose; it may never silently suppress.**
|
|
651
|
+
|
|
652
|
+
## Category Expiry
|
|
653
|
+
|
|
654
|
+
Every pending item ages out on a per-category policy. The table and its
|
|
655
|
+
helpers live in `watchtower-queue.mjs` (`expiryDaysFor`,
|
|
656
|
+
`categoryNeverExpires`, `CATEGORY_EXPIRE_DAYS`) — read the constants
|
|
657
|
+
there for the numbers; mirroring them into this document would be a
|
|
658
|
+
count claim that drifts, in a repo whose Ring 4 files `doc-drift` items
|
|
659
|
+
for exactly that.
|
|
660
|
+
|
|
661
|
+
**There are TWO expiry engines and both must delegate.** Ring 2's
|
|
662
|
+
`escalateQueueItems` runs on the 5-minute cron; `runExpiry` is called by
|
|
663
|
+
`/inbox` and `/briefing`. Before act:ea23b3a5 each hardcoded 30 days and
|
|
664
|
+
inlined its own qa-handoff carve-out, so a per-category policy set in one
|
|
665
|
+
would be silently violated by whichever engine reached an item first.
|
|
666
|
+
|
|
667
|
+
**The qa-handoff exemption is structural, not a preference.**
|
|
668
|
+
`expireItem` THROWS on a qa-handoff by the recipient-gate contract, so
|
|
669
|
+
`categoryNeverExpires` lives beside that throw: returning anything but
|
|
670
|
+
null for it would crash the caller's loop. One fact, one place.
|
|
671
|
+
|
|
672
|
+
**A shortened expiry can open a refile loop.** completion-review expires
|
|
673
|
+
at 14 days, which equals `COMPLETION_REVIEW_DEDUP_DAYS`. An item
|
|
674
|
+
therefore leaves the dedup window at exactly the moment it expires — so
|
|
675
|
+
Phase 2c's dedup corpus must include `expired` and `superseded`, or the
|
|
676
|
+
fid is in no corpus at all and the next session refiles it forever. That
|
|
677
|
+
is Detector Symmetry §2's named failure, reached from the other
|
|
678
|
+
direction: **when you shorten a category's expiry, check what its filing
|
|
679
|
+
site dedups against.**
|
|
680
|
+
|
|
681
|
+
## Queue Items Require a Decision; Ambient State Describes Conditions
|
|
682
|
+
|
|
683
|
+
A queue item asks the operator to decide something. A per-project state
|
|
684
|
+
file describes a condition. Filing a condition as an item spends the
|
|
685
|
+
operator's scarcest resource — a triage slot — on something they can
|
|
686
|
+
only acknowledge.
|
|
687
|
+
|
|
688
|
+
**The axis is who is judging, not whether the value is recomputable.**
|
|
689
|
+
Recomputability is the wrong test and would contradict a rule this
|
|
690
|
+
system already ships: derivable and perishable facts (act:471dd701,
|
|
691
|
+
act:a69c21f8) are recomputable *by definition*, and they are **always
|
|
692
|
+
filed** — routed to their derivation or the session record, never
|
|
693
|
+
dropped — because that routing is a per-item MODEL judgment, and
|
|
694
|
+
dropping one would be silent model auto-suppression. A deterministic
|
|
695
|
+
ring measurement is different in kind: the operator routes it once, by
|
|
696
|
+
category, and the routing holds.
|
|
697
|
+
|
|
698
|
+
**The functional test: does waiting cost something irreversible?**
|
|
699
|
+
`worktree-unmerged` is the calibrating counter-example. Ring 1
|
|
700
|
+
recomputes it from git every five minutes, so by a recomputability rule
|
|
701
|
+
it would be pure ambient state — and it must stay in the queue, because
|
|
702
|
+
the worktree gets cleaned up and the work is gone. A recomputable
|
|
703
|
+
condition with decay belongs in the queue; a recomputable condition that
|
|
704
|
+
is stable belongs in ambient state.
|
|
705
|
+
|
|
706
|
+
**The calibrating pair (act:ea23b3a5).** The cabinet-roster review
|
|
707
|
+
produces three signals. `uncovered-tech` names a specific per-project
|
|
708
|
+
gap and proposes adding a cabinet seat — a decision, so it stays a queue
|
|
709
|
+
item. `dormant-skill` and `stale-briefing` are measurements, so they
|
|
710
|
+
moved to `state/roster-review.json` and render in Ring 1's Standing
|
|
711
|
+
Issues.
|
|
712
|
+
|
|
713
|
+
Three obligations come with moving something to ambient state:
|
|
714
|
+
|
|
715
|
+
- **Fix the measurement first.** The dormant-skill count was a saturated
|
|
716
|
+
display cap: every project ever measured reported exactly "12 dead",
|
|
717
|
+
because the title was built from an array already sliced to
|
|
718
|
+
`ROSTER_DORMANT_LIST_CAP`. Moving that into seven state files would
|
|
719
|
+
have given a constant the authority of a fact.
|
|
720
|
+
- **Render on change, or give it a firing threshold.** Ambient state has
|
|
721
|
+
no dismiss verb — the queue item at least had one. A number that never
|
|
722
|
+
moves in a file read at every briefing is furniture by the third read.
|
|
723
|
+
The precedent is the recall canary, which Ring 1 renders only on alert.
|
|
724
|
+
- **Retire what is already filed.** The filing site was usually the only
|
|
725
|
+
code that ever touched those items. Removing it without a one-shot
|
|
726
|
+
supersede leaves them displaying a stale count for weeks beside a live
|
|
727
|
+
ambient line saying something different — dual existence.
|
|
417
728
|
|
|
418
729
|
**Known limitation (named, not hidden):** reconcilers run per
|
|
419
730
|
configured project — items whose project was deleted or de-registered
|
|
@@ -176,7 +176,7 @@ const TOOLS = [
|
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
name: 'pib_defer_with_trigger',
|
|
179
|
-
description: 'Defer an action or project with a free-text trigger condition describing what would reactivate it.
|
|
179
|
+
description: 'Defer an action or project with a free-text trigger condition describing what would reactivate it. Each session re-evaluates triggers and surfaces those whose condition has fired. Use this INSTEAD of pib_update_action with status=deferred when the deferral is waiting for a specific condition.',
|
|
180
180
|
inputSchema: {
|
|
181
181
|
type: 'object',
|
|
182
182
|
properties: {
|
|
@@ -199,7 +199,7 @@ const TOOLS = [
|
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
201
|
name: 'pib_mark_trigger_checked',
|
|
202
|
-
description: 'Record that a trigger was evaluated. Use after
|
|
202
|
+
description: 'Record that a trigger was evaluated. Use after a session evaluates triggers against session context. Result must be one of: triggered | still-waiting | needs-info | condition-obsolete.',
|
|
203
203
|
inputSchema: {
|
|
204
204
|
type: 'object',
|
|
205
205
|
properties: {
|
|
@@ -35,7 +35,7 @@ Descriptive slug. Convention: `pattern-{domain}-{topic}` or just
|
|
|
35
35
|
`pattern-{topic}`.
|
|
36
36
|
|
|
37
37
|
### `description`
|
|
38
|
-
One-line summary. Used by
|
|
38
|
+
One-line summary. Used by the session loop to decide whether to load
|
|
39
39
|
the full pattern. Be specific — "deployment issues" is too vague;
|
|
40
40
|
"silent deployment failures and stale build log output" is useful.
|
|
41
41
|
|
package/templates/mux/bin/mux
CHANGED
|
@@ -28,6 +28,11 @@ in_tmux() { [[ -n "${TMUX:-}" ]]; }
|
|
|
28
28
|
|
|
29
29
|
slugify() {
|
|
30
30
|
local input="$*"
|
|
31
|
+
# Flatten to one line FIRST — sed/cut below process input line-by-line,
|
|
32
|
+
# so a multi-paragraph prompt would otherwise yield a multi-line "slug"
|
|
33
|
+
# (one line per input line) that mangles or empties out every caller's
|
|
34
|
+
# branch/directory name instead of collapsing to a single slug.
|
|
35
|
+
input="${input//$'\n'/ }"
|
|
31
36
|
# If prompt has "name. rest", use only the name part
|
|
32
37
|
if [[ "$input" == *". "* ]]; then
|
|
33
38
|
input="${input%%". "*}"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""MCP server for mux — lets Claude manage desks, windows, and notes."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import shlex
|
|
5
6
|
import subprocess
|
|
6
7
|
import sys
|
|
7
8
|
import os
|
|
@@ -192,11 +193,12 @@ def handle_tool(name, args):
|
|
|
192
193
|
elif name == "mux_new":
|
|
193
194
|
prompt = args.get("prompt", "")
|
|
194
195
|
if prompt:
|
|
195
|
-
out, _,
|
|
196
|
-
return out or f"New window '{prompt}' created."
|
|
196
|
+
out, _, rc = run(f'{os.path.expanduser("~/.local/bin/mux")} new {shlex.quote(prompt)} 2>&1')
|
|
197
197
|
else:
|
|
198
|
-
out, _,
|
|
199
|
-
|
|
198
|
+
out, _, rc = run(f'{os.path.expanduser("~/.local/bin/mux")} new 2>&1')
|
|
199
|
+
if rc != 0:
|
|
200
|
+
return f"Failed to open new window: {out or '(no output — check tmux/mux state)'}"
|
|
201
|
+
return out or (f"New window '{prompt}' created." if prompt else "New window created.")
|
|
200
202
|
|
|
201
203
|
elif name == "mux_note_add":
|
|
202
204
|
text = args["text"]
|
|
@@ -236,7 +238,7 @@ def handle_tool(name, args):
|
|
|
236
238
|
elif name == "mux_status":
|
|
237
239
|
project = args.get("project", "")
|
|
238
240
|
if project:
|
|
239
|
-
out, _, _ = run(f'{os.path.expanduser("~/.local/bin/mux")} status
|
|
241
|
+
out, _, _ = run(f'{os.path.expanduser("~/.local/bin/mux")} status {shlex.quote(project)} 2>&1')
|
|
240
242
|
else:
|
|
241
243
|
out, _, _ = run(f'{os.path.expanduser("~/.local/bin/mux")} status 2>&1')
|
|
242
244
|
return out or "No desks open."
|
|
@@ -107,10 +107,10 @@ promotion candidates (specific rules ready for promotion).
|
|
|
107
107
|
|
|
108
108
|
| Step | Owner | When |
|
|
109
109
|
|------|-------|------|
|
|
110
|
-
| Capture + Classify | /debrief | Every session |
|
|
110
|
+
| Capture + Classify | watchtower Ring 3 (or /debrief) | Every session |
|
|
111
111
|
| Pattern analysis | Periodic review | As feedback accumulates |
|
|
112
112
|
| Promotion checkpoint | Review cadence | When patterns mature |
|
|
113
|
-
| Enforcement health | /
|
|
113
|
+
| Enforcement health | /briefing or /pulse | Regular check |
|
|
114
114
|
|
|
115
115
|
## Audit Findings as Promotion Input
|
|
116
116
|
|
|
@@ -17,12 +17,12 @@ write it to memory without asking whether to save it.
|
|
|
17
17
|
`/cc-remember` ensures:
|
|
18
18
|
- The new memory lands at `~/.claude/projects/<slug>/memory/<slug>.md`
|
|
19
19
|
- `MEMORY.md` gets a new entry in the "Curated entries (hand-authored)"
|
|
20
|
-
section, so next session
|
|
20
|
+
section, so the next session can find the memory
|
|
21
21
|
- Filename collisions are handled (suffix with date if needed)
|
|
22
22
|
- The write is atomic (temp + rename — safe under concurrent sessions)
|
|
23
23
|
|
|
24
24
|
Direct flat-`.md` writes work but skip the indexing step. The memory
|
|
25
|
-
becomes invisible to
|
|
25
|
+
becomes invisible to the next session until you manually update `MEMORY.md`.
|
|
26
26
|
The `memory-index-guard` PostToolUse hook will flag this when it
|
|
27
27
|
happens, but it's better to avoid the issue: **use `/cc-remember`**.
|
|
28
28
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// absorbThreadSessions (act:6f93a1f1, N4 of grp:retro-remeaning) — the
|
|
2
|
+
// age-only session-record pruning gate (ABSORPTION_SESSION_AGE_DAYS = 90)
|
|
3
|
+
// is RETIRED. Structural contract under test:
|
|
4
|
+
// A1: an active thread's session record survives a slow-tier run, even when
|
|
5
|
+
// the session is far older than the old 90-day cutoff (the deleted
|
|
6
|
+
// constant no longer exists to prune it).
|
|
7
|
+
// A2: dormant-marking is UNCHANGED — a thread untouched for 30+ days still
|
|
8
|
+
// flips to 'dormant'.
|
|
9
|
+
// A3: the retired constant is gone from the module's exports/source.
|
|
10
|
+
//
|
|
11
|
+
// Hermetic per qa-handoff-gate.test.mjs: WATCHTOWER_DIR points at a temp dir,
|
|
12
|
+
// set BEFORE the dynamic import. No Anthropic calls — absorbThreadSessions
|
|
13
|
+
// touches only the filesystem.
|
|
14
|
+
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import assert from 'node:assert/strict';
|
|
17
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
|
|
21
|
+
const root = mkdtempSync(join(tmpdir(), 'absorption-no-prune-'));
|
|
22
|
+
process.env.WATCHTOWER_DIR = root;
|
|
23
|
+
|
|
24
|
+
const ring2 = await import('../watchtower-ring2.mjs');
|
|
25
|
+
const { absorbThreadSessions, DORMANT_THREAD_AGE_DAYS } = ring2;
|
|
26
|
+
|
|
27
|
+
test.after(() => rmSync(root, { recursive: true, force: true }));
|
|
28
|
+
|
|
29
|
+
function isoDaysAgo(days) {
|
|
30
|
+
return new Date(Date.now() - days * 86400000).toISOString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('A1: an active thread carrying a >90-day-old session record survives a slow-tier run', async () => {
|
|
34
|
+
const threadsDir = join(root, 'state', 'threads');
|
|
35
|
+
mkdirSync(threadsDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const thread = {
|
|
38
|
+
schema_version: 2,
|
|
39
|
+
thread: 'fixture-thread',
|
|
40
|
+
display_name: 'Fixture thread',
|
|
41
|
+
cursor_history: [{ date: isoDaysAgo(2), session_id: 'sess-recent', cursor: { what: 'x', where_left_off: 'y', open_questions: [] } }],
|
|
42
|
+
// Fixture must pin active + recent last_updated, or this hits the
|
|
43
|
+
// dormant branch and the survival assertion is vacuous.
|
|
44
|
+
sessions: [{ id: 'sess-old', project: 'fixture-proj', date: isoDaysAgo(200) }],
|
|
45
|
+
related_fids: [],
|
|
46
|
+
last_updated: isoDaysAgo(2),
|
|
47
|
+
status: 'active',
|
|
48
|
+
};
|
|
49
|
+
writeFileSync(join(threadsDir, 'fixture-thread.json'), JSON.stringify(thread, null, 2));
|
|
50
|
+
|
|
51
|
+
const sessionsDir = join(root, 'state', 'projects', 'fixture-proj', 'sessions');
|
|
52
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
53
|
+
const sessionFilePath = join(sessionsDir, `${isoDaysAgo(200).slice(0, 10)}-sess-old.md`);
|
|
54
|
+
writeFileSync(sessionFilePath, '# Session sess-old\n\nDate: ' + isoDaysAgo(200) + '\n\n- did a thing\n');
|
|
55
|
+
|
|
56
|
+
await absorbThreadSessions();
|
|
57
|
+
|
|
58
|
+
assert.ok(existsSync(sessionFilePath), 'the 200-day-old session record must survive — age-only pruning is retired');
|
|
59
|
+
const stored = JSON.parse(readFileSync(join(threadsDir, 'fixture-thread.json'), 'utf8'));
|
|
60
|
+
assert.equal(stored.status, 'active', 'a recently-updated thread must stay active, not be pruned into dormant');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('A2: dormant-marking is unchanged — a thread untouched 30+ days flips to dormant', async () => {
|
|
64
|
+
const threadsDir = join(root, 'state', 'threads');
|
|
65
|
+
mkdirSync(threadsDir, { recursive: true });
|
|
66
|
+
|
|
67
|
+
const thread = {
|
|
68
|
+
schema_version: 2,
|
|
69
|
+
thread: 'stale-thread',
|
|
70
|
+
display_name: 'Stale thread',
|
|
71
|
+
cursor_history: [],
|
|
72
|
+
sessions: [],
|
|
73
|
+
related_fids: [],
|
|
74
|
+
last_updated: isoDaysAgo(DORMANT_THREAD_AGE_DAYS + 5),
|
|
75
|
+
status: 'active',
|
|
76
|
+
};
|
|
77
|
+
writeFileSync(join(threadsDir, 'stale-thread.json'), JSON.stringify(thread, null, 2));
|
|
78
|
+
|
|
79
|
+
await absorbThreadSessions();
|
|
80
|
+
|
|
81
|
+
const stored = JSON.parse(readFileSync(join(threadsDir, 'stale-thread.json'), 'utf8'));
|
|
82
|
+
assert.equal(stored.status, 'dormant');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('A3: the retired ABSORPTION_SESSION_AGE_DAYS constant is gone', () => {
|
|
86
|
+
assert.equal(ring2.ABSORPTION_SESSION_AGE_DAYS, undefined);
|
|
87
|
+
const src = readFileSync(new URL('../watchtower-ring2.mjs', import.meta.url), 'utf8');
|
|
88
|
+
assert.ok(!src.includes('ABSORPTION_SESSION_AGE_DAYS'));
|
|
89
|
+
});
|
|
@@ -373,9 +373,7 @@ test('R8: the draft-sweep sidecar has a reader — assessRingsAlive surfaces its
|
|
|
373
373
|
last_run: daysAgo(2),
|
|
374
374
|
projects_scanned: 3,
|
|
375
375
|
items_scanned: 41,
|
|
376
|
-
freshness_annotated: 2,
|
|
377
376
|
fold_annotated: 4,
|
|
378
|
-
skipped_projects: [],
|
|
379
377
|
unscanned_items: 0,
|
|
380
378
|
errors: 0,
|
|
381
379
|
}));
|