@rulemetric/local 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/meta.json +2 -2
- package/dist/server.mjs +703 -427
- package/dist/supabase/migrations/00087_cleanup_test_data_fix.sql +1 -1
- package/dist/supabase/migrations/00185_trial_throughput_settings.sql +43 -0
- package/dist/supabase/migrations/00186_canonical_checkout_prefers_live.sql +256 -0
- package/dist/supabase/migrations/00187_recommendation_applications.sql +32 -0
- package/dist/supabase/migrations/00188_training_producer_canonical_path.sql +117 -0
- package/dist/supabase/migrations/00189_limit_obs_window_index.sql +37 -0
- package/dist/supabase/migrations/00190_eval_targets_artifact_link.sql +176 -0
- package/dist/supabase/migrations/00191_recommendation_applications_effect_ledger.sql +106 -0
- package/dist/supabase/migrations/00192_cleanup_reaps_memberless_orgs.sql +142 -0
- package/dist/supabase/migrations/00193_judge_segment_pin_and_anchors.sql +367 -0
- package/dist/supabase/migrations/00194_refine_cases_producer.sql +91 -0
- package/dist/supabase/migrations/00195_skill_usage_views.sql +109 -0
- package/dist/supabase/migrations/00196_retire_unused_skills_producer.sql +102 -0
- package/dist/supabase/migrations/00197_auto_accept_producer_admits_skills.sql +115 -0
- package/dist/supabase/migrations/00198_unstale_non_catalog_suggestions.sql +51 -0
- package/dist/supabase/migrations/00199_auto_accept_runs_every_two_hours.sql +54 -0
- package/dist/supabase/migrations/00200_auto_accept_runs_hourly.sql +42 -0
- 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/web/assets/{docs-B_21wyvj.js → docs-KIeJWTb-.js} +1 -1
- package/dist/web/assets/index-CFJatBZ3.css +1 -0
- package/dist/web/assets/{index-OhhQFJmS.js → index-MYof2fnF.js} +54 -54
- package/dist/web/index.html +3 -3
- package/package.json +2 -2
- package/dist/web/assets/index-_M9l6iMX.css +0 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
-- An actuator for the refusal the loop had been printing every night.
|
|
2
|
+
--
|
|
3
|
+
-- `cron_instruction_evolution` has been ending in `{"skipped":"not-adequate"}`
|
|
4
|
+
-- with the reason "3/5 cases do not discriminate with vs without — cull or
|
|
5
|
+
-- replace them before adding runs; more runs of a non-discriminating case
|
|
6
|
+
-- cannot lower the MDE; MDE ±49pp exceeds the ±20pp target effect" on every
|
|
7
|
+
-- night measured (2026-08-16, -17, -18). The remedy shipped in 2026-07 as
|
|
8
|
+
-- `rulemetric evals refine-cases <id> --yes` — a command a human types. So the
|
|
9
|
+
-- nightly loop diagnosed its own blocker correctly, spent a two-arm replay
|
|
10
|
+
-- proving it, and had no way to act.
|
|
11
|
+
--
|
|
12
|
+
-- Slot: 05:30, inside the existing nightly chain and deliberately BEFORE the
|
|
13
|
+
-- 06:10 autorun batch, so replacements generated tonight start collecting depth
|
|
14
|
+
-- tonight rather than waiting a day:
|
|
15
|
+
--
|
|
16
|
+
-- 04:30 suggestions -> 04:45 training -> 05:00 auto-accept
|
|
17
|
+
-- -> 05:30 REFINE -> 06:10 autorun -> 07:30 evolution
|
|
18
|
+
--
|
|
19
|
+
-- Opt-in per target (metadata.autoRefineCases), matching autoEvolveEnabled:
|
|
20
|
+
-- refinement rewrites what a target MEASURES, and a measurement apparatus that
|
|
21
|
+
-- reshapes itself without consent cannot then be trusted to report on itself.
|
|
22
|
+
-- The handler re-reads consent when it claims the job, so revoking it stops an
|
|
23
|
+
-- already-queued run.
|
|
24
|
+
-- ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
create or replace function public.enqueue_refine_cases()
|
|
27
|
+
returns integer
|
|
28
|
+
language plpgsql
|
|
29
|
+
security definer
|
|
30
|
+
set search_path = public
|
|
31
|
+
as $$
|
|
32
|
+
declare
|
|
33
|
+
queued integer;
|
|
34
|
+
begin
|
|
35
|
+
with eligible as (
|
|
36
|
+
select t.id, t.user_id
|
|
37
|
+
from eval_targets t
|
|
38
|
+
where coalesce(t.metadata->>'autoRefineCases', 'false') = 'true'
|
|
39
|
+
-- Discrimination is a CROSS-COHORT quantity: without runs in both arms
|
|
40
|
+
-- there is nothing to diagnose, and the handler would spend an API round
|
|
41
|
+
-- trip to say so. Cheap pre-filter, same answer.
|
|
42
|
+
and exists (
|
|
43
|
+
select 1 from eval_runs r
|
|
44
|
+
where r.eval_target_id = t.id
|
|
45
|
+
and r.status = 'completed'
|
|
46
|
+
and r.configuration = 'with_target'
|
|
47
|
+
)
|
|
48
|
+
and exists (
|
|
49
|
+
select 1 from eval_runs r
|
|
50
|
+
where r.eval_target_id = t.id
|
|
51
|
+
and r.status = 'completed'
|
|
52
|
+
and r.configuration = 'without_target'
|
|
53
|
+
)
|
|
54
|
+
-- There must be something left to measure with. A target already at the
|
|
55
|
+
-- floor cannot afford a retirement, and the handler refuses anyway —
|
|
56
|
+
-- but not enqueuing it keeps the refusal out of the nightly feed.
|
|
57
|
+
and (
|
|
58
|
+
select count(*) from evals ev
|
|
59
|
+
where ev.eval_target_id = t.id and ev.retired_at is null
|
|
60
|
+
) >= 1
|
|
61
|
+
-- One in flight at a time, per the 00083/00086 pattern.
|
|
62
|
+
and not exists (
|
|
63
|
+
select 1 from agent_jobs j
|
|
64
|
+
where j.task_kind = 'cron_refine_cases'
|
|
65
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
66
|
+
and j.payload->>'evalTargetId' = t.id::text
|
|
67
|
+
)
|
|
68
|
+
), ins as (
|
|
69
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
70
|
+
select e.user_id,
|
|
71
|
+
'cron_refine_cases',
|
|
72
|
+
'pending',
|
|
73
|
+
jsonb_build_object('evalTargetId', e.id::text),
|
|
74
|
+
'refine-cases:' || e.id::text
|
|
75
|
+
from eligible e
|
|
76
|
+
returning 1
|
|
77
|
+
)
|
|
78
|
+
select count(*)::int into queued from ins;
|
|
79
|
+
|
|
80
|
+
return queued;
|
|
81
|
+
end;
|
|
82
|
+
$$;
|
|
83
|
+
|
|
84
|
+
comment on function public.enqueue_refine_cases() is
|
|
85
|
+
'Nightly producer for cron_refine_cases (§6e). Enqueues one refinement per OPTED-IN target (metadata.autoRefineCases=true) that has completed runs in BOTH cohorts and at least one live case. Retires cases carrying no treatment signal and regenerates replacements — the actuator for the "N/M cases do not discriminate" refusal the evolution loop printed nightly with no way to act on it. Runs at 05:30, before the 06:10 autorun slot, so tonight''s replacements collect depth tonight. Opt in with: update eval_targets set metadata = coalesce(metadata, ''{}''::jsonb) || ''{"autoRefineCases":true}''::jsonb where id = ...';
|
|
86
|
+
|
|
87
|
+
select cron.schedule(
|
|
88
|
+
'enqueue_refine_cases',
|
|
89
|
+
'30 5 * * *',
|
|
90
|
+
$$select public.enqueue_refine_cases();$$
|
|
91
|
+
);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
-- Skills were being USED and the product could not see it.
|
|
2
|
+
--
|
|
3
|
+
-- Measured 2026-08-18: `session_events` holds 158 `Skill` tool invocations over
|
|
4
|
+
-- 30 days, most recent that day, with the skill name sitting in plain sight at
|
|
5
|
+
-- `tool_input->>'skill'`. Meanwhile `session_instructions` held 0 rows for any
|
|
6
|
+
-- skill, `treatment_exposures` had no `skill` type at all, and of 6,086 skill
|
|
7
|
+
-- instructions 0 had ever been accepted and 0 archived. The signal was captured
|
|
8
|
+
-- and dropped on the floor.
|
|
9
|
+
--
|
|
10
|
+
-- That gap is why the loop has never proposed retiring a skill: grooming
|
|
11
|
+
-- reasons over measured effect, and there was no measurement to reason over.
|
|
12
|
+
--
|
|
13
|
+
-- ## Views, not a table
|
|
14
|
+
--
|
|
15
|
+
-- The obvious shape is a `skill_invocations` table written at ingest. It is the
|
|
16
|
+
-- wrong one here: the event side-effects already exist in TWO mirrored copies
|
|
17
|
+
-- (`routes/sessions/ingest/events.ts` and `lib/ingest-batcher.ts`), and a third
|
|
18
|
+
-- dual-write would be a third thing to keep in sync — plus a backfill to get
|
|
19
|
+
-- wrong. A view over the events cannot drift from its source, needs no
|
|
20
|
+
-- backfill, and is correct for all history the moment it exists.
|
|
21
|
+
--
|
|
22
|
+
-- ## Why this is a stronger signal than anything already recorded
|
|
23
|
+
--
|
|
24
|
+
-- Every existing `session_instructions.link_method` — project_match,
|
|
25
|
+
-- context_file, auto_capture, manual — means the instruction was PRESENT in
|
|
26
|
+
-- context. A Skill invocation means it was USED. §12 spends its length on the
|
|
27
|
+
-- gap between exposure and effect; this is the first record on the right side
|
|
28
|
+
-- of that gap, and it is worth keeping distinct from presence rather than
|
|
29
|
+
-- folding into it.
|
|
30
|
+
-- ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
-- Skill events are a rounding error in session_events (158 of ~30k), so a
|
|
33
|
+
-- partial index keeps the scan proportional to what is actually read.
|
|
34
|
+
create index if not exists idx_session_events_skill_tool
|
|
35
|
+
on public.session_events (session_id, timestamp)
|
|
36
|
+
where tool_name = 'Skill';
|
|
37
|
+
|
|
38
|
+
-- Catalog skills are named `<source-repo>--<skill>`; invocations carry either
|
|
39
|
+
-- the bare `<skill>` or `<plugin>:<skill>`. Matching on the leaf therefore
|
|
40
|
+
-- needs the leaf to be indexable — a `like '%--' || name` predicate cannot use
|
|
41
|
+
-- an index at all, and there are 6,086 skill rows to scan without one.
|
|
42
|
+
create index if not exists idx_instructions_skill_leaf
|
|
43
|
+
on public.instructions ((split_part(name, '--', 2)))
|
|
44
|
+
where type = 'skill';
|
|
45
|
+
|
|
46
|
+
-- ---------------------------------------------------------------------------
|
|
47
|
+
-- Raw invocations, parsed.
|
|
48
|
+
-- ---------------------------------------------------------------------------
|
|
49
|
+
create or replace view public.skill_invocations as
|
|
50
|
+
select
|
|
51
|
+
e.session_id,
|
|
52
|
+
e.timestamp as invoked_at,
|
|
53
|
+
e.tool_input->>'skill' as raw_name,
|
|
54
|
+
-- `plugin:skill` is one namespace form Claude Code emits; a bare name is the
|
|
55
|
+
-- other. Splitting rather than storing the raw string keeps the join below
|
|
56
|
+
-- honest about which half is the identity.
|
|
57
|
+
nullif(split_part(e.tool_input->>'skill', ':', 1), e.tool_input->>'skill')
|
|
58
|
+
as plugin,
|
|
59
|
+
case
|
|
60
|
+
when position(':' in e.tool_input->>'skill') > 0
|
|
61
|
+
then split_part(e.tool_input->>'skill', ':', 2)
|
|
62
|
+
else e.tool_input->>'skill'
|
|
63
|
+
end as skill_name
|
|
64
|
+
from public.session_events e
|
|
65
|
+
where e.tool_name = 'Skill'
|
|
66
|
+
and e.tool_input->>'skill' is not null
|
|
67
|
+
and e.tool_input->>'skill' <> '';
|
|
68
|
+
|
|
69
|
+
comment on view public.skill_invocations is
|
|
70
|
+
'Every Skill tool invocation, parsed out of session_events. A view rather than a table: the event side-effects already exist in two mirrored copies and a third dual-write would be a third thing to keep in sync. Names arrive as `<skill>` or `<plugin>:<skill>`.';
|
|
71
|
+
|
|
72
|
+
-- ---------------------------------------------------------------------------
|
|
73
|
+
-- Invocations resolved to the catalog, WITHOUT discarding the unresolved ones.
|
|
74
|
+
--
|
|
75
|
+
-- Measured 2026-08-18: only 11 of 81 distinct invoked skills exist in the
|
|
76
|
+
-- instructions catalog. The other 70 are the user's own local skills
|
|
77
|
+
-- (`git-ship`, `commit`, `publish`, `auto-deploy`) — the ones actually used
|
|
78
|
+
-- every day, and the ones a catalog-only join would silently drop. An inner
|
|
79
|
+
-- join here would have reported 14% of reality and looked complete.
|
|
80
|
+
--
|
|
81
|
+
-- `instruction_id` is therefore NULLABLE and null means "used but not in the
|
|
82
|
+
-- catalog", which is a finding, not a failure.
|
|
83
|
+
-- ---------------------------------------------------------------------------
|
|
84
|
+
create or replace view public.skill_usage as
|
|
85
|
+
select
|
|
86
|
+
si.session_id,
|
|
87
|
+
si.invoked_at,
|
|
88
|
+
si.raw_name,
|
|
89
|
+
si.plugin,
|
|
90
|
+
si.skill_name,
|
|
91
|
+
s.user_id,
|
|
92
|
+
s.project_id,
|
|
93
|
+
s.project_path,
|
|
94
|
+
m.instruction_id
|
|
95
|
+
from public.skill_invocations si
|
|
96
|
+
join public.sessions s on s.id = si.session_id
|
|
97
|
+
left join lateral (
|
|
98
|
+
select i.id as instruction_id
|
|
99
|
+
from public.instructions i
|
|
100
|
+
where i.type = 'skill'
|
|
101
|
+
and (i.name = si.skill_name or split_part(i.name, '--', 2) = si.skill_name)
|
|
102
|
+
-- Exact name beats a leaf match, so a skill whose full name happens to equal
|
|
103
|
+
-- another's leaf cannot steal the attribution.
|
|
104
|
+
order by (i.name = si.skill_name) desc, i.id
|
|
105
|
+
limit 1
|
|
106
|
+
) m on true;
|
|
107
|
+
|
|
108
|
+
comment on view public.skill_usage is
|
|
109
|
+
'Skill invocations joined to the session that ran them and, where one exists, the catalog instruction. instruction_id is NULL for skills used but not catalogued — measured 2026-08-18, that was 70 of 81 distinct skills, so an inner join here would report 14% of reality.';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
-- The loop's first unattended REMOVAL.
|
|
2
|
+
--
|
|
3
|
+
-- Everything the loop has actuated until now has added: a rule appended to
|
|
4
|
+
-- CLAUDE.md, a skill installed, a hook registered, an instruction promoted.
|
|
5
|
+
-- Grooming's stated contract was that removals are surfaced for human review
|
|
6
|
+
-- and NEVER auto-archived, and this producer deliberately changes that for one
|
|
7
|
+
-- narrow case (owner's decision, 2026-08-18). Recording the change here so the
|
|
8
|
+
-- next reader finds the departure rather than inferring it.
|
|
9
|
+
--
|
|
10
|
+
-- What makes it narrow:
|
|
11
|
+
--
|
|
12
|
+
-- * ONLY skills, and only ones with ZERO recorded invocations since install.
|
|
13
|
+
-- "Invoked but measures unhelpful" is an observational estimate and stays a
|
|
14
|
+
-- proposal, exactly as measured-harmful rules do. An absence of use is a
|
|
15
|
+
-- fact; an absence of benefit is an inference.
|
|
16
|
+
-- * ONLY skills the loop itself installed, evidenced by a
|
|
17
|
+
-- `recommendation_applications` row with status='applied'. The handler
|
|
18
|
+
-- re-checks this; the API is the authority.
|
|
19
|
+
-- * ONLY while skill capture is demonstrably live. A dark Skill channel makes
|
|
20
|
+
-- every skill look unused, and the first quiet fortnight would otherwise
|
|
21
|
+
-- read as "retire everything". Judged by `loadUnusedSkillCandidates`.
|
|
22
|
+
-- * A per-run cap in the handler, and a backup written before every delete
|
|
23
|
+
-- (`applyRetraction`) because an untracked skill file has no other copy and
|
|
24
|
+
-- git cannot restore it.
|
|
25
|
+
--
|
|
26
|
+
-- Slot: 05:45, after auto-accept (05:00) and refine (05:30), before the autorun
|
|
27
|
+
-- batch (06:10). Adding and removing in the same nightly pass, in that order,
|
|
28
|
+
-- means a skill adopted tonight is never a retirement candidate tonight — it
|
|
29
|
+
-- has to survive the unused window first.
|
|
30
|
+
--
|
|
31
|
+
-- 04:30 suggestions -> 04:45 training -> 05:00 auto-accept
|
|
32
|
+
-- -> 05:30 refine -> 05:45 RETIRE -> 06:10 autorun -> 07:30 evolution
|
|
33
|
+
--
|
|
34
|
+
-- Opt-in per project (metadata.autoRetireSkills). Nothing is retired for a user
|
|
35
|
+
-- who has not asked for it, and the handler cannot be reached without a job.
|
|
36
|
+
-- ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
create or replace function public.enqueue_retire_unused_skills()
|
|
39
|
+
returns integer
|
|
40
|
+
language plpgsql
|
|
41
|
+
security definer
|
|
42
|
+
set search_path = public
|
|
43
|
+
as $$
|
|
44
|
+
declare
|
|
45
|
+
queued integer;
|
|
46
|
+
begin
|
|
47
|
+
with eligible as (
|
|
48
|
+
-- The canonical checkout comes from the VIEW, never re-derived: is_worktree
|
|
49
|
+
-- is rewritten from each session's own metadata on every ingest, so a
|
|
50
|
+
-- hand-rolled ranking elects a path nobody works in (00186).
|
|
51
|
+
select pr.created_by as user_id, p.path as canonical_path
|
|
52
|
+
from public.project_canonical_checkouts p
|
|
53
|
+
join public.projects pr on pr.id = p.project_id
|
|
54
|
+
where coalesce(pr.metadata->'autoRetireSkills'->>'enabled', 'false') = 'true'
|
|
55
|
+
-- Cheap pre-filter: a user with no skill invocations captured at all has
|
|
56
|
+
-- a dark channel, and the handler would refuse anyway. Not enqueuing it
|
|
57
|
+
-- keeps a guaranteed refusal out of the nightly feed.
|
|
58
|
+
and exists (
|
|
59
|
+
select 1 from public.skill_usage su
|
|
60
|
+
where su.user_id = pr.created_by
|
|
61
|
+
and su.invoked_at > now() - interval '14 days'
|
|
62
|
+
)
|
|
63
|
+
-- There must be something that could plausibly be retired: at least one
|
|
64
|
+
-- of the user's own skills old enough to have had a fair chance.
|
|
65
|
+
and exists (
|
|
66
|
+
select 1 from public.instructions i
|
|
67
|
+
where i.created_by = pr.created_by
|
|
68
|
+
and i.type = 'skill'
|
|
69
|
+
and i.archived = false
|
|
70
|
+
and i.created_at < now() - interval '30 days'
|
|
71
|
+
)
|
|
72
|
+
-- One in flight at a time, per the 00083/00086 pattern.
|
|
73
|
+
and not exists (
|
|
74
|
+
select 1 from public.agent_jobs j
|
|
75
|
+
where j.task_kind = 'cron_retire_unused_skills'
|
|
76
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
77
|
+
and j.payload->>'projectPath' = p.path
|
|
78
|
+
)
|
|
79
|
+
), ins as (
|
|
80
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
81
|
+
select e.user_id,
|
|
82
|
+
'cron_retire_unused_skills',
|
|
83
|
+
'pending',
|
|
84
|
+
jsonb_build_object('projectPath', e.canonical_path),
|
|
85
|
+
'retire-unused-skills:' || e.canonical_path
|
|
86
|
+
from eligible e
|
|
87
|
+
returning 1
|
|
88
|
+
)
|
|
89
|
+
select count(*)::int into queued from ins;
|
|
90
|
+
|
|
91
|
+
return queued;
|
|
92
|
+
end;
|
|
93
|
+
$$;
|
|
94
|
+
|
|
95
|
+
comment on function public.enqueue_retire_unused_skills() is
|
|
96
|
+
'Nightly producer for cron_retire_unused_skills (05:45). Enqueues one run per OPTED-IN project (projects.metadata.autoRetireSkills.enabled=true) whose user has live skill capture and at least one own skill older than the unused window. The loop''s first unattended REMOVAL: only skills, only never-invoked ones, only ones the loop itself installed, only while skill capture is live, capped per run, and with a backup written before every delete. Used-but-unhelpful skills remain proposals. Opt in with: update projects set metadata = coalesce(metadata, ''{}''::jsonb) || ''{"autoRetireSkills":{"enabled":true}}''::jsonb where id = ...';
|
|
97
|
+
|
|
98
|
+
select cron.schedule(
|
|
99
|
+
'enqueue_retire_unused_skills',
|
|
100
|
+
'45 5 * * *',
|
|
101
|
+
$$select public.enqueue_retire_unused_skills();$$
|
|
102
|
+
);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
-- 00197 — the auto-accept producer admits skills, and the loop can promote again.
|
|
2
|
+
--
|
|
3
|
+
-- #372 ("Skills become adoptable") opened two gates and said two was all there
|
|
4
|
+
-- were: `isAutoAdoptable()` and the operator-facing eligibility count in
|
|
5
|
+
-- `promotion-paths.ts`. There were FOUR. The two it did not touch are the two
|
|
6
|
+
-- that actually decide anything:
|
|
7
|
+
--
|
|
8
|
+
-- 3. GET /api/instruction-suggestions/auto-acceptable — the endpoint the
|
|
9
|
+
-- worker asks for candidates. Opened in the same change as this file.
|
|
10
|
+
-- 4. THIS producer, which decides whether a job is created at all.
|
|
11
|
+
--
|
|
12
|
+
-- Measured 2026-08-19, on the owner's own account:
|
|
13
|
+
--
|
|
14
|
+
-- * Last applied instruction promotion: 2026-08-01. Last auto-accept
|
|
15
|
+
-- adoption: 2026-08-15. Nothing since.
|
|
16
|
+
-- * Every `add` proposal generated from 08-16 onward was type `skill` or
|
|
17
|
+
-- `feature`. Not one `instruction`. On 08-19 alone: 20 skills, 13 of them
|
|
18
|
+
-- at or above the 0.8 score gate, top score 0.95.
|
|
19
|
+
-- * So this function's `eligible` CTE matched zero rows on 08-17, 08-18 and
|
|
20
|
+
-- 08-19. The cron fired and succeeded all three mornings and enqueued
|
|
21
|
+
-- NOTHING — no job, no log line, no refusal anyone could read. The loop
|
|
22
|
+
-- did not report that it was blocked, because from its own point of view
|
|
23
|
+
-- there was nothing to do.
|
|
24
|
+
--
|
|
25
|
+
-- The gate's original comment said "a skill pasted into CLAUDE.md is a category
|
|
26
|
+
-- error". It was right, and it no longer describes the actuator: `acceptSkill`
|
|
27
|
+
-- writes a file under `.claude/skills/` and `.agents/skills/` and never touches
|
|
28
|
+
-- CLAUDE.md. The rule this enforced outlived the behaviour it was protecting
|
|
29
|
+
-- against by exactly as long as it took someone to check.
|
|
30
|
+
--
|
|
31
|
+
-- ── Why no skill budget check here ──────────────────────────────────────────
|
|
32
|
+
--
|
|
33
|
+
-- 00184's comment warns that "queueing a job that can only refuse is how the
|
|
34
|
+
-- loop reported nothing eligible for three days", so it is fair to ask why the
|
|
35
|
+
-- skill window budget is not also checked here. Because the two failure modes
|
|
36
|
+
-- are not the same kind:
|
|
37
|
+
--
|
|
38
|
+
-- * A TYPE ban can never pass. A project whose only proposals are skills is
|
|
39
|
+
-- permanently unreachable, and that is what this migration fixes.
|
|
40
|
+
-- * A BUDGET is transient and self-resetting. A run that refuses on budget
|
|
41
|
+
-- still writes its refusals to the job log with the numbers in them, which
|
|
42
|
+
-- is a stage saying why it did nothing — the thing that was missing above.
|
|
43
|
+
--
|
|
44
|
+
-- Same cheap-gates-only contract as 00170 and 00184: this decides whether to
|
|
45
|
+
-- ASK. The authoritative check, including both window budgets, stays in
|
|
46
|
+
-- GET /api/instruction-suggestions/auto-acceptable.
|
|
47
|
+
|
|
48
|
+
create or replace function public.enqueue_auto_accept_suggestions()
|
|
49
|
+
returns integer
|
|
50
|
+
language plpgsql
|
|
51
|
+
security definer
|
|
52
|
+
set search_path = public
|
|
53
|
+
as $$
|
|
54
|
+
declare
|
|
55
|
+
queued integer;
|
|
56
|
+
begin
|
|
57
|
+
with eligible as (
|
|
58
|
+
select s.user_id, s.project_path
|
|
59
|
+
from instruction_suggestions s
|
|
60
|
+
join profiles p on p.id = s.user_id
|
|
61
|
+
join instructions i on i.id = s.instruction_id
|
|
62
|
+
left join projects proj on proj.id = s.project_id
|
|
63
|
+
where
|
|
64
|
+
s.kind = 'add'
|
|
65
|
+
-- Score OR provenance. Insights nominations are filed at 0.5 by
|
|
66
|
+
-- POST /api/insights/recommendations/nominate and exempted from the
|
|
67
|
+
-- score gate by the authoritative endpoint; requiring 0.8 here made
|
|
68
|
+
-- that exemption unreachable in any project without a high-scoring
|
|
69
|
+
-- catalog proposal. The per-window budget still bounds what a run may
|
|
70
|
+
-- take, so a permissive trigger cannot become a permissive adoption.
|
|
71
|
+
and (s.score >= 0.8 or i.frontmatter->>'source' = 'insights')
|
|
72
|
+
-- The two auto-adoptable types, mirroring `isAutoAdoptable()` and the
|
|
73
|
+
-- endpoint. Executable artifacts (hook, mcp, subagent) and advisory ones
|
|
74
|
+
-- (feature) are deliberately absent: installing those runs code or edits
|
|
75
|
+
-- tool configuration, which always requires a human, and the actuator
|
|
76
|
+
-- refuses them anyway. Widening this must never widen that.
|
|
77
|
+
and i.type in ('instruction', 'skill')
|
|
78
|
+
and i.archived = false
|
|
79
|
+
and s.accepted_at is null
|
|
80
|
+
and s.dismissed_at is null
|
|
81
|
+
and s.declined_at is null
|
|
82
|
+
and s.is_stale = false
|
|
83
|
+
-- Consent: per-project override wins, else the user default (true).
|
|
84
|
+
and coalesce(
|
|
85
|
+
proj.metadata->>'autoAcceptSuggestions',
|
|
86
|
+
p.auto_accept_suggestions::text
|
|
87
|
+
) = 'true'
|
|
88
|
+
-- One in flight per (user, project), per the 00083/00086 pattern.
|
|
89
|
+
and not exists (
|
|
90
|
+
select 1 from agent_jobs j
|
|
91
|
+
where j.task_kind = 'cron_auto_accept_suggestions'
|
|
92
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
93
|
+
and j.payload->>'projectPath' = s.project_path
|
|
94
|
+
and j.user_id = s.user_id
|
|
95
|
+
)
|
|
96
|
+
group by s.user_id, s.project_path
|
|
97
|
+
),
|
|
98
|
+
ins as (
|
|
99
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
100
|
+
select e.user_id,
|
|
101
|
+
'cron_auto_accept_suggestions',
|
|
102
|
+
'pending',
|
|
103
|
+
jsonb_build_object('projectPath', e.project_path),
|
|
104
|
+
'auto-accept:' || e.user_id::text || ':' || e.project_path
|
|
105
|
+
from eligible e
|
|
106
|
+
returning 1
|
|
107
|
+
)
|
|
108
|
+
select count(*)::int into queued from ins;
|
|
109
|
+
|
|
110
|
+
return queued;
|
|
111
|
+
end;
|
|
112
|
+
$$;
|
|
113
|
+
|
|
114
|
+
comment on function public.enqueue_auto_accept_suggestions() is
|
|
115
|
+
'Nightly 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, and whose instruction is a non-archived instruction body OR skill (00197 — skills joined the auto-adoptable set in #372; this producer and the auto-acceptable endpoint were the two gates that change missed, and the loop adopted nothing from 2026-08-16 to 2026-08-19 as a result). Cheap gates only — the authoritative eligibility check is GET /api/instruction-suggestions/auto-acceptable, which applies BOTH window budgets: maxPerWindow for rules (CLAUDE.md bloat) and maxSkillsPerWindow for skills (directory sprawl). Budget overrides: profiles.auto_accept_max_per_window (account) and projects.metadata.autoAcceptLimits (project). Turn it off for one repo: update projects set metadata = coalesce(metadata,''{}''::jsonb) || ''{"autoAcceptSuggestions":false}''::jsonb where id = ...; or globally: update profiles set auto_accept_suggestions = false where id = ...';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- Clear the is_stale flags the blanket mark-stale sweep should never have set.
|
|
2
|
+
--
|
|
3
|
+
-- `POST /api/instruction-suggestions/mark-stale` fires when the LOCAL skills
|
|
4
|
+
-- registry (`~/.config/rulemetric/skills-registry.json`) is older than 48h, or
|
|
5
|
+
-- simply unreadable — `checkSkillsRegistryStale()` catches to `true`. Until
|
|
6
|
+
-- today it ran as `UPDATE instruction_suggestions SET is_stale = true WHERE
|
|
7
|
+
-- user_id = ?`, with no second predicate: the age of one JSON file on the
|
|
8
|
+
-- operator's laptop invalidated every proposal the system had ever made for
|
|
9
|
+
-- that user, of every type and from every producer.
|
|
10
|
+
--
|
|
11
|
+
-- That would be recoverable if anything cleared the flag. Nothing does. The
|
|
12
|
+
-- only writers of `is_stale = false` are the two nomination upserts, and they
|
|
13
|
+
-- clear it for the ONE instruction being re-proposed. Insights nomination
|
|
14
|
+
-- identifies items by content hash, so a re-worded finding files a NEW row
|
|
15
|
+
-- rather than reviving the flagged one. So the sweep was a one-way drain, and
|
|
16
|
+
-- `is_stale = false` is a hard clause in the auto-accept eligibility gate.
|
|
17
|
+
--
|
|
18
|
+
-- Measured 2026-08-20 on the production database, trailing 30 days:
|
|
19
|
+
--
|
|
20
|
+
-- type stale / total
|
|
21
|
+
-- instruction 49 / 49 (100%)
|
|
22
|
+
-- skill 198 / 214 (93%)
|
|
23
|
+
-- feature 43 / 55 (78%)
|
|
24
|
+
-- context_file 26 / 35 (74%)
|
|
25
|
+
--
|
|
26
|
+
-- and the 05:00 auto-accept run reported `{"skipped": "nothing-eligible"}` —
|
|
27
|
+
-- the same shape it reports on a genuinely quiet night. Nothing anywhere said
|
|
28
|
+
-- the pool had been emptied by a file-age check.
|
|
29
|
+
--
|
|
30
|
+
-- The route is now scoped to catalog-backed skill proposals (the only thing the
|
|
31
|
+
-- registry is evidence about); this clears the backlog it left behind.
|
|
32
|
+
--
|
|
33
|
+
-- Deliberately NOT cleared:
|
|
34
|
+
-- * catalog skills — the registry really may be stale, and the scoped sweep
|
|
35
|
+
-- will re-flag them on the next run if it is.
|
|
36
|
+
-- * anything with `dismissed_at` set — the other two `is_stale` writers
|
|
37
|
+
-- (`retire-placeholder-nominations`, the instruction-delete path) always
|
|
38
|
+
-- stamp `dismissed_at` alongside it, so that column separates a deliberate
|
|
39
|
+
-- retirement from collateral damage.
|
|
40
|
+
-- * anything already accepted or declined — those rows have an answer, and
|
|
41
|
+
-- `is_stale` is not what is holding them.
|
|
42
|
+
|
|
43
|
+
UPDATE instruction_suggestions s
|
|
44
|
+
SET is_stale = false
|
|
45
|
+
FROM instructions i
|
|
46
|
+
WHERE i.id = s.instruction_id
|
|
47
|
+
AND s.is_stale = true
|
|
48
|
+
AND s.dismissed_at IS NULL
|
|
49
|
+
AND s.accepted_at IS NULL
|
|
50
|
+
AND s.declined_at IS NULL
|
|
51
|
+
AND NOT (i.type = 'skill' AND coalesce(i.frontmatter->>'source', '') <> 'insights');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- 00199 — auto-accept stops running once a day and starts running every two hours.
|
|
2
|
+
--
|
|
3
|
+
-- This is a LATENCY fix, not a throughput one — for RULES. Rule adoption stays
|
|
4
|
+
-- bounded by `maxPerWindow` (15 per project per 7 days) in
|
|
5
|
+
-- GET /api/instruction-suggestions/auto-acceptable, so twelve runs a day cannot
|
|
6
|
+
-- adopt more rules than one run a day could over a week; they can only adopt
|
|
7
|
+
-- SOONER.
|
|
8
|
+
--
|
|
9
|
+
-- CORRECTION (2026-08-20, same PR): this comment originally also cited
|
|
10
|
+
-- `maxSkillsPerWindow` (5), which that PR REMOVES — skills now return
|
|
11
|
+
-- `skillsMax: null` and are bounded only by `maxPerRun` (3), counted per type.
|
|
12
|
+
-- So for skills this cadence change IS a throughput change: 3 x 12 = up to 36 a
|
|
13
|
+
-- day where the ceiling was 5 a week. The deliberate argument for uncapping is
|
|
14
|
+
-- that skills load on demand and so do not carry the measured -22.1% cost of an
|
|
15
|
+
-- over-large CLAUDE.md, which is what `maxPerWindow` exists to bound. What holds
|
|
16
|
+
-- the line instead is the duplicate gate in `readInstalledSkills` /
|
|
17
|
+
-- `assessSkillDuplication` — which means that gate must see the WHOLE disk, both
|
|
18
|
+
-- `.agents/skills` and `.claude/skills`. It did not when this migration was
|
|
19
|
+
-- written; that is fixed in the same PR.
|
|
20
|
+
--
|
|
21
|
+
-- Sooner is the entire problem. A proposal is permanently declined once it has
|
|
22
|
+
-- been surfaced in 5 distinct session-hours (`SURFACE_BUDGET`, the
|
|
23
|
+
-- `declined_by_silence` sweep in instruction-suggestions/shared.ts), and
|
|
24
|
+
-- `declined_at` is deliberately never cleared by re-nomination. The owner opens
|
|
25
|
+
-- ~12 substantial sessions a day, so a proposal burns its budget well inside one
|
|
26
|
+
-- working day — while the only pass that could adopt it ran at 05:00 and took at
|
|
27
|
+
-- most 3.
|
|
28
|
+
--
|
|
29
|
+
-- The two clocks were incompatible, and the scoreboard says so: 59 proposals
|
|
30
|
+
-- declined by silence against 34 ever accepted. Measured 2026-08-20, this repo
|
|
31
|
+
-- had FIVE skills at or above the score gate and adopted none — four already
|
|
32
|
+
-- carried `declined_at` from the sweep, stamped before 05:00 ever came round.
|
|
33
|
+
--
|
|
34
|
+
-- Safe to run this often for two independent reasons:
|
|
35
|
+
-- * The producer's `not exists (... status in ('pending','claimed','running'))`
|
|
36
|
+
-- clause admits one job per (user, project) at a time, so runs cannot pile
|
|
37
|
+
-- up. `idx_agent_jobs_dedupe` is a plain lookup index, not a unique one, so
|
|
38
|
+
-- a COMPLETED job correctly does not block the next run.
|
|
39
|
+
-- * A run with nothing to do costs one producer query and enqueues nothing.
|
|
40
|
+
--
|
|
41
|
+
-- Cadence chosen over hourly deliberately: two hours is comfortably inside the
|
|
42
|
+
-- window in which a proposal burns 5 session-hours, and leaves the nightly
|
|
43
|
+
-- ordering (generate 04:30 → adopt) intact rather than interleaving with it
|
|
44
|
+
-- twenty-four times.
|
|
45
|
+
--
|
|
46
|
+
-- SUPERSEDED the same day by 00200, which goes hourly: "comfortably inside the
|
|
47
|
+
-- window" held for the average day and not for a morning burst, which is where
|
|
48
|
+
-- proposals were being lost. Read 00200 for the reasoning that replaced this
|
|
49
|
+
-- paragraph — this file is left as it ran.
|
|
50
|
+
|
|
51
|
+
select cron.alter_job(
|
|
52
|
+
(select jobid from cron.job where jobname = 'enqueue_auto_accept_suggestions'),
|
|
53
|
+
schedule => '0 */2 * * *'
|
|
54
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
-- 00200 — auto-accept goes from every two hours to hourly.
|
|
2
|
+
--
|
|
3
|
+
-- This REVERSES the cadence choice 00199 made eight hours earlier, deliberately
|
|
4
|
+
-- and with the owner's decision on 2026-08-20. 00199's reasoning was:
|
|
5
|
+
--
|
|
6
|
+
-- "Cadence chosen over hourly deliberately: two hours is comfortably inside
|
|
7
|
+
-- the window in which a proposal burns 5 session-hours, and leaves the
|
|
8
|
+
-- nightly ordering (generate 04:30 → adopt) intact rather than interleaving
|
|
9
|
+
-- with it twenty-four times."
|
|
10
|
+
--
|
|
11
|
+
-- Two hours is *inside* that window on an average day, which is the weakest
|
|
12
|
+
-- form of the claim: it depends on the owner opening ~12 substantial sessions a
|
|
13
|
+
-- day spread evenly, and they are not spread evenly. A morning burst of five
|
|
14
|
+
-- sessions exhausts `SURFACE_BUDGET` before a two-hourly pass ever looks, and
|
|
15
|
+
-- the proposal is `declined_by_silence` without a human having declined
|
|
16
|
+
-- anything. Hourly halves the worst case rather than the average one, which is
|
|
17
|
+
-- the case that was actually losing proposals.
|
|
18
|
+
--
|
|
19
|
+
-- The interleaving objection stands and is accepted rather than answered: the
|
|
20
|
+
-- 04:30 generate → adopt ordering now has an adoption pass inside it. That is
|
|
21
|
+
-- safe for the same reason 00199 gave for running at all — a pass with nothing
|
|
22
|
+
-- eligible costs one producer query and enqueues nothing — and the producer's
|
|
23
|
+
-- `not exists (... status in ('pending','claimed','running'))` clause still
|
|
24
|
+
-- admits one job per (user, project) at a time, so a slow run cannot pile up
|
|
25
|
+
-- behind a fast schedule.
|
|
26
|
+
--
|
|
27
|
+
-- THROUGHPUT, stated plainly because 00199's original comment got this wrong:
|
|
28
|
+
-- rules stay bounded by `maxPerWindow` (15 per project per 7 days), so hourly
|
|
29
|
+
-- changes only WHEN a rule is adopted, never how many. Skills have no window
|
|
30
|
+
-- cap since `maxSkillsPerWindow` was removed, so for skills this is a real
|
|
31
|
+
-- throughput increase: `maxPerRun` (3) × 24 = up to 72 a day, from 36. What
|
|
32
|
+
-- bounds skills instead is the duplicate gate (`readInstalledSkills` +
|
|
33
|
+
-- `assessSkillDuplication`), which as of the same PR reads BOTH `.agents/skills`
|
|
34
|
+
-- and `.claude/skills` — on the owner's checkout that is 19 installed skills
|
|
35
|
+
-- visible where the gate previously saw 2. A ceiling that depends on a gate is
|
|
36
|
+
-- only as good as the gate's coverage, so if that regresses, this cadence is
|
|
37
|
+
-- the first thing to walk back.
|
|
38
|
+
|
|
39
|
+
select cron.alter_job(
|
|
40
|
+
(select jobid from cron.job where jobname = 'enqueue_auto_accept_suggestions'),
|
|
41
|
+
schedule => '0 * * * *'
|
|
42
|
+
);
|