@rulemetric/local 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/meta.json +2 -2
- package/dist/server.mjs +244 -190
- package/dist/supabase/migrations/00201_canonical_checkout_total_order.sql +102 -0
- package/dist/supabase/migrations/00202_producers_skip_paths_observed_missing.sql +136 -0
- package/dist/supabase/migrations/00203_auto_accept_admits_silence_declines.sql +120 -0
- package/dist/supabase/migrations/00204_unstale_catalog_skills_on_request.sql +115 -0
- package/dist/supabase/migrations/00205_auto_accept_writes_only_to_canonical_checkouts.sql +110 -0
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
-- 00201 — the canonical checkout stops depending on the query plan.
|
|
2
|
+
--
|
|
3
|
+
-- THE DEFECT (measured against production 2026-08-24).
|
|
4
|
+
--
|
|
5
|
+
-- 00186 replaced five hand-copied `ORDER BY is_worktree ASC, last_seen DESC`
|
|
6
|
+
-- with one view, on the recorded grounds that "the copies disagree in the
|
|
7
|
+
-- silent direction". The view fixed the disagreement between call sites and
|
|
8
|
+
-- introduced a worse one: its ORDER BY is not TOTAL, so when two checkouts of
|
|
9
|
+
-- a project tie on every sort key, `DISTINCT ON` returns whichever row the
|
|
10
|
+
-- executor happens to emit — and that differs by the CALLER's query plan.
|
|
11
|
+
--
|
|
12
|
+
-- Live, same second, same rows, project bbf29181 (local-ai-packaged):
|
|
13
|
+
--
|
|
14
|
+
-- SELECT ... WHERE project_id = $1 -- canonical-checkout.ts:43, :54
|
|
15
|
+
-- -> Sort -> Limit 1
|
|
16
|
+
-- -> /Users/nickyeager/Code/agents/local-ai-packaged (on disk)
|
|
17
|
+
--
|
|
18
|
+
-- LEFT JOIN ... ON c.project_id = p.project_id -- routes/loop.ts:244, :256
|
|
19
|
+
-- -> Incremental Sort (Presorted Key: project_id) -> Unique
|
|
20
|
+
-- -> /Users/.../local-ai-packaged/localai-admin-dashboard (deleted)
|
|
21
|
+
--
|
|
22
|
+
-- Both plans sort on a key set that does not identify a row: the two checkouts
|
|
23
|
+
-- carry the same is_worktree and the same last_seen TO THE MICROSECOND, because
|
|
24
|
+
-- a single backfill statement wrote both. Postgres sorts are not stable and the
|
|
25
|
+
-- two plans use different sort implementations, so each is deterministic on its
|
|
26
|
+
-- own and they disagree with each other.
|
|
27
|
+
--
|
|
28
|
+
-- So `/api/loop/status` renders one path to the operator while the enqueue
|
|
29
|
+
-- sites aim jobs at the other. This is strictly worse than the five copies it
|
|
30
|
+
-- replaced: those were consistently wrong and could be found by diffing them.
|
|
31
|
+
-- This cannot be found by reading the SQL at all.
|
|
32
|
+
--
|
|
33
|
+
-- THE FIX, in two parts.
|
|
34
|
+
--
|
|
35
|
+
-- 1. A TOTAL order. `path` is appended last and `project_checkouts.path` is
|
|
36
|
+
-- globally UNIQUE (00052), so the sort key now identifies exactly one row
|
|
37
|
+
-- and no plan has anything left to decide. This is the load-bearing half:
|
|
38
|
+
-- every rule below it is a preference, this is what makes the view a
|
|
39
|
+
-- function of its inputs.
|
|
40
|
+
--
|
|
41
|
+
-- 2. An ancestor preference, ranked directly under the staleness tier. A
|
|
42
|
+
-- checkout that sits strictly INSIDE another checkout of the SAME project is
|
|
43
|
+
-- a subdirectory somebody opened a session in, not a second checkout of the
|
|
44
|
+
-- repo. Both ties in the entire table are exactly this shape:
|
|
45
|
+
--
|
|
46
|
+
-- /Users/.../local-ai-packaged + /Users/.../local-ai-packaged/localai-admin-dashboard
|
|
47
|
+
-- /tmp/budget-1786799685058 + /tmp/budget-1786799685058/apps/cli
|
|
48
|
+
--
|
|
49
|
+
-- and it is the same shape as the ORIGINAL 00186 defect, whose crown went to
|
|
50
|
+
-- `…/momento-mori/apps/cli`. Preferring the ancestor states the rule that
|
|
51
|
+
-- accident has been standing in for.
|
|
52
|
+
--
|
|
53
|
+
-- Deliberately NOT part of this migration: whether the elected path still
|
|
54
|
+
-- EXISTS. The database cannot see a filesystem, and 6 of the 10 paths this view
|
|
55
|
+
-- currently elects are absent from the operator's machine. That is 00202's job,
|
|
56
|
+
-- and it is a separate defect — fixing this one does not stop a single one of
|
|
57
|
+
-- the no-op jobs, because `enqueue_auto_accept_suggestions` never consulted
|
|
58
|
+
-- this view in the first place.
|
|
59
|
+
--
|
|
60
|
+
-- `left(a, length(b) + 1) = b || '/'` rather than `a LIKE b || '/%'`: an
|
|
61
|
+
-- underscore is a legal character in a path and a wildcard in LIKE, so the
|
|
62
|
+
-- pattern form would rank unrelated siblings as descendants.
|
|
63
|
+
|
|
64
|
+
create or replace view public.project_canonical_checkouts as
|
|
65
|
+
select distinct on (project_id)
|
|
66
|
+
project_id,
|
|
67
|
+
path,
|
|
68
|
+
is_worktree,
|
|
69
|
+
last_seen,
|
|
70
|
+
-- Exposed so callers can SAY the path is stale rather than silently
|
|
71
|
+
-- acting on it. /api/loop/status renders this; a canonical path that
|
|
72
|
+
-- has gone quiet while the project has not is how 00186's defect hid.
|
|
73
|
+
(last_seen < freshest - interval '30 days') as is_stale,
|
|
74
|
+
freshest as project_last_seen
|
|
75
|
+
from (
|
|
76
|
+
select pc.project_id,
|
|
77
|
+
pc.path,
|
|
78
|
+
pc.is_worktree,
|
|
79
|
+
pc.last_seen,
|
|
80
|
+
max(pc.last_seen) over (partition by pc.project_id) as freshest,
|
|
81
|
+
exists (
|
|
82
|
+
select 1
|
|
83
|
+
from public.project_checkouts anc
|
|
84
|
+
where anc.project_id = pc.project_id
|
|
85
|
+
and anc.path <> pc.path
|
|
86
|
+
and left(pc.path, length(anc.path) + 1) = anc.path || '/'
|
|
87
|
+
) as is_inside_another_checkout
|
|
88
|
+
from public.project_checkouts pc
|
|
89
|
+
where pc.path is not null
|
|
90
|
+
and pc.path <> ''
|
|
91
|
+
) q
|
|
92
|
+
order by project_id,
|
|
93
|
+
(last_seen < freshest - interval '30 days') asc, -- live before stale
|
|
94
|
+
is_inside_another_checkout asc, -- repo before subdirectory
|
|
95
|
+
is_worktree asc, -- checkout before worktree
|
|
96
|
+
last_seen desc, -- then most recent
|
|
97
|
+
path asc; -- TOTAL: never let the plan decide
|
|
98
|
+
|
|
99
|
+
comment on view public.project_canonical_checkouts is
|
|
100
|
+
'The single authority for a project''s canonical checkout path. Ranks live paths above ones more than 30 days staler than the project''s freshest, THEN a repo above its own subdirectories, THEN non-worktree, THEN most-recently-seen, THEN path as a TOTAL tiebreak. The final key is not cosmetic: without it (00186) two checkouts tied on every key and DISTINCT ON returned a different winner per caller query plan, so routes/loop.ts read a deleted subdirectory while canonical-checkout.ts read the live repo, for the same project in the same second. See 00201. Says nothing about whether the path exists on disk — see 00202.';
|
|
101
|
+
|
|
102
|
+
grant select on public.project_canonical_checkouts to authenticated, service_role;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
-- 00202 — a producer stops re-enqueueing work against a path the worker has
|
|
2
|
+
-- already told it is not on disk.
|
|
3
|
+
--
|
|
4
|
+
-- THE DEFECT (measured against production 2026-08-24).
|
|
5
|
+
--
|
|
6
|
+
-- `cron_auto_accept_suggestions` stats its target directory before writing
|
|
7
|
+
-- anything — a deliberate guard, so an unattended writer can never recreate a
|
|
8
|
+
-- CLAUDE.md inside a deleted repo. When the directory is gone it returns
|
|
9
|
+
-- `{"skipped":"project-path-missing"}` and the job is marked COMPLETED.
|
|
10
|
+
--
|
|
11
|
+
-- Nothing consumes that observation. So the producer enqueues the same job
|
|
12
|
+
-- again an hour later (00200), the worker stats the same absent directory, and
|
|
13
|
+
-- the job completes clean. Every health view that counts failures sees a
|
|
14
|
+
-- healthy queue while the work never happens — the shape CLAUDE.md already
|
|
15
|
+
-- records for the producers: "a producer matching zero rows looks exactly like
|
|
16
|
+
-- a quiet night."
|
|
17
|
+
--
|
|
18
|
+
-- Live: `/Users/nickyeager/Code/agents/local-ai-packaged/localai-admin-dashboard`
|
|
19
|
+
-- is absent from disk and burned 17 no-op jobs in 24h. There are exactly TWO
|
|
20
|
+
-- paths in the whole system that this producer can enqueue, so half of all
|
|
21
|
+
-- auto-accept jobs were no-ops. The path is carried by ONE open suggestion row
|
|
22
|
+
-- (994feff0, insights-authored, score 0.5, generated 2026-08-08) which can
|
|
23
|
+
-- never be accepted, declined or staled, so this would have run forever.
|
|
24
|
+
--
|
|
25
|
+
-- WHY THIS IS NOT FIXED UPSTREAM. It is tempting to call this a canonical-path
|
|
26
|
+
-- defect, and 00201 does fix the election that first aimed at a subdirectory.
|
|
27
|
+
-- But this producer never consulted `project_canonical_checkouts` at all — it
|
|
28
|
+
-- groups by `instruction_suggestions.project_path` verbatim. Repairing the
|
|
29
|
+
-- election stops zero of these jobs. The two defects share a symptom and
|
|
30
|
+
-- nothing else.
|
|
31
|
+
--
|
|
32
|
+
-- THE RULE: RATE-LIMIT, NEVER BLOCK.
|
|
33
|
+
--
|
|
34
|
+
-- The obvious fix — suppress the path once a missing observation exists — is
|
|
35
|
+
-- the bug this repo keeps shipping. `is_stale` has no inverse. `declined_at` is
|
|
36
|
+
-- permanent for the machine. Both are one-way flags whose owners intended
|
|
37
|
+
-- "not now" and delivered "not ever". A hard skip here is worse than either,
|
|
38
|
+
-- because it is self-sealing: if no job is ever enqueued, no worker ever stats
|
|
39
|
+
-- the directory, so nothing can ever observe that it came back. The flag would
|
|
40
|
+
-- have no inverse BY CONSTRUCTION.
|
|
41
|
+
--
|
|
42
|
+
-- So the observation suppresses for 24 hours and then lets exactly one job
|
|
43
|
+
-- through. Cost: one no-op job a day instead of twenty-four. Recovery: the
|
|
44
|
+
-- directory reappears, the next probe does real work, emits no missing result,
|
|
45
|
+
-- and the path is never suppressed again. No flag, no backfill, no inverse to
|
|
46
|
+
-- forget — the suppression expires on its own because it is a statement about
|
|
47
|
+
-- an observation's age, not a bit somebody has to remember to clear.
|
|
48
|
+
|
|
49
|
+
create or replace function public.project_path_recently_missing(
|
|
50
|
+
p_user_id uuid,
|
|
51
|
+
p_path text,
|
|
52
|
+
p_window interval default interval '24 hours'
|
|
53
|
+
)
|
|
54
|
+
returns boolean
|
|
55
|
+
language sql
|
|
56
|
+
stable
|
|
57
|
+
security definer
|
|
58
|
+
set search_path = public
|
|
59
|
+
as $$
|
|
60
|
+
select exists (
|
|
61
|
+
select 1
|
|
62
|
+
from agent_jobs j
|
|
63
|
+
where j.user_id = p_user_id
|
|
64
|
+
and j.status = 'completed'
|
|
65
|
+
and j.result->>'skipped' = 'project-path-missing'
|
|
66
|
+
and j.payload->>'projectPath' = p_path
|
|
67
|
+
and j.completed_at > now() - p_window
|
|
68
|
+
);
|
|
69
|
+
$$;
|
|
70
|
+
|
|
71
|
+
comment on function public.project_path_recently_missing(uuid, text, interval) is
|
|
72
|
+
'Has a worker reported, within p_window, that this path is not on its disk? The database cannot see a filesystem; the worker is the only thing that can, and it already records the answer in agent_jobs.result. Deliberately a WINDOW and not a flag: a hard skip would stop the probes that are the only way to learn the directory came back, making it a one-way flag by construction — the defect shape of is_stale and declined_at. See 00202.';
|
|
73
|
+
|
|
74
|
+
-- ── The auto-accept producer ────────────────────────────────────────────────
|
|
75
|
+
-- Body unchanged from 00197 except for the `project_path_recently_missing`
|
|
76
|
+
-- clause. Every comment there still applies and is not restated.
|
|
77
|
+
|
|
78
|
+
create or replace function public.enqueue_auto_accept_suggestions()
|
|
79
|
+
returns integer
|
|
80
|
+
language plpgsql
|
|
81
|
+
security definer
|
|
82
|
+
set search_path = public
|
|
83
|
+
as $$
|
|
84
|
+
declare
|
|
85
|
+
queued integer;
|
|
86
|
+
begin
|
|
87
|
+
with eligible as (
|
|
88
|
+
select s.user_id, s.project_path
|
|
89
|
+
from instruction_suggestions s
|
|
90
|
+
join profiles p on p.id = s.user_id
|
|
91
|
+
join instructions i on i.id = s.instruction_id
|
|
92
|
+
left join projects proj on proj.id = s.project_id
|
|
93
|
+
where
|
|
94
|
+
s.kind = 'add'
|
|
95
|
+
and (s.score >= 0.8 or i.frontmatter->>'source' = 'insights')
|
|
96
|
+
and i.type in ('instruction', 'skill')
|
|
97
|
+
and i.archived = false
|
|
98
|
+
and s.accepted_at is null
|
|
99
|
+
and s.dismissed_at is null
|
|
100
|
+
and s.declined_at is null
|
|
101
|
+
and s.is_stale = false
|
|
102
|
+
and coalesce(
|
|
103
|
+
proj.metadata->>'autoAcceptSuggestions',
|
|
104
|
+
p.auto_accept_suggestions::text
|
|
105
|
+
) = 'true'
|
|
106
|
+
-- The worker said this directory is not there. Ask again tomorrow, not
|
|
107
|
+
-- in an hour. See the function's comment for why this expires rather
|
|
108
|
+
-- than latching.
|
|
109
|
+
and not public.project_path_recently_missing(s.user_id, s.project_path)
|
|
110
|
+
and not exists (
|
|
111
|
+
select 1 from agent_jobs j
|
|
112
|
+
where j.task_kind = 'cron_auto_accept_suggestions'
|
|
113
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
114
|
+
and j.payload->>'projectPath' = s.project_path
|
|
115
|
+
and j.user_id = s.user_id
|
|
116
|
+
)
|
|
117
|
+
group by s.user_id, s.project_path
|
|
118
|
+
),
|
|
119
|
+
ins as (
|
|
120
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
121
|
+
select e.user_id,
|
|
122
|
+
'cron_auto_accept_suggestions',
|
|
123
|
+
'pending',
|
|
124
|
+
jsonb_build_object('projectPath', e.project_path),
|
|
125
|
+
'auto-accept:' || e.user_id::text || ':' || e.project_path
|
|
126
|
+
from eligible e
|
|
127
|
+
returning 1
|
|
128
|
+
)
|
|
129
|
+
select count(*)::int into queued from ins;
|
|
130
|
+
|
|
131
|
+
return queued;
|
|
132
|
+
end;
|
|
133
|
+
$$;
|
|
134
|
+
|
|
135
|
+
comment on function public.enqueue_auto_accept_suggestions() is
|
|
136
|
+
'Hourly producer for cron_auto_accept_suggestions. One row per (user, project) holding at least one consented ADD proposal that is either high-confidence (>= 0.8) or insights-authored, whose instruction is a non-archived instruction body OR skill (00197), and whose path a worker has not reported missing from disk in the last 24h (00202 — one dead path burned 17 no-op COMPLETED jobs in 24h, half of every auto-accept job in the system, with nothing reporting a problem). Cheap gates only — the authoritative eligibility check is GET /api/instruction-suggestions/auto-acceptable.';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
-- 00203 — a proposal nobody answered stops blocking the lane that is not asking.
|
|
2
|
+
--
|
|
3
|
+
-- THE DEFECT (measured against production 2026-08-24).
|
|
4
|
+
--
|
|
5
|
+
-- `declined_at` answers two different questions with one column, and every
|
|
6
|
+
-- reader treated them the same:
|
|
7
|
+
--
|
|
8
|
+
-- `surface_budget` — nobody answered. `sweepDeclinedBySilence` stamps it
|
|
9
|
+
-- after a proposal has been shown to 5 distinct sessions with no reply.
|
|
10
|
+
-- A statement about the HUMAN channel: stop asking this person.
|
|
11
|
+
--
|
|
12
|
+
-- `trial_neutral` — it WAS answered, by measurement. `settleExperiment`
|
|
13
|
+
-- stamps it when a randomised trial found no effect. A verdict on the
|
|
14
|
+
-- proposal itself.
|
|
15
|
+
--
|
|
16
|
+
-- The feed already knows the first kind is temporary and re-opens it after 90
|
|
17
|
+
-- days (`notDeclinedBySilence`). The machine lane did not: this producer and
|
|
18
|
+
-- GET /api/instruction-suggestions/auto-acceptable both required `declined_at
|
|
19
|
+
-- IS NULL` strictly, and re-nomination deliberately never clears the column
|
|
20
|
+
-- (00199), so a row that ran out of surface budget was machine-blocked FOREVER
|
|
21
|
+
-- while looking freshly generated.
|
|
22
|
+
--
|
|
23
|
+
-- What that cost, counted: 33 open `add` proposals of an auto-adoptable type at
|
|
24
|
+
-- score >= 0.8, average 0.87, top 0.95 (`antigravity--platform-mirrors`, on
|
|
25
|
+
-- this repo). 27 of them were silence-declined BEFORE 2026-08-20 — that is,
|
|
26
|
+
-- while the skill type-gate (00197) was still shut and the machine could not
|
|
27
|
+
-- have adopted them however loudly it was asked. They spent their surface
|
|
28
|
+
-- budget in an era with no actuator behind them and were locked out for it.
|
|
29
|
+
--
|
|
30
|
+
-- Every `declined_reason` in the entire table today is `surface_budget` (86 of
|
|
31
|
+
-- 86), so on current data this is equivalent to a full amnesty. It is written
|
|
32
|
+
-- as a predicate anyway, because the equivalence is an accident of history and
|
|
33
|
+
-- the rule has to hold when `trial_neutral` rows exist.
|
|
34
|
+
--
|
|
35
|
+
-- WHY A PREDICATE AND NOT AN AMNESTY. `sweepDeclinedBySilence` recomputes
|
|
36
|
+
-- surface units from the WHOLE event log on every feed read — explicitly never
|
|
37
|
+
-- from a checkpoint, so the guard applies to historic proposals for free. Every
|
|
38
|
+
-- one of these rows is therefore permanently over budget. `SET declined_at =
|
|
39
|
+
-- NULL` would be undone by the next SessionStart, quite possibly before the
|
|
40
|
+
-- hourly adoption pass ever saw the row. A predicate mutates nothing and has no
|
|
41
|
+
-- such race.
|
|
42
|
+
--
|
|
43
|
+
-- WHAT STILL BLOCKS. `trial_neutral` (declined by measurement). `dismissed_at`
|
|
44
|
+
-- (a human said no, including via undo) — a separate column, checked
|
|
45
|
+
-- separately, and never touched here. And downstream, the actuator's own
|
|
46
|
+
-- refusals: redundant, partial_overlap, unresolvable_reference, the 30-day
|
|
47
|
+
-- re-offer block, `maxPerWindow` and `maxPerRun`. Opening this gate cannot
|
|
48
|
+
-- flood a CLAUDE.md; it decides what may be ASKED about, not what is written.
|
|
49
|
+
--
|
|
50
|
+
-- The TRIAL lane is deliberately NOT changed (`/instruction-suggestions/
|
|
51
|
+
-- trialable` keeps `declined_at IS NULL`). A trial spends real session capacity
|
|
52
|
+
-- and a scarce slot, so "nobody engaged with this" is a fair reason to leave it
|
|
53
|
+
-- alone there. Auto-accept spends a file write nobody has to read.
|
|
54
|
+
--
|
|
55
|
+
-- Three readers must agree or the operator is shown a different set than the
|
|
56
|
+
-- loop adopts from. The other two are `notDeclinedForActuator` in
|
|
57
|
+
-- routes/instruction-suggestions/shared.ts (used by the auto-acceptable
|
|
58
|
+
-- endpoint) and the auto-accept eligibility count in lib/instructions/
|
|
59
|
+
-- promotion-paths.ts. Both ship in the same change as this migration.
|
|
60
|
+
|
|
61
|
+
create or replace function public.enqueue_auto_accept_suggestions()
|
|
62
|
+
returns integer
|
|
63
|
+
language plpgsql
|
|
64
|
+
security definer
|
|
65
|
+
set search_path = public
|
|
66
|
+
as $$
|
|
67
|
+
declare
|
|
68
|
+
queued integer;
|
|
69
|
+
begin
|
|
70
|
+
with eligible as (
|
|
71
|
+
select s.user_id, s.project_path
|
|
72
|
+
from instruction_suggestions s
|
|
73
|
+
join profiles p on p.id = s.user_id
|
|
74
|
+
join instructions i on i.id = s.instruction_id
|
|
75
|
+
left join projects proj on proj.id = s.project_id
|
|
76
|
+
where
|
|
77
|
+
s.kind = 'add'
|
|
78
|
+
and (s.score >= 0.8 or i.frontmatter->>'source' = 'insights')
|
|
79
|
+
and i.type in ('instruction', 'skill')
|
|
80
|
+
and i.archived = false
|
|
81
|
+
and s.accepted_at is null
|
|
82
|
+
-- A human's refusal. Never relaxed.
|
|
83
|
+
and s.dismissed_at is null
|
|
84
|
+
-- Nobody answering is not a refusal, and this lane is not asking.
|
|
85
|
+
-- `trial_neutral` — declined by measurement — still blocks. See above.
|
|
86
|
+
and (s.declined_at is null or s.declined_reason = 'surface_budget')
|
|
87
|
+
and s.is_stale = false
|
|
88
|
+
and coalesce(
|
|
89
|
+
proj.metadata->>'autoAcceptSuggestions',
|
|
90
|
+
p.auto_accept_suggestions::text
|
|
91
|
+
) = 'true'
|
|
92
|
+
-- The worker said this directory is not there (00202).
|
|
93
|
+
and not public.project_path_recently_missing(s.user_id, s.project_path)
|
|
94
|
+
and not exists (
|
|
95
|
+
select 1 from agent_jobs j
|
|
96
|
+
where j.task_kind = 'cron_auto_accept_suggestions'
|
|
97
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
98
|
+
and j.payload->>'projectPath' = s.project_path
|
|
99
|
+
and j.user_id = s.user_id
|
|
100
|
+
)
|
|
101
|
+
group by s.user_id, s.project_path
|
|
102
|
+
),
|
|
103
|
+
ins as (
|
|
104
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
105
|
+
select e.user_id,
|
|
106
|
+
'cron_auto_accept_suggestions',
|
|
107
|
+
'pending',
|
|
108
|
+
jsonb_build_object('projectPath', e.project_path),
|
|
109
|
+
'auto-accept:' || e.user_id::text || ':' || e.project_path
|
|
110
|
+
from eligible e
|
|
111
|
+
returning 1
|
|
112
|
+
)
|
|
113
|
+
select count(*)::int into queued from ins;
|
|
114
|
+
|
|
115
|
+
return queued;
|
|
116
|
+
end;
|
|
117
|
+
$$;
|
|
118
|
+
|
|
119
|
+
comment on function public.enqueue_auto_accept_suggestions() is
|
|
120
|
+
'Hourly producer for cron_auto_accept_suggestions. One row per (user, project) holding at least one consented ADD proposal that is high-confidence (>= 0.8) or insights-authored, of an auto-adoptable type (00197), not declined BY MEASUREMENT (00203 — a surface_budget decline means nobody answered the human channel, which this lane is not using; 33 adoption-grade proposals, 27 of them declined while the skill gate was still shut, were machine-blocked forever by conflating the two), and whose path a worker has not reported missing from disk in 24h (00202). Cheap gates only — the authoritative check is GET /api/instruction-suggestions/auto-acceptable.';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
-- 00204 — the catalog-skill staleness backlog is released ON REQUEST, one
|
|
2
|
+
-- project at a time. Applying this migration moves ZERO rows.
|
|
3
|
+
--
|
|
4
|
+
-- 00198 cleared the collateral damage from the blanket mark-stale sweep and
|
|
5
|
+
-- explicitly declined to clear catalog skills, reasoning:
|
|
6
|
+
--
|
|
7
|
+
-- "catalog skills — the registry really may be stale, and the scoped sweep
|
|
8
|
+
-- will re-flag them on the next run if it is."
|
|
9
|
+
--
|
|
10
|
+
-- Sound, and wrong by one word: *re*-flag implies the flag can come off, and
|
|
11
|
+
-- nothing could take it off. The only writers of `is_stale = false` were the two
|
|
12
|
+
-- nomination upserts, which clear it for the ONE instruction being re-proposed.
|
|
13
|
+
-- So "the sweep will sort it out" described a sweep that could only ever add.
|
|
14
|
+
--
|
|
15
|
+
-- Measured 2026-08-24, open `add` proposals of type skill at score >= 0.8:
|
|
16
|
+
-- 51 flagged across 12 projects, against exactly ONE that was not. The flag was
|
|
17
|
+
-- not skewing that population, it WAS the population — and the operator's
|
|
18
|
+
-- registry file had been regenerated 17 hours earlier, so every one of those
|
|
19
|
+
-- rows was held out of the adoptable pool by an observation that had stopped
|
|
20
|
+
-- being true. `is_stale = false` is a hard clause in the auto-accept gate.
|
|
21
|
+
--
|
|
22
|
+
-- ── WHY A FUNCTION AND NOT AN UPDATE ────────────────────────────────────────
|
|
23
|
+
--
|
|
24
|
+
-- The inverse shipped alongside this (POST /api/instruction-suggestions/
|
|
25
|
+
-- mark-stale takes `{stale:boolean}`) is the real fix, and it is also a release
|
|
26
|
+
-- valve with no throttle: it clears the WHOLE catalog-skill scope for a user in
|
|
27
|
+
-- one statement. Counted against production, the auto-accept eligible pool moves
|
|
28
|
+
--
|
|
29
|
+
-- today 3 rules, 0 skills
|
|
30
|
+
-- with the decline predicate 3 rules, 8 skills (00203)
|
|
31
|
+
-- with staleness cleared 3 rules, 85 skills
|
|
32
|
+
--
|
|
33
|
+
-- and skills have had no window budget since `maxSkillsPerWindow` was removed on
|
|
34
|
+
-- 2026-08-20 — `windowRemaining` is literally Infinity for a skill at
|
|
35
|
+
-- routes/instruction-suggestions/loop.ts, so the only ceiling left is
|
|
36
|
+
-- `maxPerRun` (3) at an hourly cadence: up to 72 adoptions a day. 00200 accepted
|
|
37
|
+
-- that uncap explicitly and said the duplicate gate was the thing holding the
|
|
38
|
+
-- line, and that if it regressed the cadence was "the first thing to walk back".
|
|
39
|
+
-- An 85-row backlog arriving in a day and a half is exactly the case it meant.
|
|
40
|
+
--
|
|
41
|
+
-- Two further reasons a burst is worse than a trickle, neither of which is about
|
|
42
|
+
-- CLAUDE.md bloat:
|
|
43
|
+
--
|
|
44
|
+
-- * The undo path is per-instruction. Reviewable at 3/day; not at 72/day.
|
|
45
|
+
-- * `skill_usage` (00195) and `retire_unused_skills` (00196) can only tell you
|
|
46
|
+
-- which adoptions earned their place if tranches land far enough apart for
|
|
47
|
+
-- usage to accrue between them. Draining in one burst converts "measured
|
|
48
|
+
-- adoption" back into "bulk install", which is the thing this pipeline
|
|
49
|
+
-- exists to be better than.
|
|
50
|
+
--
|
|
51
|
+
-- So the backlog is released deliberately, per project, by an operator who is
|
|
52
|
+
-- watching:
|
|
53
|
+
--
|
|
54
|
+
-- select public.unstale_catalog_skill_suggestions(
|
|
55
|
+
-- '<user-id>'::uuid, '/Users/me/Code/some-project');
|
|
56
|
+
--
|
|
57
|
+
-- -- or, for every project that user has, in one go:
|
|
58
|
+
-- select public.unstale_catalog_skill_suggestions('<user-id>'::uuid);
|
|
59
|
+
--
|
|
60
|
+
-- ⚠️ The function is NOT the only thing that can release these rows, and that is
|
|
61
|
+
-- worth knowing before relying on it as a brake. A worker running a CLI new
|
|
62
|
+
-- enough to report `{stale:false}` clears the same scope on its next run against
|
|
63
|
+
-- a fresh registry. Until that CLI is published, the pool stays at 11 rows; once
|
|
64
|
+
-- it is, the backlog releases whether or not this function is ever called.
|
|
65
|
+
-- Pacing therefore means pacing the CLI publish too — this function exists so
|
|
66
|
+
-- the release can happen EARLIER and NARROWER than that, not so it can be
|
|
67
|
+
-- prevented.
|
|
68
|
+
--
|
|
69
|
+
-- NOT CLEARED, following 00198:
|
|
70
|
+
-- * anything with `dismissed_at` set — the other `is_stale` writers stamp that
|
|
71
|
+
-- column alongside, so it separates a deliberate retirement from collateral
|
|
72
|
+
-- damage.
|
|
73
|
+
-- * anything accepted, or declined by MEASUREMENT (`trial_neutral`). A
|
|
74
|
+
-- `surface_budget` decline is not an answer and IS cleared, matching the
|
|
75
|
+
-- actuator predicate in 00203 — leaving it out would half-open two gates and
|
|
76
|
+
-- adopt nothing, the exact shape #372 shipped when it opened two of four.
|
|
77
|
+
|
|
78
|
+
create or replace function public.unstale_catalog_skill_suggestions(
|
|
79
|
+
p_user_id uuid,
|
|
80
|
+
p_project_path text default null
|
|
81
|
+
)
|
|
82
|
+
returns integer
|
|
83
|
+
language plpgsql
|
|
84
|
+
security definer
|
|
85
|
+
set search_path = public
|
|
86
|
+
as $$
|
|
87
|
+
declare
|
|
88
|
+
cleared integer;
|
|
89
|
+
begin
|
|
90
|
+
with released as (
|
|
91
|
+
update instruction_suggestions s
|
|
92
|
+
set is_stale = false
|
|
93
|
+
from instructions i
|
|
94
|
+
where i.id = s.instruction_id
|
|
95
|
+
and s.user_id = p_user_id
|
|
96
|
+
and (p_project_path is null or s.project_path = p_project_path)
|
|
97
|
+
and s.is_stale = true
|
|
98
|
+
and i.type = 'skill'
|
|
99
|
+
and coalesce(i.frontmatter->>'source', '') <> 'insights'
|
|
100
|
+
and i.archived = false
|
|
101
|
+
and s.accepted_at is null
|
|
102
|
+
and s.dismissed_at is null
|
|
103
|
+
and (s.declined_at is null or s.declined_reason = 'surface_budget')
|
|
104
|
+
returning 1
|
|
105
|
+
)
|
|
106
|
+
select count(*)::int into cleared from released;
|
|
107
|
+
|
|
108
|
+
return cleared;
|
|
109
|
+
end;
|
|
110
|
+
$$;
|
|
111
|
+
|
|
112
|
+
comment on function public.unstale_catalog_skill_suggestions(uuid, text) is
|
|
113
|
+
'Release the catalog-skill staleness backlog for one user, optionally scoped to one project. Returns how many rows were cleared. Applying 00204 moves zero rows BY DESIGN: 00198 left 51 of these flagged on the reasoning that the sweep would re-flag them, which assumed an inverse that did not exist until 2026-08-24, and clearing them all at once takes the auto-accept eligible pool from 11 to 88 with no window budget on skills to absorb it (maxSkillsPerWindow was removed 2026-08-20; maxPerRun 3 at hourly cadence is up to 72 adoptions/day). Release per project, watching. See 00204.';
|
|
114
|
+
|
|
115
|
+
grant execute on function public.unstale_catalog_skill_suggestions(uuid, text) to service_role;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
-- 00205 — an unattended file writer may only write to a project's canonical
|
|
2
|
+
-- checkout.
|
|
3
|
+
--
|
|
4
|
+
-- THE DEFECT, found 2026-08-24 by releasing the staleness backlog and LOOKING at
|
|
5
|
+
-- what became eligible rather than assuming.
|
|
6
|
+
--
|
|
7
|
+
-- `enqueue_auto_accept_suggestions` groups by `instruction_suggestions.project_path`
|
|
8
|
+
-- verbatim. It has never asked whether that path is a checkout, let alone THE
|
|
9
|
+
-- checkout. While the catalog-skill backlog was stale that was invisible: the
|
|
10
|
+
-- pool held two paths. The moment the backlog was released, the producer's match
|
|
11
|
+
-- set was 13 paths, of which exactly TWO are canonical checkouts:
|
|
12
|
+
--
|
|
13
|
+
-- /Users/nickyeager 14 skills HOME DIRECTORY
|
|
14
|
+
-- /private/var/folders/dn/.../T 3 skills temp directory
|
|
15
|
+
-- /Users/nickyeager/Code/agents/momento-mori/apps/cli 7 skills the dead path from 00186
|
|
16
|
+
-- …/tinyworlds/.worktrees/image-blast-m4, -m6, -m7 13 skills worktrees
|
|
17
|
+
-- /Users/kylechalmers/Development 2 skills a parent of two workspaces
|
|
18
|
+
--
|
|
19
|
+
-- `/Users/nickyeager` is the one that matters most: `acceptSkill` writes under
|
|
20
|
+
-- `<projectPath>/.claude/skills/`, so the home directory means `~/.claude/skills/`
|
|
21
|
+
-- — the GLOBAL skills directory, loaded into every session of every project. An
|
|
22
|
+
-- unattended loop would have installed 14 skills globally and reported success.
|
|
23
|
+
--
|
|
24
|
+
-- 00202 cannot catch any of these. Its question is "is this path on disk", and
|
|
25
|
+
-- all of them are. This is the same defect as 00202's — a producer aiming at a
|
|
26
|
+
-- path nobody validated — wearing a different hat.
|
|
27
|
+
--
|
|
28
|
+
-- THE RULE, and it is not new. `public.project_canonical_checkouts` (00186) is
|
|
29
|
+
-- the single authority for which path IS a project, and CLAUDE.md's invariant is
|
|
30
|
+
-- that it must never be re-derived. `enqueue_instruction_training` has been
|
|
31
|
+
-- gated on it since 00188, whose comment records the identical failure: "the
|
|
32
|
+
-- project-level activity join in 00174 let every subpath of an active repo
|
|
33
|
+
-- through, and apps/cli collected 6 undead trials that way". Auto-accept — which
|
|
34
|
+
-- writes FILES rather than nominating trials — was never given the same gate.
|
|
35
|
+
-- It is the site that needed it most.
|
|
36
|
+
--
|
|
37
|
+
-- CONSEQUENCE, stated rather than buried: this blocks the external user
|
|
38
|
+
-- entirely, because none of his three paths is a canonical checkout. That is the
|
|
39
|
+
-- correct default for an unattended writer (better to write nowhere than into a
|
|
40
|
+
-- directory holding two unrelated workspaces), but it means his skill lane moves
|
|
41
|
+
-- from "dead via is_stale" to "dead via this gate", and the real fix is that his
|
|
42
|
+
-- checkouts are not registered in `project_checkouts`. Tracked separately; do not
|
|
43
|
+
-- widen this gate to paper over it.
|
|
44
|
+
|
|
45
|
+
create or replace function public.enqueue_auto_accept_suggestions()
|
|
46
|
+
returns integer
|
|
47
|
+
language plpgsql
|
|
48
|
+
security definer
|
|
49
|
+
set search_path = public
|
|
50
|
+
as $$
|
|
51
|
+
declare
|
|
52
|
+
queued integer;
|
|
53
|
+
begin
|
|
54
|
+
with eligible as (
|
|
55
|
+
select s.user_id, s.project_path
|
|
56
|
+
from instruction_suggestions s
|
|
57
|
+
join profiles p on p.id = s.user_id
|
|
58
|
+
join instructions i on i.id = s.instruction_id
|
|
59
|
+
left join projects proj on proj.id = s.project_id
|
|
60
|
+
where
|
|
61
|
+
s.kind = 'add'
|
|
62
|
+
and (s.score >= 0.8 or i.frontmatter->>'source' = 'insights')
|
|
63
|
+
and i.type in ('instruction', 'skill')
|
|
64
|
+
and i.archived = false
|
|
65
|
+
and s.accepted_at is null
|
|
66
|
+
and s.dismissed_at is null
|
|
67
|
+
-- Nobody answering is not a refusal (00203).
|
|
68
|
+
and (s.declined_at is null or s.declined_reason = 'surface_budget')
|
|
69
|
+
and s.is_stale = false
|
|
70
|
+
and coalesce(
|
|
71
|
+
proj.metadata->>'autoAcceptSuggestions',
|
|
72
|
+
p.auto_accept_suggestions::text
|
|
73
|
+
) = 'true'
|
|
74
|
+
-- WHERE an unattended writer may write: the project's canonical checkout,
|
|
75
|
+
-- and nowhere else. Not a home directory, not a temp directory, not a
|
|
76
|
+
-- worktree, not a subdirectory somebody once opened a session in. Read
|
|
77
|
+
-- from the view, never re-derived (00186, and CLAUDE.md's invariant).
|
|
78
|
+
and exists (
|
|
79
|
+
select 1 from public.project_canonical_checkouts c
|
|
80
|
+
where c.path = s.project_path
|
|
81
|
+
)
|
|
82
|
+
-- The worker said this directory is not there (00202).
|
|
83
|
+
and not public.project_path_recently_missing(s.user_id, s.project_path)
|
|
84
|
+
and not exists (
|
|
85
|
+
select 1 from agent_jobs j
|
|
86
|
+
where j.task_kind = 'cron_auto_accept_suggestions'
|
|
87
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
88
|
+
and j.payload->>'projectPath' = s.project_path
|
|
89
|
+
and j.user_id = s.user_id
|
|
90
|
+
)
|
|
91
|
+
group by s.user_id, s.project_path
|
|
92
|
+
),
|
|
93
|
+
ins as (
|
|
94
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
95
|
+
select e.user_id,
|
|
96
|
+
'cron_auto_accept_suggestions',
|
|
97
|
+
'pending',
|
|
98
|
+
jsonb_build_object('projectPath', e.project_path),
|
|
99
|
+
'auto-accept:' || e.user_id::text || ':' || e.project_path
|
|
100
|
+
from eligible e
|
|
101
|
+
returning 1
|
|
102
|
+
)
|
|
103
|
+
select count(*)::int into queued from ins;
|
|
104
|
+
|
|
105
|
+
return queued;
|
|
106
|
+
end;
|
|
107
|
+
$$;
|
|
108
|
+
|
|
109
|
+
comment on function public.enqueue_auto_accept_suggestions() is
|
|
110
|
+
'Hourly producer for cron_auto_accept_suggestions. Requires a consented ADD proposal that is high-confidence or insights-authored, of an auto-adoptable type (00197), not declined by MEASUREMENT (00203), on a path that IS the project canonical checkout (00205 — releasing the staleness backlog revealed the match set was 13 paths of which 2 were checkouts; the others were a HOME directory, a temp directory, three worktrees and the dead apps/cli path, and acceptSkill would have written 14 skills into ~/.claude/skills/ globally), and that a worker has not reported missing from disk in 24h (00202).';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulemetric/local",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "RuleMetric local-mode runtime: bundled API server, web dashboard, and database migrations for fully local (zero cloud contact) deployments. Installed on demand by `rulemetric local up` — not meant to be used directly.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"postgres": "^3.4.0",
|
|
29
29
|
"resend": "^4.0.0",
|
|
30
30
|
"zod": "^3.24.0",
|
|
31
|
-
"@rulemetric/skills-registry": "0.12.
|
|
31
|
+
"@rulemetric/skills-registry": "0.12.2"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"esbuild": "^0.25.0",
|