instar 1.3.992 → 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,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.992",
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-27T03:37:34.934Z",
5
- "instarVersion": "1.3.992",
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,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.