@sigrun/ext-curator 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The targets step (`src/steps/targets.ts`, Task B4 — loop 2 step
3
+ * `targets`, the pass's LAST step): run each maintain-capable curation
4
+ * target's maintenance hook through `host.curation.maintain`, handing
5
+ * it the tick's deadline and the REMAINING daily digest runs (the
6
+ * budget the five model steps did not spend — the brief: "within the
7
+ * remaining budget").
8
+ *
9
+ * The step itself spends nothing — the target's own `run` calls are
10
+ * budgeted by the `runs` count it receives (one digest run each, per
11
+ * the P6 contract), and a target that reports `more` keeps the rest for
12
+ * the next pass. A failing target is logged and never blocks the others.
13
+ */
14
+ import { stepLog } from "./run.js";
15
+ /** The loop-2 targets step: maintain every maintain-capable target. */
16
+ export async function maintainTargets(deps) {
17
+ const { host, user } = deps;
18
+ const log = stepLog(deps);
19
+ if (deps.budget.left() < 1)
20
+ return false; // nothing to hand a target: stop
21
+ const targets = await host.curation.targets(user);
22
+ for (const target of targets) {
23
+ if (!target.maintain)
24
+ continue;
25
+ if (deps.now().getTime() >= deps.deadlineAt)
26
+ return false;
27
+ await host.curation
28
+ .maintain(user, target.id, {
29
+ deadline: deps.deadlineAt,
30
+ runs: deps.budget.left(),
31
+ })
32
+ .then((report) => {
33
+ if (report.more) {
34
+ log(`loop 2: target ${target.id} stopped early — the next pass continues it`);
35
+ }
36
+ })
37
+ .catch((error) => {
38
+ log(`loop 2: ${user} maintain of target ${target.id} failed: ${error instanceof Error ? error.message : String(error)}`);
39
+ });
40
+ }
41
+ return true;
42
+ }
package/dist/view.d.ts ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The curator view (`src/view.ts`, tabs+skills Task 13): the per-user
3
+ * surface the notice and the core Skills history link navigate to —
4
+ * the change FEED (newest 50) with an **Undo review** action per
5
+ * review, and a pause switch. Skill management (list, pin,
6
+ * archive/restore, overlays) moved to the core Skills view when it
7
+ * landed (core-views/skills.ts): this view lists no learned skills and
8
+ * no overlays any more, and its **Skills** page action navigates
9
+ * `{target:{view:{extension:"core", id:"skills"}}}`.
10
+ *
11
+ * A `review` param filters the feed to that review's commits (the core
12
+ * skills history's curator-commit link and the notice both navigate
13
+ * here with it). Feed entry ids are commit SHAs in the user's curation
14
+ * history repo (tabs+skills Task 9), so the undo groups key on the
15
+ * SHA, never on the review id: a review reverted through
16
+ * `host.curation.undoReview` needs every commit the review authored,
17
+ * and grouping by review id would merge two interleaved reviews into
18
+ * one undo. Actors render as their names — `curator`, `agent`, the
19
+ * user, and `outside sigrun` for catch-up commits the store itself
20
+ * did not author (the git trailer's actor name).
21
+ *
22
+ * Access follows `accessOf` (spec P8: "the user, plus the superuser as
23
+ * with conversations"): the resource is the caller's OWN curation data,
24
+ * so a signed-in user is its owner ("write") and an anonymous caller
25
+ * has no instance at all ("none" — accessOf's ownerless rule would
26
+ * otherwise widen to household write, so the anonymous case is gated by
27
+ * hand). Every render and action resolves the CALLING principal's own
28
+ * stores — bob's instance reads bob's data, never alice's — and the
29
+ * per-principal `instanceKey` keeps two users' views apart as surface
30
+ * instances.
31
+ *
32
+ * The actions (what an ambient record can never carry — a view's action
33
+ * routes DO reach the extension). Per-item actions ride the block
34
+ * set's item-scoped dispatches, which carry `{itemId}`:
35
+ *
36
+ * - `undoReview` — the feed collection's `onActivate`
37
+ * (`{itemId: groupId}` where the group id is `review-<newest sha>`):
38
+ * resolves the sha back to the entry's review id through the feed
39
+ * (core's `undoReview` matches `entry.reviewId`, never the group
40
+ * key), then `host.curation.undoReview`, then `host.clearAmbient` on
41
+ * the notice conversation's `curator.notice` slot (the notice ledger
42
+ * in `src/notices.ts` remembers it); a commit with no review id
43
+ * answers "Nothing to undo" without calling the host;
44
+ * - `setPaused` — the page header's pause button (a submit dispatching
45
+ * `{}`): toggles the user's `paused` setting, read live by both
46
+ * loops — pausing takes effect on the next event, restart-free.
47
+ *
48
+ * The tree is data-driven where the block set binds (item templates
49
+ * over literal rows); the whole tree passes the server's own
50
+ * `validateTree` (pinned in the tests) because the server runs that
51
+ * exact walk before serving.
52
+ */
53
+ import type { CurationFeedEntry, Principal, SigrunHost, SurfaceDef } from "@sigrun/sigrun/extension";
54
+ import type { Access } from "@sigrun/shared/access";
55
+ /** How many feed entries the view lists (the brief: newest 50). */
56
+ export declare const VIEW_FEED_LIMIT = 50;
57
+ /** How many pages a `review` deep-link walks before giving up (the
58
+ * link arrives from the core skills history at ANY depth; core's
59
+ * own `feedOf` walks until the entries run out — this view bounds
60
+ * the walk and says so when the bound elapses). */
61
+ export declare const VIEW_FEED_MAX_PAGES = 3;
62
+ /** One grouped review block in the feed (newest first). */
63
+ export interface ReviewGroup {
64
+ readonly id: string;
65
+ readonly at: string;
66
+ readonly entries: readonly {
67
+ readonly id: string;
68
+ readonly skill: string;
69
+ readonly action: string;
70
+ readonly at: string;
71
+ readonly actor: string;
72
+ readonly reason?: string;
73
+ }[];
74
+ }
75
+ /** The view's data document (everything the tree binds against). */
76
+ export interface CuratorViewData {
77
+ readonly user: string;
78
+ readonly paused: boolean;
79
+ readonly reviews: readonly ReviewGroup[];
80
+ /** True when a `review` deep-link missed after the bounded walk
81
+ * (the empty state is then "older than the recent changes", never
82
+ * the false "Nothing learned yet"). */
83
+ readonly reviewMissed: boolean;
84
+ }
85
+ /**
86
+ * Resolve one principal's access to their per-user view instance (spec
87
+ * P8: "the user, plus the superuser as with conversations"): the
88
+ * resource is the principal's OWN curation data, so a signed-in user is
89
+ * its owner (write) and the service principal the superuser (write);
90
+ * an anonymous caller has no instance at all — accessOf's ownerless
91
+ * rule would widen to household write, so anonymous is none BY HAND.
92
+ */
93
+ export declare function viewAccess(principal: Principal, user: string): Access;
94
+ /** One actor's display name (the git trailer's name: outside
95
+ * catch-up commits render as "outside sigrun"). */
96
+ export declare function actorLabel(entry: CurationFeedEntry): string;
97
+ /** Group a feed for undo: entries SHARING A REVIEW ID AND ADJACENT in
98
+ * the newest-first feed form one group — the group's id is its newest
99
+ * commit sha (`review-<sha>`). Keying on the sha (never the review
100
+ * id) keeps two interleaved reviews apart: undoing one must revert
101
+ * exactly the commits it authored. An entry with no reviewId groups
102
+ * alone (a lone commit is never undoable as a review). */
103
+ export declare function groupFeed(feed: readonly CurationFeedEntry[]): readonly ReviewGroup[];
104
+ /** The view surface (`SurfaceDef<"view">`), built over the host. */
105
+ export declare function createCuratorView(host: SigrunHost): SurfaceDef<"view">;
package/dist/view.js ADDED
@@ -0,0 +1,412 @@
1
+ /**
2
+ * The curator view (`src/view.ts`, tabs+skills Task 13): the per-user
3
+ * surface the notice and the core Skills history link navigate to —
4
+ * the change FEED (newest 50) with an **Undo review** action per
5
+ * review, and a pause switch. Skill management (list, pin,
6
+ * archive/restore, overlays) moved to the core Skills view when it
7
+ * landed (core-views/skills.ts): this view lists no learned skills and
8
+ * no overlays any more, and its **Skills** page action navigates
9
+ * `{target:{view:{extension:"core", id:"skills"}}}`.
10
+ *
11
+ * A `review` param filters the feed to that review's commits (the core
12
+ * skills history's curator-commit link and the notice both navigate
13
+ * here with it). Feed entry ids are commit SHAs in the user's curation
14
+ * history repo (tabs+skills Task 9), so the undo groups key on the
15
+ * SHA, never on the review id: a review reverted through
16
+ * `host.curation.undoReview` needs every commit the review authored,
17
+ * and grouping by review id would merge two interleaved reviews into
18
+ * one undo. Actors render as their names — `curator`, `agent`, the
19
+ * user, and `outside sigrun` for catch-up commits the store itself
20
+ * did not author (the git trailer's actor name).
21
+ *
22
+ * Access follows `accessOf` (spec P8: "the user, plus the superuser as
23
+ * with conversations"): the resource is the caller's OWN curation data,
24
+ * so a signed-in user is its owner ("write") and an anonymous caller
25
+ * has no instance at all ("none" — accessOf's ownerless rule would
26
+ * otherwise widen to household write, so the anonymous case is gated by
27
+ * hand). Every render and action resolves the CALLING principal's own
28
+ * stores — bob's instance reads bob's data, never alice's — and the
29
+ * per-principal `instanceKey` keeps two users' views apart as surface
30
+ * instances.
31
+ *
32
+ * The actions (what an ambient record can never carry — a view's action
33
+ * routes DO reach the extension). Per-item actions ride the block
34
+ * set's item-scoped dispatches, which carry `{itemId}`:
35
+ *
36
+ * - `undoReview` — the feed collection's `onActivate`
37
+ * (`{itemId: groupId}` where the group id is `review-<newest sha>`):
38
+ * resolves the sha back to the entry's review id through the feed
39
+ * (core's `undoReview` matches `entry.reviewId`, never the group
40
+ * key), then `host.curation.undoReview`, then `host.clearAmbient` on
41
+ * the notice conversation's `curator.notice` slot (the notice ledger
42
+ * in `src/notices.ts` remembers it); a commit with no review id
43
+ * answers "Nothing to undo" without calling the host;
44
+ * - `setPaused` — the page header's pause button (a submit dispatching
45
+ * `{}`): toggles the user's `paused` setting, read live by both
46
+ * loops — pausing takes effect on the next event, restart-free.
47
+ *
48
+ * The tree is data-driven where the block set binds (item templates
49
+ * over literal rows); the whole tree passes the server's own
50
+ * `validateTree` (pinned in the tests) because the server runs that
51
+ * exact walk before serving.
52
+ */
53
+ import { accessOf } from "@sigrun/shared/access";
54
+ import { readNotices } from "./notices.js";
55
+ import { CURATOR_NOTICE_KIND } from "./notices.js";
56
+ import { parseCuratorSettings } from "./settings.js";
57
+ /** How many feed entries the view lists (the brief: newest 50). */
58
+ export const VIEW_FEED_LIMIT = 50;
59
+ /** How many pages a `review` deep-link walks before giving up (the
60
+ * link arrives from the core skills history at ANY depth; core's
61
+ * own `feedOf` walks until the entries run out — this view bounds
62
+ * the walk and says so when the bound elapses). */
63
+ export const VIEW_FEED_MAX_PAGES = 3;
64
+ /** The zero-signal context the ambient clear runs under. */
65
+ const CONTEXT = { abortSignal: undefined };
66
+ /**
67
+ * Resolve one principal's access to their per-user view instance (spec
68
+ * P8: "the user, plus the superuser as with conversations"): the
69
+ * resource is the principal's OWN curation data, so a signed-in user is
70
+ * its owner (write) and the service principal the superuser (write);
71
+ * an anonymous caller has no instance at all — accessOf's ownerless
72
+ * rule would widen to household write, so anonymous is none BY HAND.
73
+ */
74
+ export function viewAccess(principal, user) {
75
+ if (principal.user === undefined)
76
+ return "none";
77
+ return accessOf({ user: principal.user, superuser: principal.service }, { owner: user });
78
+ }
79
+ /** One actor's display name (the git trailer's name: outside
80
+ * catch-up commits render as "outside sigrun"). */
81
+ export function actorLabel(entry) {
82
+ return entry.actor === "outside" ? "outside sigrun" : entry.actor;
83
+ }
84
+ /** Group a feed for undo: entries SHARING A REVIEW ID AND ADJACENT in
85
+ * the newest-first feed form one group — the group's id is its newest
86
+ * commit sha (`review-<sha>`). Keying on the sha (never the review
87
+ * id) keeps two interleaved reviews apart: undoing one must revert
88
+ * exactly the commits it authored. An entry with no reviewId groups
89
+ * alone (a lone commit is never undoable as a review). */
90
+ export function groupFeed(feed) {
91
+ const groups = [];
92
+ let current;
93
+ let currentReview;
94
+ for (const entry of feed) {
95
+ const row = {
96
+ id: entry.id,
97
+ skill: entry.skill,
98
+ action: entry.action,
99
+ at: entry.at,
100
+ actor: actorLabel(entry),
101
+ ...(entry.reason === undefined ? {} : { reason: entry.reason }),
102
+ };
103
+ if (entry.reviewId !== undefined &&
104
+ current !== undefined &&
105
+ currentReview === entry.reviewId) {
106
+ current = { ...current, entries: [...current.entries, row] };
107
+ groups[groups.length - 1] = current;
108
+ }
109
+ else {
110
+ current = {
111
+ id: `review-${entry.id}`,
112
+ at: entry.at,
113
+ entries: [row],
114
+ };
115
+ groups.push(current);
116
+ }
117
+ currentReview = entry.reviewId;
118
+ }
119
+ return groups;
120
+ }
121
+ /** The `review` param a render was anchored at (the core skills
122
+ * history link and the notice navigate with it), or undefined. */
123
+ function reviewParam(ctx) {
124
+ const review = ctx.params.review;
125
+ return typeof review === "string" && review !== "" ? review : undefined;
126
+ }
127
+ /** Read the feed: the plain view is the newest VIEW_FEED_LIMIT only
128
+ * (the section's "newest 50" contract — every rendered group's sha
129
+ * resolves for undo from that same page). A `review` deep-link pages
130
+ * on (last sha as the cursor, core's feedOf shape, bounded) until
131
+ * the review's entries appear or the walk elapses; a miss on ANY
132
+ * exit path — short page or bound — is reported, never rendered as
133
+ * the false "Nothing learned yet". */
134
+ async function feedFor(host, user, review) {
135
+ const store = host.curation.skills(user);
136
+ const collected = [];
137
+ let before;
138
+ do {
139
+ const rows = await store.feed(VIEW_FEED_LIMIT, before);
140
+ collected.push(...rows);
141
+ if (rows.length < VIEW_FEED_LIMIT)
142
+ break; // the last page
143
+ if (review !== undefined &&
144
+ rows.some((entry) => entry.reviewId === review))
145
+ break; // the deep-link found its review
146
+ before = rows[rows.length - 1]?.id;
147
+ } while (review !== undefined && collected.length < VIEW_FEED_LIMIT * VIEW_FEED_MAX_PAGES);
148
+ return {
149
+ feed: collected,
150
+ reviewMissed: review !== undefined &&
151
+ !collected.some((entry) => entry.reviewId === review),
152
+ };
153
+ }
154
+ /** Build the view's data document for one user (the render's model). */
155
+ async function readData(host, user, review) {
156
+ const settings = parseCuratorSettings(await host.stores.user(user).settings.get());
157
+ const { feed, reviewMissed } = await feedFor(host, user, review);
158
+ return {
159
+ user,
160
+ paused: settings.paused,
161
+ reviews: groupFeed(review === undefined
162
+ ? feed
163
+ : feed.filter((entry) => entry.reviewId === review)),
164
+ reviewMissed,
165
+ };
166
+ }
167
+ /** Whether `payload` (an action's JSON argument) is a plain object. */
168
+ function isObject(payload) {
169
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload);
170
+ }
171
+ /** The `{itemId}` dispatch an `onActivate` click carries. */
172
+ function itemIdOf(payload) {
173
+ if (!isObject(payload))
174
+ return undefined;
175
+ const itemId = payload.itemId;
176
+ return typeof itemId === "string" && itemId !== "" ? itemId : undefined;
177
+ }
178
+ /** The view surface (`SurfaceDef<"view">`), built over the host. */
179
+ export function createCuratorView(host) {
180
+ /** Reject for a caller with no instance of their own. */
181
+ const noAccess = () => ({
182
+ ok: false,
183
+ error: { message: "sign in to use the curator view" },
184
+ });
185
+ /** The caller's user, or a reject when they have no instance. */
186
+ function callerOf(ctx) {
187
+ const user = ctx.principal.user;
188
+ if (user === undefined)
189
+ return { reject: noAccess() };
190
+ if (viewAccess(ctx.principal, user) === "none")
191
+ return { reject: noAccess() };
192
+ return { user };
193
+ }
194
+ return {
195
+ id: "curator",
196
+ title: "Curator",
197
+ icon: "book",
198
+ placement: "view",
199
+ // A principal-owned surface (§7.3): the principal folds into the
200
+ // instance identity so two users' views are two instances.
201
+ instanceKey: (params, principal) => JSON.stringify([principal.user ?? "", params]),
202
+ render: async (ctx) => {
203
+ const who = callerOf(ctx);
204
+ if ("reject" in who) {
205
+ return { tree: emptyTree(), data: {}, access: "none" };
206
+ }
207
+ const data = await readData(host, who.user, reviewParam(ctx));
208
+ return {
209
+ tree: viewTree(data),
210
+ // SAFETY: the data document is a JSON mirror (every field a
211
+ // literal); the interface lacks the index signature JsonObject
212
+ // requires, so the cast only restores that erased identity.
213
+ data: data,
214
+ access: "write",
215
+ };
216
+ },
217
+ data: async (ctx) => {
218
+ const who = callerOf(ctx);
219
+ if ("reject" in who)
220
+ return { data: {}, access: "none" };
221
+ return {
222
+ // SAFETY: same JSON-mirror cast as `render`'s (the comment there).
223
+ data: (await readData(host, who.user, reviewParam(ctx))),
224
+ access: "write",
225
+ };
226
+ },
227
+ actions: {
228
+ undoReview: async (ctx, payload) => {
229
+ const who = callerOf(ctx);
230
+ if ("reject" in who)
231
+ return who.reject;
232
+ const itemId = itemIdOf(payload);
233
+ if (itemId === undefined) {
234
+ return { ok: false, error: { message: "undoReview expects {itemId}" } };
235
+ }
236
+ // A failing undo (the store's report, or a host error) is a
237
+ // named action error — never a thrown action the surface
238
+ // layer renders generically (main's 774c660 semantics,
239
+ // kept through the git-history rework).
240
+ const fail = (cause) => ({
241
+ ok: false,
242
+ error: { message: cause instanceof Error ? cause.message : String(cause) },
243
+ });
244
+ // On a deep-link the param already names the review — and
245
+ // the group's sha may sit past page 1 of the feed, where
246
+ // the single-page lookup below cannot see it. Dispatch the
247
+ // param's review id directly.
248
+ const anchored = reviewParam(ctx);
249
+ if (anchored !== undefined) {
250
+ let reverted;
251
+ try {
252
+ reverted = await host.curation.undoReview(who.user, anchored);
253
+ }
254
+ catch (cause) {
255
+ return fail(cause);
256
+ }
257
+ const anchoredNotice = (await readNotices(host, who.user)).find((entry) => entry.reviewId === anchored);
258
+ if (anchoredNotice !== undefined) {
259
+ await host.clearAmbient(anchoredNotice.conversationId, CURATOR_NOTICE_KIND, CONTEXT).catch(() => undefined);
260
+ }
261
+ return {
262
+ ok: true,
263
+ toast: reverted > 0 ? `Undid ${reverted} change(s)` : "Nothing to undo",
264
+ };
265
+ }
266
+ // The dispatch carries the GROUP key (`review-<sha>`, minted
267
+ // by groupFeed); core's undoReview matches entry.reviewId.
268
+ // Resolve the sha back to the review id through the feed — a
269
+ // verbatim dispatch would match nothing and silently revert 0.
270
+ // A bare review id (the pre-upgrade coordinate, and digest
271
+ // ids like "digest-<date>" that already carry the prefix) is
272
+ // the fallback when no entry's sha matches.
273
+ const sha = itemId.startsWith("review-")
274
+ ? itemId.slice("review-".length)
275
+ : itemId;
276
+ const feed = await host.curation.skills(who.user).feed(VIEW_FEED_LIMIT);
277
+ const reviewId = feed.find((entry) => entry.id === sha)?.reviewId ??
278
+ (feed.some((entry) => entry.reviewId === itemId)
279
+ ? itemId
280
+ : undefined);
281
+ if (reviewId === undefined) {
282
+ return { ok: true, toast: "Nothing to undo" };
283
+ }
284
+ let reverted;
285
+ try {
286
+ reverted = await host.curation.undoReview(who.user, reviewId);
287
+ }
288
+ catch (cause) {
289
+ return fail(cause);
290
+ }
291
+ // Clear the notice this review set, when the ledger remembers
292
+ // one (a digest review with no notice clears nothing).
293
+ const notice = (await readNotices(host, who.user)).find((entry) => entry.reviewId === reviewId);
294
+ if (notice !== undefined) {
295
+ // The revert already succeeded — a failing clear only leaves a
296
+ // stale notice (replaced by the next review's), so it must
297
+ // never turn the undo into a failed action with no toast.
298
+ await host.clearAmbient(notice.conversationId, CURATOR_NOTICE_KIND, CONTEXT).catch(() => undefined);
299
+ }
300
+ return {
301
+ ok: true,
302
+ toast: reverted > 0 ? `Undid ${reverted} change(s)` : "Nothing to undo",
303
+ };
304
+ },
305
+ setPaused: async (ctx, payload) => {
306
+ const who = callerOf(ctx);
307
+ if ("reject" in who)
308
+ return who.reject;
309
+ // The header button dispatches `{}` (toggle); a form-shaped
310
+ // `{values: {paused}}` or bare `{paused}` sets explicitly.
311
+ const explicit = (isObject(payload) && isObject(payload.values)
312
+ ? payload.values.paused
313
+ : undefined) ??
314
+ (isObject(payload) ? payload.paused : undefined);
315
+ const raw = await host.stores.user(who.user).settings.get();
316
+ const current = parseCuratorSettings(raw).paused;
317
+ const paused = typeof explicit === "boolean" ? explicit : !current;
318
+ if (paused === current) {
319
+ return { ok: true, toast: paused ? "Curation paused" : "Curation resumed" };
320
+ }
321
+ await host.stores.user(who.user).settings.set({ ...raw, paused });
322
+ return {
323
+ ok: true,
324
+ toast: paused ? "Curation paused" : "Curation resumed",
325
+ };
326
+ },
327
+ },
328
+ };
329
+ }
330
+ /** The signed-out/none-access tree (an empty page; the route 404s it). */
331
+ function emptyTree() {
332
+ return {
333
+ v: 1,
334
+ root: {
335
+ type: "page",
336
+ title: "Curator",
337
+ main: [
338
+ { type: "markdown", text: "Sign in to see your curator activity." },
339
+ ],
340
+ },
341
+ };
342
+ }
343
+ /** The view tree: header actions (pause switch + the Skills
344
+ * link to the core view), then the feed grouped for undo. */
345
+ function viewTree(data) {
346
+ return {
347
+ v: 1,
348
+ root: {
349
+ type: "page",
350
+ id: "curator-root",
351
+ title: "Curator",
352
+ subtitle: `Learned activity for ${data.user}${data.paused ? " (paused)" : ""}`,
353
+ actions: [
354
+ {
355
+ id: "pause-switch",
356
+ label: data.paused ? "Resume curation" : "Pause curation",
357
+ icon: data.paused ? "sun" : "moon",
358
+ action: { type: "submit", name: "setPaused" },
359
+ },
360
+ {
361
+ id: "open-skills",
362
+ label: "Skills",
363
+ icon: "book",
364
+ action: {
365
+ type: "navigate",
366
+ target: {
367
+ view: { extension: "core", id: "skills" },
368
+ },
369
+ },
370
+ },
371
+ ],
372
+ main: [
373
+ {
374
+ type: "section",
375
+ id: "feed-section",
376
+ title: "Change feed (newest 50, grouped by review)",
377
+ children: [
378
+ {
379
+ type: "markdown",
380
+ text: "Click a review to undo every change it made. Outside sigrun marks catch-up commits made outside sigrun.",
381
+ },
382
+ data.reviews.length === 0
383
+ ? {
384
+ type: "markdown",
385
+ text: data.reviewMissed
386
+ ? "This review is older than the recent changes."
387
+ : "Nothing learned yet. Use /learn to ask for a review now.",
388
+ }
389
+ : {
390
+ type: "collection",
391
+ id: "reviews",
392
+ display: "list",
393
+ items: data.reviews.map((group) => ({
394
+ id: group.id,
395
+ at: group.at,
396
+ summary: group.entries
397
+ .map((entry) => `${entry.action} ${entry.skill} (${entry.actor})`)
398
+ .join(", "),
399
+ })),
400
+ onActivate: { type: "edit", name: "undoReview" },
401
+ item: {
402
+ title: { $: "./id" },
403
+ subtitle: { $: "./summary" },
404
+ meta: [{ time: { $: "./at" }, format: "relative" }],
405
+ },
406
+ },
407
+ ],
408
+ },
409
+ ],
410
+ },
411
+ };
412
+ }
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
- "name": "@sigrun/ext-curator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@sigrun/ext-curator",
3
+ "version": "0.2.0",
4
+ "description": "Curator extension for sigrun (pi-durable): the per-user fork review and idle maintenance pass over learned skills and curation targets",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22.19.0"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "node scripts/check-manifest.mjs --expect-name @sigrun/ext-curator && vitest run",
23
+ "typecheck": "tsc --noEmit",
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "peerDependencies": {
28
+ "@earendil-works/chord": "1.3.0 || 1.4.0",
29
+ "@earendil-works/pi-ai": "1.3.0 || 1.4.0",
30
+ "@earendil-works/pi-durable": "1.3.0 || 1.4.0"
31
+ },
32
+ "devDependencies": {
33
+ "@earendil-works/chord": "1.4.0",
34
+ "@earendil-works/pi-ai": "1.4.0",
35
+ "@earendil-works/pi-durable": "1.4.0",
36
+ "@sigrun/sigrun": "file:../sigrun",
37
+ "@sigrun/shared": "file:../sigrun/shared",
38
+ "@types/node": "^26.6.4",
39
+ "tsx": "^4.23.15",
40
+ "typescript": "^7.0.2",
41
+ "vitest": "^5.0.3"
42
+ },
43
+ "license": "MIT",
44
+ "repository": {
45
+ "type": "github",
46
+ "url": "git+https://github.com/sigrun-ai/sigrun-ext-curator.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/sigrun-ai/sigrun-ext-curator/issues"
50
+ },
51
+ "homepage": "https://github.com/sigrun-ai/sigrun-ext-curator#readme",
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "provenance": true
55
+ }
56
+ }