instar 1.3.991 → 1.3.993

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.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * RecurrenceActuator — turns a recurrence finding into tracked work, once.
3
+ *
4
+ * `RecurrenceReader` makes recurrence VISIBLE. Visibility is not the goal: the
5
+ * project's whole diagnosis is that instar notices constantly and closes almost
6
+ * nothing (filing-to-completion ≈ 30:1). A reader that produces a beautiful
7
+ * report which nobody acts on would be the 30:1 ratio with better typography.
8
+ *
9
+ * Operator directive, 2026-07-26 20:08Z: *"the synthesis itself must lead to
10
+ * ACTION and a fully closed loop"*, with as little user dependence as possible.
11
+ *
12
+ * So this proposes ONE thing, through a path that already exists: for a cluster
13
+ * that genuinely recurs and that nobody has ever turned into work, create a
14
+ * tracked action on the EXISTING evolution action queue. That closes the loop —
15
+ * noticed repeatedly → becomes real work → appears where work is tracked → gets
16
+ * done or explicitly cancelled.
17
+ *
18
+ * WHAT THIS IS NOT:
19
+ * - Not a new notification channel. The single funniest way to fail at fixing
20
+ * "we notice things and never close them" is to build a fourth place that
21
+ * notices things. Nothing here notifies anybody.
22
+ * - Not authority. Creating a tracked action QUEUES work for a human or agent to
23
+ * judge. It does not close, prioritise, escalate, or act on anything.
24
+ * - Not a bulk importer. See "the fix must not become its own pile" below.
25
+ */
26
+ import type { RecurrenceReport } from './RecurrenceReader.js';
27
+ /** A proposed piece of tracked work. The caller performs the actual write. */
28
+ export interface ProposedAction {
29
+ title: string;
30
+ description: string;
31
+ priority: 'critical' | 'high' | 'medium' | 'low';
32
+ /**
33
+ * Stable key derived from the cluster, so a re-run UPDATES rather than
34
+ * duplicates. Without this the actuator would itself become a generator of
35
+ * repeated noticings — the precise disease it treats.
36
+ */
37
+ externalKey: string;
38
+ /** The cluster this came from, for the caller's audit trail. */
39
+ sourceKey: string;
40
+ observedCount: number;
41
+ }
42
+ export type ActuationRefusal = {
43
+ reason: 'actions-store-unreadable';
44
+ detail: string;
45
+ } | {
46
+ reason: 'no-qualifying-clusters';
47
+ detail: string;
48
+ };
49
+ export interface ActuationPlan {
50
+ /** Empty when `refused` is set. */
51
+ propose: ProposedAction[];
52
+ /** Set when the actuator declined to act at all, with why. */
53
+ refused?: ActuationRefusal;
54
+ /** Clusters that qualified but were held back by the per-run cap. */
55
+ deferredByCap: number;
56
+ /** Always present, so a caller can report honestly even on a refusal. */
57
+ consideredClusters: number;
58
+ }
59
+ export interface ActuationOptions {
60
+ /**
61
+ * A thing seen twice is not yet a pattern worth spending a work item on.
62
+ * Default 10: high enough that a proposal is obviously justified by volume.
63
+ */
64
+ minCount?: number;
65
+ /**
66
+ * Hard cap per run. 69 qualifying clusters turned into 69 action items would
67
+ * be a new backlog wearing a different hat — the fix becoming its own pile.
68
+ * A small cap converges across sessions instead, densest-first.
69
+ */
70
+ maxPerRun?: number;
71
+ }
72
+ /**
73
+ * Decide what work to propose from a recurrence report.
74
+ *
75
+ * Pure: it returns a PLAN. The caller writes it, so the write path (and its
76
+ * gating) stays exactly where it already is.
77
+ */
78
+ export declare function planActuation(report: RecurrenceReport, opts?: ActuationOptions): ActuationPlan;
79
+ //# sourceMappingURL=RecurrenceActuator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecurrenceActuator.d.ts","sourceRoot":"","sources":["../../src/core/RecurrenceActuator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAqB,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEjF,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACjD;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,MAAM,EAAE,0BAA0B,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACtD;IAAE,MAAM,EAAE,wBAAwB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,mCAAmC;IACnC,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,8DAA8D;IAC9D,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,qEAAqE;IACrE,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,gBAAgB,EACxB,IAAI,GAAE,gBAAqB,GAC1B,aAAa,CAyDf"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * RecurrenceActuator — turns a recurrence finding into tracked work, once.
3
+ *
4
+ * `RecurrenceReader` makes recurrence VISIBLE. Visibility is not the goal: the
5
+ * project's whole diagnosis is that instar notices constantly and closes almost
6
+ * nothing (filing-to-completion ≈ 30:1). A reader that produces a beautiful
7
+ * report which nobody acts on would be the 30:1 ratio with better typography.
8
+ *
9
+ * Operator directive, 2026-07-26 20:08Z: *"the synthesis itself must lead to
10
+ * ACTION and a fully closed loop"*, with as little user dependence as possible.
11
+ *
12
+ * So this proposes ONE thing, through a path that already exists: for a cluster
13
+ * that genuinely recurs and that nobody has ever turned into work, create a
14
+ * tracked action on the EXISTING evolution action queue. That closes the loop —
15
+ * noticed repeatedly → becomes real work → appears where work is tracked → gets
16
+ * done or explicitly cancelled.
17
+ *
18
+ * WHAT THIS IS NOT:
19
+ * - Not a new notification channel. The single funniest way to fail at fixing
20
+ * "we notice things and never close them" is to build a fourth place that
21
+ * notices things. Nothing here notifies anybody.
22
+ * - Not authority. Creating a tracked action QUEUES work for a human or agent to
23
+ * judge. It does not close, prioritise, escalate, or act on anything.
24
+ * - Not a bulk importer. See "the fix must not become its own pile" below.
25
+ */
26
+ /**
27
+ * Decide what work to propose from a recurrence report.
28
+ *
29
+ * Pure: it returns a PLAN. The caller writes it, so the write path (and its
30
+ * gating) stays exactly where it already is.
31
+ */
32
+ export function planActuation(report, opts = {}) {
33
+ const minCount = opts.minCount ?? 10;
34
+ const maxPerRun = opts.maxPerRun ?? 3;
35
+ // THE REFUSAL, and it is sharper than the reader's.
36
+ //
37
+ // The reader withholds a VERDICT on any partial read. The actuator must
38
+ // withhold the ACTION — but only one missing store actually invalidates the
39
+ // decision, and conflating them would be lazy symmetry:
40
+ //
41
+ // attention/sentinel unreadable → the reader saw FEWER observations. Counts
42
+ // are understated, so a cluster that qualifies still genuinely qualifies.
43
+ // Acting is conservative and safe.
44
+ //
45
+ // ACTIONS unreadable → `tracked` is UNKNOWABLE for every cluster, because
46
+ // `tracked` means "a member came from the action queue". Every cluster
47
+ // would look untracked. Acting would duplicate work that may already
48
+ // exist — the actuator would manufacture the exact redundancy it exists to
49
+ // remove, and do it under the banner of fixing it.
50
+ //
51
+ // So: actions-store unreadable ⇒ propose NOTHING, and say why.
52
+ const actionsUnreadable = report.coverage.unreadable.find((u) => u.store === 'actions');
53
+ if (actionsUnreadable) {
54
+ return {
55
+ propose: [],
56
+ refused: {
57
+ reason: 'actions-store-unreadable',
58
+ detail: `the action queue could not be read (${actionsUnreadable.reason}), so "has anyone already ` +
59
+ 'committed to this?" is unanswerable for every cluster. Proposing work now would duplicate ' +
60
+ 'whatever is already tracked. No actions proposed.',
61
+ },
62
+ deferredByCap: 0,
63
+ consideredClusters: report.clusters.length,
64
+ };
65
+ }
66
+ const qualifying = report.clusters.filter((c) => !c.tracked && c.count >= minCount);
67
+ if (qualifying.length === 0) {
68
+ return {
69
+ propose: [],
70
+ refused: {
71
+ reason: 'no-qualifying-clusters',
72
+ detail: `no untracked cluster reached the minCount=${minCount} threshold`,
73
+ },
74
+ deferredByCap: 0,
75
+ consideredClusters: report.clusters.length,
76
+ };
77
+ }
78
+ const selected = qualifying.slice(0, maxPerRun);
79
+ return {
80
+ propose: selected.map(toProposedAction),
81
+ deferredByCap: qualifying.length - selected.length,
82
+ consideredClusters: report.clusters.length,
83
+ };
84
+ }
85
+ /** Priority from volume alone — deterministic, no model, no judgment. */
86
+ function priorityFor(count) {
87
+ if (count >= 100)
88
+ return 'high';
89
+ if (count >= 25)
90
+ return 'medium';
91
+ return 'low';
92
+ }
93
+ function toProposedAction(c) {
94
+ const span = c.firstSeen && c.lastSeen && c.firstSeen !== c.lastSeen
95
+ ? ` between ${c.firstSeen} and ${c.lastSeen}`
96
+ : '';
97
+ return {
98
+ title: `Recurring, untracked: ${c.exemplar.slice(0, 90)}`,
99
+ description: `Noticed ${c.count} times${span} across ${c.stores.join(', ')}` +
100
+ (c.sources.length ? ` (sources: ${c.sources.slice(0, 5).join(', ')})` : '') +
101
+ `, and never turned into tracked work.\n\n` +
102
+ 'Surfaced by RecurrenceReader, which groups observations across the attention queue, the ' +
103
+ 'action queue and the sentinel log. The individual noticings are each minor and each true; ' +
104
+ 'the VOLUME is the finding.\n\n' +
105
+ 'This action exists so the recurrence is either fixed or deliberately dismissed, rather than ' +
106
+ 'noticed a further N times. Cancelling it IS a valid outcome — an explicit decision beats ' +
107
+ 'silent accumulation.',
108
+ priority: priorityFor(c.count),
109
+ // Stable across runs: the same cluster always maps to the same key, so a
110
+ // second pass updates one row instead of adding another.
111
+ externalKey: `recurrence:${c.key}`,
112
+ sourceKey: c.key,
113
+ observedCount: c.count,
114
+ };
115
+ }
116
+ //# sourceMappingURL=RecurrenceActuator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecurrenceActuator.js","sourceRoot":"","sources":["../../src/core/RecurrenceActuator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * RecurrenceReader — one reader across the stores that already notice things.
3
+ *
4
+ * THE DEFECT THIS ADDRESSES (project convergence-towards-coherence, Tier 2).
5
+ * Instar notices constantly and in three separate places: the attention queue,
6
+ * the evolution action queue, and the sentinel event log. Nothing reads across
7
+ * them, so the same underlying problem is noticed dozens of times and closed
8
+ * zero times. The measured filing-to-completion ratio is roughly 30:1.
9
+ *
10
+ * Measured on this machine, 2026-07-27 — the number that makes the case:
11
+ *
12
+ * 371 OPEN attention items → 49 distinct problems
13
+ * the single largest cluster ("credential rebalancer") is 177 items — 48%
14
+ *
15
+ * Reading that queue item-by-item, nobody would ever see it. The items are all
16
+ * individually true and individually minor; the SHAPE is the finding, and the
17
+ * shape is invisible without a reader that groups.
18
+ *
19
+ * WHAT THIS IS NOT. It is not a new notification channel, and it must never
20
+ * become one (operator directive 2026-07-26 20:08Z: synthesis must lead to
21
+ * ACTION through the paths that already exist — a phase advance, a blocker
22
+ * raised, work queued — with the loop closed and without depending on the user).
23
+ * This module is READ-ONLY and returns a report; driving action from it is a
24
+ * separate, gated concern.
25
+ *
26
+ * THE HONEST-DENOMINATOR RULE APPLIES TO THIS READER TOO. Every store it could
27
+ * not read is named in `coverage`, and a report with any unreadable store is
28
+ * NEVER `complete`. "I found no recurrence" and "I could not look" are different
29
+ * answers, and conflating them is the exact failure this project exists to
30
+ * remove — a synthesiser that silently synthesised over two of three stores
31
+ * would be the most expensive instance of it yet.
32
+ */
33
+ /** A single observation from any of the noticing stores. */
34
+ export interface Observation {
35
+ /** Which store this came from. */
36
+ store: 'attention' | 'actions' | 'sentinel';
37
+ /** Stable id within that store, when it has one. */
38
+ id?: string;
39
+ /** Human title / summary — the recurrence key is derived from this. */
40
+ title: string;
41
+ /** Emitting subsystem, when known. `undefined` is itself a finding (see below). */
42
+ source?: string;
43
+ /** ISO timestamp, when known. */
44
+ at?: string;
45
+ /** Open / unresolved. Only open observations form recurrence clusters. */
46
+ open: boolean;
47
+ }
48
+ /** One recurring problem, as distinct from the many times it was noticed. */
49
+ export interface RecurrenceCluster {
50
+ /** Normalized key the members share. */
51
+ key: string;
52
+ /** A readable exemplar (the first member's title, untruncated). */
53
+ exemplar: string;
54
+ /** How many times this was noticed. THE point of the whole module. */
55
+ count: number;
56
+ /** Which stores noticed it — a problem seen in more than one is stronger evidence. */
57
+ stores: Observation['store'][];
58
+ /** Sources that emitted it, when known. */
59
+ sources: string[];
60
+ /** Earliest / latest observation timestamps available. */
61
+ firstSeen?: string;
62
+ lastSeen?: string;
63
+ /**
64
+ * True when at least one member came from the ACTION queue — i.e. somebody has
65
+ * at some point committed to doing something about this. A high-count cluster
66
+ * with `tracked: false` is the sharpest signal available: noticed many times,
67
+ * never once turned into work.
68
+ */
69
+ tracked: boolean;
70
+ }
71
+ /** What the reader could and could not see. Never omitted, never inferred. */
72
+ export interface Coverage {
73
+ /** Stores read successfully. */
74
+ read: Observation['store'][];
75
+ /** Stores that could NOT be read, with the reason. */
76
+ unreadable: {
77
+ store: Observation['store'];
78
+ reason: string;
79
+ }[];
80
+ /**
81
+ * `complete` only when every store was read. Any unreadable store makes this
82
+ * `partial`, and a partial report may never be presented as a clean bill.
83
+ */
84
+ completeness: 'complete' | 'partial';
85
+ }
86
+ export interface RecurrenceReport {
87
+ generatedAt: string;
88
+ coverage: Coverage;
89
+ /** Distinct problems, densest first. */
90
+ clusters: RecurrenceCluster[];
91
+ /** Open observations considered. */
92
+ observationsConsidered: number;
93
+ /**
94
+ * observations ÷ clusters — the noticing-to-problem ratio. `null` when there
95
+ * are no clusters: no denominator, no ratio (never a flattering 1, never a
96
+ * damning 0).
97
+ */
98
+ noticingRatio: number | null;
99
+ /**
100
+ * Present ONLY on a complete report. On a partial read this is absent and the
101
+ * caller must say "I could not look", never "nothing recurring was found".
102
+ */
103
+ verdict?: 'no-recurrence' | 'recurrence-found';
104
+ }
105
+ /**
106
+ * Derive the recurrence key from a title.
107
+ *
108
+ * Digits collapse to `N` and long hex runs to `H`, so "3 topics stranded on
109
+ * m_cc2ec…" and "7 topics stranded on m_91af…" are recognised as ONE problem
110
+ * rather than two. This is deliberately blunt: the goal is to surface shape, and
111
+ * an over-eager grouping that a human immediately recognises as one problem is
112
+ * more useful than a precise grouping that preserves the illusion of 371
113
+ * separate things.
114
+ */
115
+ export declare function recurrenceKey(title: string): string;
116
+ /**
117
+ * Group open observations into recurrence clusters.
118
+ *
119
+ * Pure over its input — every store read (and every read FAILURE) is the
120
+ * caller's job to supply, so this function cannot silently swallow one.
121
+ */
122
+ export declare function buildRecurrenceReport(observations: Observation[], coverage: Coverage): RecurrenceReport;
123
+ /**
124
+ * The clusters worth a human's attention, by a deterministic rule.
125
+ *
126
+ * `minCount` defaults to 2 because a thing noticed once is not yet recurrence.
127
+ * `untrackedOnly` narrows to the sharpest class: repeatedly noticed, never
128
+ * turned into work.
129
+ */
130
+ export declare function significantClusters(report: RecurrenceReport, opts?: {
131
+ minCount?: number;
132
+ untrackedOnly?: boolean;
133
+ }): RecurrenceCluster[];
134
+ //# sourceMappingURL=RecurrenceReader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecurrenceReader.d.ts","sourceRoot":"","sources":["../../src/core/RecurrenceReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,kCAAkC;IAClC,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,UAAU,CAAC;IAC5C,oDAAoD;IACpD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,mFAAmF;IACnF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iCAAiC;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,IAAI,EAAE,OAAO,CAAC;CACf;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/B,2CAA2C;IAC3C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,8EAA8E;AAC9E,MAAM,WAAW,QAAQ;IACvB,gCAAgC;IAChC,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7B,sDAAsD;IACtD,UAAU,EAAE;QAAE,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9D;;;OAGG;IACH,YAAY,EAAE,UAAU,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;IACnB,wCAAwC;IACxC,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,oCAAoC;IACpC,sBAAsB,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,OAAO,CAAC,EAAE,eAAe,GAAG,kBAAkB,CAAC;CAChD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQnD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,WAAW,EAAE,EAC3B,QAAQ,EAAE,QAAQ,GACjB,gBAAgB,CA0ClB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,gBAAgB,EACxB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACxD,iBAAiB,EAAE,CAKrB"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * RecurrenceReader — one reader across the stores that already notice things.
3
+ *
4
+ * THE DEFECT THIS ADDRESSES (project convergence-towards-coherence, Tier 2).
5
+ * Instar notices constantly and in three separate places: the attention queue,
6
+ * the evolution action queue, and the sentinel event log. Nothing reads across
7
+ * them, so the same underlying problem is noticed dozens of times and closed
8
+ * zero times. The measured filing-to-completion ratio is roughly 30:1.
9
+ *
10
+ * Measured on this machine, 2026-07-27 — the number that makes the case:
11
+ *
12
+ * 371 OPEN attention items → 49 distinct problems
13
+ * the single largest cluster ("credential rebalancer") is 177 items — 48%
14
+ *
15
+ * Reading that queue item-by-item, nobody would ever see it. The items are all
16
+ * individually true and individually minor; the SHAPE is the finding, and the
17
+ * shape is invisible without a reader that groups.
18
+ *
19
+ * WHAT THIS IS NOT. It is not a new notification channel, and it must never
20
+ * become one (operator directive 2026-07-26 20:08Z: synthesis must lead to
21
+ * ACTION through the paths that already exist — a phase advance, a blocker
22
+ * raised, work queued — with the loop closed and without depending on the user).
23
+ * This module is READ-ONLY and returns a report; driving action from it is a
24
+ * separate, gated concern.
25
+ *
26
+ * THE HONEST-DENOMINATOR RULE APPLIES TO THIS READER TOO. Every store it could
27
+ * not read is named in `coverage`, and a report with any unreadable store is
28
+ * NEVER `complete`. "I found no recurrence" and "I could not look" are different
29
+ * answers, and conflating them is the exact failure this project exists to
30
+ * remove — a synthesiser that silently synthesised over two of three stores
31
+ * would be the most expensive instance of it yet.
32
+ */
33
+ /**
34
+ * Derive the recurrence key from a title.
35
+ *
36
+ * Digits collapse to `N` and long hex runs to `H`, so "3 topics stranded on
37
+ * m_cc2ec…" and "7 topics stranded on m_91af…" are recognised as ONE problem
38
+ * rather than two. This is deliberately blunt: the goal is to surface shape, and
39
+ * an over-eager grouping that a human immediately recognises as one problem is
40
+ * more useful than a precise grouping that preserves the illusion of 371
41
+ * separate things.
42
+ */
43
+ export function recurrenceKey(title) {
44
+ return (title || '')
45
+ .toLowerCase()
46
+ .replace(/[a-f0-9]{8,}/g, 'H')
47
+ .replace(/\d+/g, 'N')
48
+ .replace(/\s+/g, ' ')
49
+ .trim()
50
+ .slice(0, 80);
51
+ }
52
+ /**
53
+ * Group open observations into recurrence clusters.
54
+ *
55
+ * Pure over its input — every store read (and every read FAILURE) is the
56
+ * caller's job to supply, so this function cannot silently swallow one.
57
+ */
58
+ export function buildRecurrenceReport(observations, coverage) {
59
+ const open = observations.filter((o) => o.open);
60
+ const byKey = new Map();
61
+ for (const o of open) {
62
+ const k = recurrenceKey(o.title);
63
+ if (!k)
64
+ continue;
65
+ const bucket = byKey.get(k);
66
+ if (bucket)
67
+ bucket.push(o);
68
+ else
69
+ byKey.set(k, [o]);
70
+ }
71
+ const clusters = [...byKey.entries()].map(([key, members]) => {
72
+ const times = members.map((m) => m.at).filter((t) => !!t).sort();
73
+ return {
74
+ key,
75
+ exemplar: members[0].title,
76
+ count: members.length,
77
+ stores: [...new Set(members.map((m) => m.store))],
78
+ sources: [...new Set(members.map((m) => m.source).filter((s) => !!s))],
79
+ firstSeen: times[0],
80
+ lastSeen: times[times.length - 1],
81
+ tracked: members.some((m) => m.store === 'actions'),
82
+ };
83
+ }).sort((a, b) => b.count - a.count);
84
+ const report = {
85
+ generatedAt: new Date().toISOString(),
86
+ coverage,
87
+ clusters,
88
+ observationsConsidered: open.length,
89
+ noticingRatio: clusters.length === 0 ? null : Number((open.length / clusters.length).toFixed(2)),
90
+ };
91
+ // A verdict is only meaningful over a COMPLETE read. On a partial read the
92
+ // field is absent entirely rather than set to a hedged value — an absent field
93
+ // forces the caller to handle it; a hedged value invites it to be rendered as
94
+ // if it were an answer.
95
+ if (coverage.completeness === 'complete') {
96
+ report.verdict = clusters.some((c) => c.count > 1) ? 'recurrence-found' : 'no-recurrence';
97
+ }
98
+ return report;
99
+ }
100
+ /**
101
+ * The clusters worth a human's attention, by a deterministic rule.
102
+ *
103
+ * `minCount` defaults to 2 because a thing noticed once is not yet recurrence.
104
+ * `untrackedOnly` narrows to the sharpest class: repeatedly noticed, never
105
+ * turned into work.
106
+ */
107
+ export function significantClusters(report, opts = {}) {
108
+ const min = opts.minCount ?? 2;
109
+ return report.clusters.filter((c) => c.count >= min && (!opts.untrackedOnly || !c.tracked));
110
+ }
111
+ //# sourceMappingURL=RecurrenceReader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RecurrenceReader.js","sourceRoot":"","sources":["../../src/core/RecurrenceReader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AA2EH;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;SACjB,WAAW,EAAE;SACb,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE;SACN,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CACnC,YAA2B,EAC3B,QAAkB;IAElB,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACjC,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,MAAM;YAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;;YACtB,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACzB,CAAC;IAED,MAAM,QAAQ,GAAwB,CAAC,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,EAAE;QAChF,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9E,OAAO;YACL,GAAG;YACH,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK;YAC1B,KAAK,EAAE,OAAO,CAAC,MAAM;YACrB,MAAM,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;YACjD,OAAO,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACnF,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;YACnB,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;YACjC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC;SACpD,CAAC;IACJ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IAErC,MAAM,MAAM,GAAqB;QAC/B,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACrC,QAAQ;QACR,QAAQ;QACR,sBAAsB,EAAE,IAAI,CAAC,MAAM;QACnC,aAAa,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;KACjG,CAAC;IAEF,2EAA2E;IAC3E,+EAA+E;IAC/E,8EAA8E;IAC9E,wBAAwB;IACxB,IAAI,QAAQ,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QACzC,MAAM,CAAC,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,eAAe,CAAC;IAC5F,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAwB,EACxB,OAAuD,EAAE;IAEzD,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;IAC/B,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAC3B,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAC7D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * recurrenceLoop — the caller that makes the loop actually close.
3
+ *
4
+ * `RecurrenceReader` sees. `RecurrenceActuator` decides. Neither touches a store,
5
+ * which is what keeps both pure and testable — but it also meant "the loop
6
+ * closes" was a DESIGNED property and not a demonstrated one. This is the piece
7
+ * that demonstrates it: it reads the three stores, plans, and writes the result
8
+ * through the caller-supplied action-creation function.
9
+ *
10
+ * It is deliberately the ONLY place in this feature that performs I/O, so every
11
+ * read failure has exactly one place to be reported from and cannot be swallowed
12
+ * somewhere in the middle.
13
+ *
14
+ * Operator directive 2026-07-26 20:08Z: synthesis must lead to ACTION, through
15
+ * paths that already exist, with the loop closed and minimal user dependence.
16
+ */
17
+ import { type Observation, type RecurrenceReport } from './RecurrenceReader.js';
18
+ import { type ActuationOptions, type ProposedAction } from './RecurrenceActuator.js';
19
+ /** Reads one store. Returning a rejected promise is how it reports "unreadable". */
20
+ export type StoreReader = () => Promise<Observation[]>;
21
+ export interface LoopDeps {
22
+ readAttention: StoreReader;
23
+ readActions: StoreReader;
24
+ readSentinel: StoreReader;
25
+ /**
26
+ * Creates ONE tracked action. The caller supplies this so the write goes
27
+ * through whatever path it already uses, with whatever gating that path
28
+ * already has. This module never constructs an HTTP call itself.
29
+ */
30
+ createAction: (a: ProposedAction) => Promise<{
31
+ id: string;
32
+ }>;
33
+ }
34
+ export interface LoopResult {
35
+ report: RecurrenceReport;
36
+ /** Actions actually created, with the ids the store returned. */
37
+ created: {
38
+ id: string;
39
+ title: string;
40
+ observedCount: number;
41
+ }[];
42
+ /** Set when the actuator declined; `created` is then empty. */
43
+ refused?: {
44
+ reason: string;
45
+ detail: string;
46
+ };
47
+ /** Qualifying clusters held back by the per-run cap. */
48
+ deferredByCap: number;
49
+ /**
50
+ * Proposals whose WRITE failed. Distinct from `refused` (a decision not to
51
+ * act) — this is a decision to act that did not land, and conflating the two
52
+ * would hide real breakage behind a principled-looking refusal.
53
+ */
54
+ writeFailures: {
55
+ title: string;
56
+ error: string;
57
+ }[];
58
+ }
59
+ /**
60
+ * Run one pass: read → group → plan → create.
61
+ *
62
+ * Never throws for an unreadable store — that is data, reported in
63
+ * `report.coverage`, and it is what the actuator's refusal reads.
64
+ */
65
+ export declare function runRecurrenceLoop(deps: LoopDeps, opts?: ActuationOptions): Promise<LoopResult>;
66
+ //# sourceMappingURL=recurrenceLoop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recurrenceLoop.d.ts","sourceRoot":"","sources":["../../src/core/recurrenceLoop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAiB,KAAK,gBAAgB,EAAE,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEpG,oFAAoF;AACpF,MAAM,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;AAEvD,MAAM,WAAW,QAAQ;IACvB,aAAa,EAAE,WAAW,CAAC;IAC3B,WAAW,EAAE,WAAW,CAAC;IACzB,YAAY,EAAE,WAAW,CAAC;IAC1B;;;;OAIG;IACH,YAAY,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,gBAAgB,CAAC;IACzB,iEAAiE;IACjE,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAChE,+DAA+D;IAC/D,OAAO,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,wDAAwD;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACnD;AAoBD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,QAAQ,EACd,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,UAAU,CAAC,CA0CrB"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * recurrenceLoop — the caller that makes the loop actually close.
3
+ *
4
+ * `RecurrenceReader` sees. `RecurrenceActuator` decides. Neither touches a store,
5
+ * which is what keeps both pure and testable — but it also meant "the loop
6
+ * closes" was a DESIGNED property and not a demonstrated one. This is the piece
7
+ * that demonstrates it: it reads the three stores, plans, and writes the result
8
+ * through the caller-supplied action-creation function.
9
+ *
10
+ * It is deliberately the ONLY place in this feature that performs I/O, so every
11
+ * read failure has exactly one place to be reported from and cannot be swallowed
12
+ * somewhere in the middle.
13
+ *
14
+ * Operator directive 2026-07-26 20:08Z: synthesis must lead to ACTION, through
15
+ * paths that already exist, with the loop closed and minimal user dependence.
16
+ */
17
+ import { buildRecurrenceReport, } from './RecurrenceReader.js';
18
+ import { planActuation } from './RecurrenceActuator.js';
19
+ /** Read a store, converting failure into a named coverage gap rather than a throw. */
20
+ async function readOrRecord(store, read, into, coverage) {
21
+ try {
22
+ into.push(...(await read()));
23
+ coverage.read.push(store);
24
+ }
25
+ catch (err) {
26
+ coverage.unreadable.push({
27
+ store,
28
+ reason: err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200),
29
+ });
30
+ }
31
+ }
32
+ /**
33
+ * Run one pass: read → group → plan → create.
34
+ *
35
+ * Never throws for an unreadable store — that is data, reported in
36
+ * `report.coverage`, and it is what the actuator's refusal reads.
37
+ */
38
+ export async function runRecurrenceLoop(deps, opts = {}) {
39
+ const observations = [];
40
+ const coverage = { read: [], unreadable: [], completeness: 'complete' };
41
+ await readOrRecord('attention', deps.readAttention, observations, coverage);
42
+ await readOrRecord('actions', deps.readActions, observations, coverage);
43
+ await readOrRecord('sentinel', deps.readSentinel, observations, coverage);
44
+ coverage.completeness = coverage.unreadable.length === 0 ? 'complete' : 'partial';
45
+ const report = buildRecurrenceReport(observations, coverage);
46
+ const plan = planActuation(report, opts);
47
+ if (plan.refused) {
48
+ return {
49
+ report,
50
+ created: [],
51
+ refused: plan.refused,
52
+ deferredByCap: plan.deferredByCap,
53
+ writeFailures: [],
54
+ };
55
+ }
56
+ const created = [];
57
+ const writeFailures = [];
58
+ for (const proposal of plan.propose) {
59
+ try {
60
+ const { id } = await deps.createAction(proposal);
61
+ created.push({ id, title: proposal.title, observedCount: proposal.observedCount });
62
+ }
63
+ catch (err) {
64
+ // A failed write is NOT a refusal. Recording it separately keeps a real
65
+ // outage from reading as a principled decision not to act — which would be
66
+ // this project's own failure mode (absence presenting as presence) at the
67
+ // very end of the loop it exists to close.
68
+ writeFailures.push({
69
+ title: proposal.title,
70
+ error: err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200),
71
+ });
72
+ }
73
+ }
74
+ return { report, created, deferredByCap: plan.deferredByCap, writeFailures };
75
+ }
76
+ //# sourceMappingURL=recurrenceLoop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recurrenceLoop.js","sourceRoot":"","sources":["../../src/core/recurrenceLoop.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.991",
3
+ "version": "1.3.993",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-26T22:54:32.025Z",
5
- "instarVersion": "1.3.991",
4
+ "generatedAt": "2026-07-27T04:10:39.708Z",
5
+ "instarVersion": "1.3.993",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,55 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Instar notices problems in three separate stores — the attention queue, the evolution action queue,
9
+ and the sentinel log — and nothing has ever read across them. So the same problem is noticed dozens
10
+ of times and closed zero times (measured filing-to-completion ≈ 30:1).
11
+
12
+ `src/core/RecurrenceReader.ts` groups OPEN observations from all three into recurrence clusters,
13
+ carrying a `coverage` block that names every store it could NOT read. Pure module, read-only, no
14
+ route, no authority.
15
+
16
+ ## What to Tell Your User
17
+
18
+ Nothing is required of you, and nothing visible changes yet — this ships the engine, not a surface.
19
+
20
+ What it does, in plain terms: your agent writes down the things it notices in three different places,
21
+ and until now nothing looked at all three together. So one recurring problem could be written down a
22
+ hundred times and read as a hundred separate problems. This groups them.
23
+
24
+ On a live agent it turned 2,068 unresolved items into 836 actual problems — and found 69 problems
25
+ that had been noticed 1,242 times between them without a single one ever being picked up. The
26
+ largest was one component's warnings repeating 177 times, nearly half that agent's attention queue.
27
+
28
+ If you later ask your agent "what keeps going wrong?", this is what lets it answer honestly instead
29
+ of reciting a list. And if it can only read some of its records, it will tell you that rather than
30
+ reporting a clean bill — "nothing found" and "couldn't look" stay different answers.
31
+
32
+ ## Summary of New Capabilities
33
+
34
+ - Groups repeated observations across the attention queue, action queue and sentinel log into
35
+ distinct problems, so recurrence becomes visible instead of buried in volume.
36
+ - Flags problems noticed repeatedly that were **never** turned into tracked work — the sharpest
37
+ signal for what is actually being dropped.
38
+ - Refuses to issue a verdict over an incomplete read: any unreadable store is named with its reason,
39
+ and the verdict field is omitted entirely rather than hedged.
40
+
41
+ ## Evidence
42
+
43
+ Live data, 2026-07-27: **2,068 open observations → 836 distinct problems** (ratio 2.47). **69
44
+ problems account for 1,242 noticings and none is tracked.** Largest clusters: 278x idle-timeout
45
+ detection, 238x escalation-suppressed, 177x credential rebalancer (48% of the attention queue).
46
+
47
+ Refusal, on real data with the action store made unreadable: reported the 59 clusters it could see
48
+ and left `verdict` **absent** rather than claiming `no-recurrence`. Unit suite 11/11; `tsc` exit 0.
49
+
50
+ ## Known limits
51
+
52
+ Title-only keying will not merge the same problem worded differently; semantic matching would mean an
53
+ LLM and a judgment point, deliberately avoided. The blunt key can occasionally over-merge — `exemplar`
54
+ and `sources` are carried so a reader spots it. Read-only: it reports, it does not act. Driving
55
+ action through existing gated paths is the next increment, not this one.
@@ -0,0 +1,73 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The recurrence reader (shipped in the previous fragment) makes recurrence visible. Visibility was
9
+ never the goal: a report nobody acts on is the same filing-to-completion ratio with better
10
+ typography. Two modules close the remaining half.
11
+
12
+ `src/core/RecurrenceActuator.ts` turns a recurrence finding into a proposal for ONE tracked action on
13
+ the EXISTING evolution action queue — no new store, no new notification channel, no new authority.
14
+ Creating a tracked action queues work for a human or agent to judge; it does not close, prioritise,
15
+ escalate, or act on anything.
16
+
17
+ `src/core/recurrenceLoop.ts` is the caller that actually closes the loop: it reads the three stores,
18
+ plans, and writes through a caller-supplied action-creation function. It is deliberately the only
19
+ place in the feature that performs I/O, so a read failure has exactly one place to be reported from
20
+ and cannot be swallowed mid-pipeline.
21
+
22
+ ## What to Tell Your User
23
+
24
+ Things you notice repeatedly can now become real work instead of being noticed again.
25
+
26
+ The part shipped earlier counted how often the same underlying problem had been raised. This part
27
+ takes the worst offenders and opens tracked items for them in the queue you already use — noticed
28
+ repeatedly, never owned, becomes a job that gets done or explicitly dropped. Dropping it is a fine
29
+ outcome, and the item says so: an explicit "no, we're fine with this" is worth far more than the same
30
+ thing being raised another two hundred times.
31
+
32
+ It is bounded on purpose. Out of 836 problems in a live dry run it would open three and hold back
33
+ seventeen, working through the backlog over time rather than replacing one pile with another.
34
+
35
+ The honest limit: nothing runs it on a schedule yet, so it closes the loop when something calls it,
36
+ not on its own.
37
+
38
+ ## Summary of New Capabilities
39
+
40
+ No new endpoint, command, or config key. Two internal modules: one that decides what recurring,
41
+ untracked problem deserves a tracked action, and one that performs the read-plan-create pass through
42
+ the action queue that already exists.
43
+
44
+ ## Evidence
45
+
46
+ Full loop run against the three live stores with only the write intercepted: coverage complete, 836
47
+ distinct problems, **would create 3, deferred 17 by the cap, zero write failures**. So "the loop
48
+ closes" is demonstrated, not designed.
49
+
50
+ Two refusals, deliberately **not** symmetric:
51
+
52
+ - **Actions store unreadable ⇒ propose nothing.** `tracked` is unknowable for every cluster, so every
53
+ cluster looks unowned and the actuator would manufacture duplicates of work that already exists —
54
+ the exact redundancy it was built to remove, under the banner of fixing it.
55
+ - **Attention or sentinel unreadable ⇒ proceed.** Those only understate counts, so a cluster still
56
+ clearing the threshold genuinely clears it. Treating all three identically would have looked
57
+ tidier and been wrong.
58
+
59
+ A **failed write** is recorded separately from a **refusal**. One is an outage; the other is a
60
+ decision not to act. Conflating them would let real breakage present as sound judgment. One failed
61
+ write does not abandon the remaining ones.
62
+
63
+ Bounds against the fix becoming its own pile: `minCount` (default 10), a hard per-run cap (default 3)
64
+ so it converges across sessions densest-first rather than turning 69 clusters into 69 items, and a
65
+ stable `externalKey` so a re-run updates instead of duplicating. Unit suite 30/30 across the feature;
66
+ `tsc --noEmit` exit 0.
67
+
68
+ ## Known limits
69
+
70
+ **Nothing schedules it.** The loop can close unattended; nothing yet calls it, so today it closes only
71
+ when something invokes it. That is stated rather than implied. Priority is derived from volume alone
72
+ — deterministic, no model, no judgment — so a rare-but-serious problem ranks below a frequent-but-
73
+ trivial one. It inherits the reader's title-only keying, including its occasional over-merge.
@@ -0,0 +1,130 @@
1
+ # Side-Effects Review — RecurrenceActuator (Tier 2 item 4, plan-only)
2
+
3
+ **Version / slug:** `recurrence-actuator`
4
+ **Date:** `2026-07-27`
5
+ **Author:** `Echo (instar-dev agent)`
6
+
7
+ ## Summary
8
+
9
+ `RecurrenceReader` makes recurrence visible. Visibility was never the goal — the project's diagnosis
10
+ is that instar notices constantly and closes almost nothing (≈30:1). A reader nobody acts on is that
11
+ ratio with better typography.
12
+
13
+ Operator directive 2026-07-26 20:08Z: *"the synthesis itself must lead to ACTION and a fully closed
14
+ loop"*, minimal user dependence.
15
+
16
+ `planActuation()` returns a PLAN: for clusters that genuinely recur AND are untracked, propose work on
17
+ the EXISTING evolution action queue. Pure — the caller performs the write, so the write path and its
18
+ gating stay exactly where they already are.
19
+
20
+ **Live dry-run, 2026-07-27:** 836 clusters considered → **3 proposed, 17 deferred by cap**
21
+ (278x idle-timeout, 238x escalation-suppressed, 177x credential rebalancer — all `high`).
22
+
23
+ ## Refusal evidence (constraint 2)
24
+
25
+ ```
26
+ REFUSAL 1 — actions store unreadable ⇒ propose NOTHING
27
+ refused.reason : actions-store-unreadable
28
+ detail : "…so 'has anyone already committed to this?' is unanswerable for every
29
+ cluster. Proposing work now would duplicate whatever is already tracked."
30
+ propose : [] consideredClusters: 1 ← still honest about scope
31
+
32
+ DELIBERATELY NOT SYMMETRIC — attention/sentinel unreadable ⇒ it DOES act
33
+ Those only UNDERSTATE counts, so a cluster still clearing the bar genuinely clears it.
34
+ Treating all partial reads alike would be lazy symmetry that blocks safe action.
35
+
36
+ REFUSAL 2 — per-run cap: 10 qualifying clusters ⇒ propose 3, deferredByCap 7
37
+ REFUSAL 3 — below threshold: seen 3x with minCount 10 ⇒ no-qualifying-clusters
38
+ REFUSAL 4 — already tracked: a member from the action queue ⇒ propose nothing
39
+ ```
40
+
41
+ Tests **10 passed (10)**; combined with the reader **21 passed (21)**; `tsc --noEmit` exit 0.
42
+
43
+ ## A test caught the documented over-merge risk, live
44
+
45
+ My first cap test built clusters titled `problem 0`…`problem 9` and asserted 3 proposals. It got 1:
46
+ the recurrence key normalizes digits to `N`, so all ten collapsed to one cluster. **The fixture was
47
+ naive, not the code** — and it is a live demonstration of the over-merge trade the reader's
48
+ side-effects review names as its weakest point. Fixed with word-titles and the reason recorded in
49
+ the test.
50
+
51
+ ## Decision-point inventory
52
+
53
+ | point | classification |
54
+ |---|---|
55
+ | actions-unreadable ⇒ refuse | `invariant` — the load-bearing rule |
56
+ | untracked + minCount filter | `invariant` — deterministic thresholds |
57
+ | per-run cap, densest-first | `invariant` |
58
+ | priority from volume | `invariant` — fixed bands, no model |
59
+ | `externalKey` from cluster key | `invariant` — stable, idempotent |
60
+
61
+ No judgment points, no LLM.
62
+
63
+ ## 1. Over-block
64
+
65
+ Refuses entirely when the actions store is unreadable — deliberately, since acting blind creates
66
+ duplicates. Cost: on a broken actions store, nothing is proposed until it is fixed. Correct trade.
67
+
68
+ The `minCount` default of 10 will skip genuine problems seen 4–9 times. Accepted: a work item per
69
+ seen-twice observation is how the queue got to 371 open in the first place.
70
+
71
+ ## 2. Under-block
72
+
73
+ **Nothing prevents the CALLER from ignoring the plan or writing it badly.** This module returns data;
74
+ the write is the caller's. That is the right seam (the write path keeps its own gating) but it means
75
+ "the loop closes" is only true once a caller is wired. No caller ships here — that is the next step
76
+ and is not claimed.
77
+
78
+ **Cancelled actions are not re-proposed-proof.** If a human cancels a proposed action, the cluster
79
+ remains untracked, so a later run could propose it again. The `externalKey` makes it the same row
80
+ rather than a new one, but a "dismissed, stop asking" state belongs to the action store, not here.
81
+ Named as a real gap. <!-- tracked: ACT-1311 -->
82
+
83
+ ## 3. Level-of-abstraction fit
84
+
85
+ Plan-only, pure over the report. It cannot flood, cannot notify, cannot write. The one thing it
86
+ must never become — a fourth place that notices things — is structurally impossible: it has no
87
+ output channel.
88
+
89
+ ## 4. Signal vs authority compliance
90
+
91
+ It proposes; it holds no authority. Creating a tracked action QUEUES work for a human or agent to
92
+ judge — it does not close, prioritise beyond a fixed volume rule, escalate, or act.
93
+
94
+ ## 5. Interactions
95
+
96
+ Consumes `RecurrenceReport` only. No writes, no schema change, no existing caller. Depends on
97
+ `RecurrenceReader` (same branch, PR #1662) — this is stacked on it.
98
+
99
+ ## 6. External surfaces
100
+
101
+ **None.** No route, no config, no persisted state, no user-visible behaviour in this increment.
102
+
103
+ ## 7. Multi-machine posture
104
+
105
+ **Posture: `machine-local`.** `machine-local-justification: physical-credential-locality` — it plans
106
+ over one machine's stores, whose observation titles carry that machine's ids, topics and account
107
+ emails. Cross-machine synthesis would mean replicating those records; the correct route is the
108
+ existing pool-scope fan-out, serving each machine's data from that machine.
109
+
110
+ ## 8. Rollback cost
111
+
112
+ **Zero.** One module, one test file, no callers. Delete removes the feature.
113
+
114
+ ## Phase 5 — Second-pass review
115
+
116
+ No gate/sentinel/watchdog, no block/allow authority, no session lifecycle, no LLM. Author lenses:
117
+
118
+ **Adversarial — "how would I make this useless?"** Let it propose blind when the actions store is
119
+ down (duplicates existing work), or let it propose for everything at once (new backlog). Both are
120
+ asserted refusals.
121
+
122
+ **"Would it have caught the incident?"** The incident is 69 untracked recurrers. It selects exactly
123
+ that class and would open the top three today.
124
+
125
+ **"Symptom or cause?"** Cause for the never-gets-picked-up half. NOT for the recurrence itself —
126
+ proposing work does not fix the 278x idle-timeout; it makes someone decide about it. Claiming
127
+ otherwise would be filing-as-progress.
128
+
129
+ **Weakest point:** no caller is wired, so "the loop closes" is a designed property, not yet a
130
+ demonstrated one. The dry-run shows what it WOULD propose; nothing has been written.
@@ -0,0 +1,112 @@
1
+ # Side-Effects Review — recurrenceLoop (the caller that closes the loop)
2
+
3
+ **Version / slug:** `recurrence-loop` · **Date:** `2026-07-27` · **Author:** `Echo (instar-dev agent)`
4
+
5
+ ## Summary
6
+
7
+ `RecurrenceReader` sees; `RecurrenceActuator` decides. Both are pure, which left **"the loop closes"
8
+ a DESIGNED property, not a demonstrated one** — the weakest point named in the actuator's own review.
9
+ This closes it: read three stores → group → plan → create, via a caller-supplied `createAction`.
10
+
11
+ It is deliberately the ONLY I/O in the feature, so every read failure has exactly one reporting site
12
+ and cannot be swallowed mid-pipeline.
13
+
14
+ **Live run, writes intercepted, 2026-07-27:** coverage `complete` (attention, actions, sentinel);
15
+ 836 problems; **would create 3, deferred 17, 0 write failures, no refusal.**
16
+
17
+ ## Refusal evidence (constraint 2)
18
+
19
+ ```
20
+ REFUSAL — action store unreadable ⇒ createAction NEVER CALLED
21
+ create spy : not called created: 0
22
+ refused.reason : actions-store-unreadable writeFailures: 0
23
+
24
+ A FAILED WRITE IS NOT A REFUSAL (the distinction this module exists to protect)
25
+ createAction throws '503 action store unavailable'
26
+ refused : undefined ← an outage must NOT present as judgement
27
+ writeFailures : [{ error: '503 …' }]
28
+ created : 0
29
+
30
+ one failure among three ⇒ created 2, writeFailures 1 (others not abandoned)
31
+
32
+ UNREADABLE STORE IS DATA, NOT AN EXCEPTION
33
+ sentinel throws ⇒ coverage.unreadable names it, completeness 'partial',
34
+ and it STILL creates 1 (sentinel only understates counts)
35
+ all three throw ⇒ verdict undefined, created 0, no crash
36
+ ```
37
+
38
+ Tests **9 passed (9)**; whole feature **30 passed (30)**; `tsc --noEmit` exit 0.
39
+
40
+ ## Decision-point inventory
41
+
42
+ | point | classification |
43
+ |---|---|
44
+ | read failure → coverage gap | `invariant` — try/catch → named entry, never a throw |
45
+ | refusal vs write-failure separation | `invariant` — distinct fields, the load-bearing rule |
46
+ | write loop continues past a failure | `invariant` |
47
+
48
+ No judgment points, no LLM, no authority: `createAction` is supplied by the caller, so the write path
49
+ keeps whatever gating it already has. This module never constructs an HTTP call.
50
+
51
+ ## 1. Over-block
52
+
53
+ Refuses to write when the actions store is unreadable — inherited from the actuator and correct: it
54
+ cannot tell what is already owned. Cost: a broken actions store stops proposals until fixed.
55
+
56
+ ## 2. Under-block
57
+
58
+ **No scheduler/route calls it.** Running it is deliberate. So this demonstrates the loop CAN close,
59
+ not that it closes *unattended*. Wiring a cadence is a separate increment with its own risk, and is
60
+ not claimed here.
61
+
62
+ **No dismissal memory.** A cancelled action leaves the cluster untracked, so a later run can propose
63
+ it again (same `externalKey`, so one row, not many). A "dismissed, stop asking" state belongs to the
64
+ action store. <!-- tracked: ACT-1311 -->
65
+
66
+ **`createAction` failures are reported, not retried.** Deliberate: retry policy belongs to the write
67
+ path, and a silent retry here would obscure the outage the separate field exists to surface.
68
+
69
+ ## 3. Level-of-abstraction fit
70
+
71
+ All I/O in one place, everything else pure. The alternative — reads scattered through reader and
72
+ actuator — is exactly how a failed read becomes an empty result that reads as "nothing found".
73
+
74
+ ## 4. Signal vs authority
75
+
76
+ Holds none. It executes a plan produced by deterministic rules and writes through a function the
77
+ caller owns.
78
+
79
+ ## 5. Interactions
80
+
81
+ Consumes `RecurrenceReader` + `RecurrenceActuator` (same stack, PRs #1662 / actuator branch). Writes
82
+ only via the injected function. No schema change.
83
+
84
+ ## 6. External surfaces
85
+
86
+ **None.** No route, no config, no persisted state of its own.
87
+
88
+ ## 7. Multi-machine posture
89
+
90
+ **Posture: `machine-local`.** `machine-local-justification: physical-credential-locality` — it reads
91
+ one machine's stores, whose observation titles carry that machine's ids, topics and account emails,
92
+ and writes to that machine's action queue. Cross-machine synthesis would mean replicating those
93
+ records; the existing pool-scope fan-out is the correct route.
94
+
95
+ ## 8. Rollback cost
96
+
97
+ **Zero.** One module, one test file, no callers.
98
+
99
+ ## Phase 5 — Second-pass review
100
+
101
+ No gate/sentinel/watchdog, no block/allow authority, no LLM. Lenses:
102
+
103
+ **Adversarial — "how would I make this useless?"** Report a write outage as a refusal, so breakage
104
+ reads as judgement. Asserted against directly.
105
+
106
+ **"Would it have caught the incident?"** It IS the incident's remedy: 69 untracked recurrers, top
107
+ three actionable today.
108
+
109
+ **"Symptom or cause?"** Cause for never-picked-up. Not for the recurrence itself — creating an item
110
+ makes someone decide about the 278x idle-timeout; it does not fix it.
111
+
112
+ **Weakest point:** nothing schedules it. "Closes unattended" remains unclaimed.
@@ -0,0 +1,149 @@
1
+ # Side-Effects Review — RecurrenceReader (Tier 2 core, read-only)
2
+
3
+ **Version / slug:** `recurrence-reader`
4
+ **Date:** `2026-07-27`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `see Phase 5`
7
+
8
+ ## Summary of the change
9
+
10
+ One pure module (`src/core/RecurrenceReader.ts`) that groups OPEN observations from the attention
11
+ queue, the evolution action queue and the sentinel log into recurrence clusters, plus a `coverage`
12
+ block naming every store it could not read.
13
+
14
+ Project `convergence-towards-coherence` Tier 2. The plan's diagnosis: instar notices constantly, in
15
+ three places, and nothing reads across them — so one problem is noticed dozens of times and closed
16
+ zero times (measured filing-to-completion ≈ 30:1).
17
+
18
+ **Measured on live data, 2026-07-27:**
19
+
20
+ ```
21
+ open observations across 3 stores : 2,068
22
+ distinct problems : 836
23
+ noticing ratio : 2.47
24
+ noticed repeatedly, NEVER tracked : 69 problems / 1,242 noticings
25
+ top clusters: 278x idle-timeout detection
26
+ 238x escalation-suppressed (telegramEscalation disabled)
27
+ 177x credential rebalancer ← 48% of the attention queue alone
28
+ ```
29
+
30
+ ## Refusal evidence (constraint 2)
31
+
32
+ The whole design risk is that a synthesiser becomes a MORE expensive version of the defect it
33
+ detects: reading 2 of 3 stores and reporting "nothing recurring" with the authority of having looked.
34
+
35
+ ```
36
+ REFUSAL — action store made unreadable, on REAL data
37
+ coverage : partial
38
+ could NOT read: [{"store":"actions","reason":"ENOENT: evolution store unreadable"}]
39
+ clusters : 59 ← still reports what it DID see
40
+ verdict : ABSENT — refuses to say no-recurrence
41
+
42
+ THE DISTINCTION THAT MATTERS
43
+ genuinely nothing there → "no-recurrence"
44
+ could not look → undefined (field absent, not hedged)
45
+ ```
46
+
47
+ Unit suite: **11 passed (11)**; `tsc --noEmit` exit 0.
48
+
49
+ ## Decision-point inventory
50
+
51
+ | point | classification | note |
52
+ |---|---|---|
53
+ | recurrence key (digits→N, hex→H) | `invariant` | Deterministic string normalization. No model. |
54
+ | cluster grouping | `invariant` | Map by key. |
55
+ | `verdict` emitted only on complete coverage | `invariant` | The load-bearing rule. |
56
+ | `significantClusters` minCount / untrackedOnly | `invariant` | Caller-supplied thresholds, defaulted, not inferred. |
57
+
58
+ No judgment points, no LLM, nothing gated. The module holds **no authority whatsoever** — it returns
59
+ a report.
60
+
61
+ ## 1. Over-block
62
+
63
+ Nothing is blocked; the module is read-only and returns data. The realistic over-*grouping* risk is
64
+ the blunt key: two genuinely different problems whose titles differ only by digits would merge. That
65
+ is a deliberate trade, stated in the source — surfacing shape is the goal, and an over-eager grouping
66
+ a human instantly recognises as one problem beats a precise grouping that preserves the illusion of
67
+ 371 separate things. `exemplar` and `sources` are carried on every cluster so a reader can spot a bad
68
+ merge immediately.
69
+
70
+ ## 2. Under-block
71
+
72
+ **Title-only keying.** Two reports of the same underlying problem with genuinely different wording
73
+ will not merge. Accepted: the alternative is semantic matching, which means an LLM, which means a
74
+ judgment point in something that currently has none.
75
+
76
+ **`open` is caller-supplied.** The module trusts the caller's open/closed determination per store.
77
+ That is the correct seam — each store knows its own status vocabulary — but it means a caller that
78
+ mis-maps status inflates or deflates the counts. The live harness maps `status === 'OPEN'`,
79
+ `pending|in_progress`, and treats sentinel events as open.
80
+
81
+ **No route yet, no action yet.** This increment is the reader only. Driving action is Tier 2 item 4,
82
+ deliberately separate because it carries authority this does not.
83
+
84
+ ## 3. Level-of-abstraction fit
85
+
86
+ A pure function over supplied observations, with I/O left entirely to the caller. That is
87
+ deliberate: it means the module **cannot** silently swallow a failed store read — the caller must
88
+ hand it a `coverage` block, so an unreadable store is structurally impossible to omit. Putting the
89
+ reads inside would have made "forgot to report the failure" a one-line mistake.
90
+
91
+ ## 4. Signal vs authority compliance
92
+
93
+ Textbook signal-producer. It returns a report and holds zero blocking, gating or notifying authority.
94
+ `docs/signal-vs-authority.md` satisfied — and this is the exact seam the operator flagged: synthesis
95
+ must drive action through EXISTING gated paths, never become a new notification channel. Keeping the
96
+ reader authority-free is what makes that possible later.
97
+
98
+ ## 5. Interactions
99
+
100
+ - **Attention queue / evolution actions / sentinel log** — read-only consumers, no writes, no schema
101
+ change. Nothing else observes this module yet.
102
+ - **Nothing shadows or is shadowed.** New module, no existing caller.
103
+
104
+ ## 6. External surfaces
105
+
106
+ **None in this increment.** No route, no config, no persisted state, no user-visible behaviour. A
107
+ route is the obvious next step and is deliberately not here.
108
+
109
+ ## 6b. Operator-surface quality
110
+
111
+ `coverage.unreadable[].reason` carries the actual failure text so a caller can say *why* it could not
112
+ look, not merely that it could not. `noticingRatio` is `null` — never `0` — when there is no
113
+ denominator, so a client that ignores the contract gets an obviously-missing value rather than a
114
+ plausible wrong one.
115
+
116
+ ## 7. Multi-machine posture
117
+
118
+ **Posture: `machine-local`.** `machine-local-justification: physical-credential-locality` — the three
119
+ stores are per-machine records of what THAT machine noticed, and observation titles routinely carry
120
+ machine ids, topic ids and account emails. Replicating them to synthesise centrally would multiply
121
+ at-rest exposure of that context across every machine. The correct cross-machine read is the existing
122
+ pool-scope fan-out (`?scope=pool`), which serves each machine's own data from that machine — a
123
+ follow-up for whoever adds the route, noted rather than assumed.
124
+
125
+ ## 8. Rollback cost
126
+
127
+ **Zero.** One new module and one new test file, with no callers. Deleting them removes the feature
128
+ entirely; nothing else changes. No persisted state, no migration, no config.
129
+
130
+ ## Phase 5 — Second-pass review
131
+
132
+ Not a gate, sentinel, guard or watchdog; no block/allow authority; no session lifecycle or trust
133
+ surface; no LLM. High-risk trigger list not engaged. Author lenses:
134
+
135
+ **Adversarial — "how would I make this useless?"** By letting it report a clean verdict over a
136
+ partial read. That is the one thing it structurally cannot do, asserted from both directions
137
+ (complete-and-empty → `no-recurrence`; incomplete → field absent) and demonstrated on real data.
138
+
139
+ **"Would it have caught the incident?"** The incident is the project's premise, and yes — 2,068
140
+ noticings collapsing to 836 problems with 69 untracked recurrers is precisely the shape nobody could
141
+ see. It found it on first run.
142
+
143
+ **"Symptom or cause?"** Cause, for the invisibility. NOT for the recurrence itself: this makes the
144
+ 69 untracked recurrers visible, it does not close them. Closing them is item 4, and claiming
145
+ otherwise would be the filing-as-progress failure the project exists to remove.
146
+
147
+ **Weakest point:** the blunt recurrence key. It will occasionally merge two things a human would
148
+ separate. Mitigated by carrying `exemplar` + `sources`, and preferable to under-grouping — but it is
149
+ the assumption most likely to need revisiting once a human reads a real report.