@arjunkhera/atlas 0.3.21 → 0.3.22
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/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/sdlc-task/SKILL.md +1 -1
- package/work/lib/roundup.mjs +206 -0
- package/work/lib/verbs.mjs +3 -1
- package/work/mcp.mjs +1 -0
package/package.json
CHANGED
|
@@ -38,7 +38,7 @@ checks and the transition log; each verb's own description says what it needs.
|
|
|
38
38
|
| Ask the owner something, or record the answer | `question_ask`, `question_answer` |
|
|
39
39
|
| Record a decision in the owner's words | `decision_record` |
|
|
40
40
|
| Mark it delivered, or reopen it | `item_done`, `item_reopen` |
|
|
41
|
-
| Say where things stand | `where_are_we`, `in_flight`, `needs_me` |
|
|
41
|
+
| Say where things stand | `roundup` for a whole product; `where_are_we`, `in_flight`, `needs_me` for one question |
|
|
42
42
|
|
|
43
43
|
The repo's own facts come from `atlas.yaml` and `atlas/`. Its kinds (`service`,
|
|
44
44
|
`library`, `schema`, `cli`, `plugin`) decide which procedures it has and what
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// The roundup: where one product stands, and where it is going (target design
|
|
2
|
+
// flow 9). Tracker item b1610d9b moved it forward from stage 4 (decision
|
|
3
|
+
// 3c268725).
|
|
4
|
+
//
|
|
5
|
+
// It joins the reads that ship already and adds no store:
|
|
6
|
+
// 1. Needs you the same read as needs_me
|
|
7
|
+
// 2. Where we stand items tagged stage-<n>, in ladder order, and a count of
|
|
8
|
+
// every item of the product by lifecycle
|
|
9
|
+
// 3. What moved delivery marks, merged pull requests and new items in
|
|
10
|
+
// the window
|
|
11
|
+
// 4. Stuck or drifting active items with no change for STUCK_DAYS, merged
|
|
12
|
+
// but not marked, open pull requests that name no item,
|
|
13
|
+
// and questions that wait longer than STUCK_DAYS
|
|
14
|
+
// 5. Trend per week, for TREND_WEEKS weeks: merges, deliveries,
|
|
15
|
+
// new items; and the open questions with the oldest age
|
|
16
|
+
// Every row carries a citation: an item id, a question id or a pull request
|
|
17
|
+
// URL. It is computed at read time and never stored.
|
|
18
|
+
import { refuse, isoDate, LIVE_LIFECYCLES } from './verb-fields.mjs';
|
|
19
|
+
import { oneLine, ROW_LINE_MAX } from './questions.mjs';
|
|
20
|
+
import { readPullsByRepo, pullsOfItem, daysBetween, listLine } from './delivery.mjs';
|
|
21
|
+
|
|
22
|
+
export const ROUNDUP_READ_VERBS = Object.freeze(['roundup']);
|
|
23
|
+
|
|
24
|
+
// A free tag that places an item on its product's ladder. An item tagged
|
|
25
|
+
// stage-2 is the stage 2 item or one of its parts.
|
|
26
|
+
export const STAGE_TAG = /^stage-(\d{1,2})$/;
|
|
27
|
+
export const STUCK_DAYS = 14;
|
|
28
|
+
export const TREND_WEEKS = 4;
|
|
29
|
+
const DEFAULT_WINDOW = 7;
|
|
30
|
+
const MAX_WINDOW = 90;
|
|
31
|
+
const MAX_ITEMS = 500;
|
|
32
|
+
const MAX_REPOS = 16;
|
|
33
|
+
const SHOWN = 8;
|
|
34
|
+
const DAY = 86_400_000;
|
|
35
|
+
const LIFECYCLE_ORDER = ['active', 'proposed', 'paused', 'done', 'dropped'];
|
|
36
|
+
|
|
37
|
+
const plural = (count, word) => `${count} ${count === 1 ? word : (/[^aeiou]y$/.test(word) ? `${word.slice(0, -1)}ies` : `${word}s`)}`;
|
|
38
|
+
const dayOf = (value) => String(value ?? '').slice(0, 10);
|
|
39
|
+
const minus = (readAt, days) => new Date(Date.parse(readAt) - days * DAY).toISOString();
|
|
40
|
+
|
|
41
|
+
export function stageOf(item) {
|
|
42
|
+
const tags = Array.isArray(item?.tags) ? item.tags : [];
|
|
43
|
+
const numbers = tags.map((tag) => STAGE_TAG.exec(String(tag).toLowerCase())).filter(Boolean).map((match) => Number(match[1]));
|
|
44
|
+
return numbers.length ? Math.min(...numbers) : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The ladder: each stage number with its items, lowest stage first. A stage
|
|
48
|
+
// reads "done" only when every item on it is done or dropped.
|
|
49
|
+
export function ladderOf(items) {
|
|
50
|
+
const byStage = new Map();
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
const stage = stageOf(item);
|
|
53
|
+
if (stage === null) continue;
|
|
54
|
+
if (!byStage.has(stage)) byStage.set(stage, []);
|
|
55
|
+
byStage.get(stage).push(item);
|
|
56
|
+
}
|
|
57
|
+
return [...byStage.entries()].sort((a, b) => a[0] - b[0]).map(([stage, list]) => {
|
|
58
|
+
const rows = list.map((item) => ({ item: item.id, title: oneLine(item.title, 120), lifecycle: item.fields?.lifecycle ?? 'unknown', stage: item.fields?.stage ?? '' }))
|
|
59
|
+
.sort((a, b) => LIFECYCLE_ORDER.indexOf(a.lifecycle) - LIFECYCLE_ORDER.indexOf(b.lifecycle) || a.title.localeCompare(b.title));
|
|
60
|
+
const counts = countBy(rows.map((row) => row.lifecycle));
|
|
61
|
+
const live = rows.filter((row) => LIVE_LIFECYCLES.includes(row.lifecycle));
|
|
62
|
+
const state = !live.length ? 'done' : (rows.some((row) => row.lifecycle === 'active') ? 'doing' : (rows.some((row) => row.lifecycle === 'done') ? 'started' : 'not started'));
|
|
63
|
+
return { stage, state, counts, rows };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function countBy(values) {
|
|
68
|
+
const counts = {};
|
|
69
|
+
for (const value of values) counts[value] = (counts[value] ?? 0) + 1;
|
|
70
|
+
return Object.fromEntries(Object.entries(counts).sort((a, b) => {
|
|
71
|
+
const rank = (key) => { const at = LIFECYCLE_ORDER.indexOf(key); return at === -1 ? LIFECYCLE_ORDER.length : at; };
|
|
72
|
+
return rank(a[0]) - rank(b[0]) || a[0].localeCompare(b[0]);
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
const countLine = (counts) => Object.entries(counts).map(([key, value]) => `${value} ${key}`).join(', ') || 'none';
|
|
76
|
+
|
|
77
|
+
// The week a moment falls in, counted back from the read: week 1 is the last
|
|
78
|
+
// seven days. A moment outside the trend is null.
|
|
79
|
+
export function weekOf(at, readAt, weeks = TREND_WEEKS) {
|
|
80
|
+
const age = Date.parse(readAt) - Date.parse(at ?? '');
|
|
81
|
+
if (Number.isNaN(age) || age < 0) return null;
|
|
82
|
+
const week = Math.floor(age / (7 * DAY)) + 1;
|
|
83
|
+
return week > weeks ? null : week;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createRoundupVerbs(core) {
|
|
87
|
+
const { client, now, typeNamed, registry, requireProduct, readGitHub, inFlight, needsMe } = core;
|
|
88
|
+
return {
|
|
89
|
+
async roundup(args = {}) {
|
|
90
|
+
if (args.product === undefined) refuse('product is required: a roundup answers for one product');
|
|
91
|
+
const reg = await registry();
|
|
92
|
+
const product = (await requireProduct(args.product, reg)).fields.registry_id;
|
|
93
|
+
const windowDays = args.days === undefined ? DEFAULT_WINDOW : args.days;
|
|
94
|
+
if (!Number.isInteger(windowDays) || windowDays < 1 || windowDays > MAX_WINDOW) refuse(`days must be a whole number from 1 to ${MAX_WINDOW}`);
|
|
95
|
+
const readAt = now();
|
|
96
|
+
const since = minus(readAt, windowDays);
|
|
97
|
+
const trendFrom = minus(readAt, Math.max(windowDays, TREND_WEEKS * 7));
|
|
98
|
+
|
|
99
|
+
// Parts 1 and 4 reuse the two views that ship, so one rule serves both.
|
|
100
|
+
const needs = await needsMe({ product });
|
|
101
|
+
const flight = await inFlight({ product, since: isoDate(dayOf(trendFrom), 'since') });
|
|
102
|
+
|
|
103
|
+
const container = await typeNamed('container');
|
|
104
|
+
const all = await client.listAllEntities({ type: container.name, state: 'active' });
|
|
105
|
+
const items = all.filter((row) => row.fields?.product === product);
|
|
106
|
+
if (items.length > MAX_ITEMS) refuse(`${items.length} items belong to ${product}; the roundup reads at most ${MAX_ITEMS}`);
|
|
107
|
+
|
|
108
|
+
const repoIds = [...reg.repos.values()].filter((repo) => repo.fields.product === product).map((repo) => repo.fields.registry_id).sort();
|
|
109
|
+
if (repoIds.length > MAX_REPOS) refuse(`${repoIds.length} repos are registered for ${product}; the roundup reads at most ${MAX_REPOS}`);
|
|
110
|
+
const { pullsByRepo, repoErrors } = await readPullsByRepo(repoIds, readGitHub);
|
|
111
|
+
|
|
112
|
+
// Which item each pull request belongs to: an item that links it or
|
|
113
|
+
// whose id it names, as in_flight reads it.
|
|
114
|
+
const owner = new Map();
|
|
115
|
+
for (const item of items) for (const pull of pullsOfItem(item, pullsByRepo, repoIds)) if (!owner.has(pull.url)) owner.set(pull.url, item.id);
|
|
116
|
+
const pulls = [...pullsByRepo.values()].flat();
|
|
117
|
+
|
|
118
|
+
// 1. Needs you.
|
|
119
|
+
const needRows = (needs.products?.[0]?.rows ?? []).map((row) => ({
|
|
120
|
+
question: row.question_id, item: row.item, text: oneLine(row.folded?.question ?? row.lines?.[0] ?? '', 200), item_title: oneLine(row.folded?.item_title ?? '', 120),
|
|
121
|
+
asked_at: row.asked_at, days_waiting: daysBetween(row.asked_at, readAt), cost: row.cost,
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
// 2. Where we stand.
|
|
125
|
+
const ladder = ladderOf(items);
|
|
126
|
+
const lifecycles = countBy(items.map((row) => row.fields?.lifecycle ?? 'unknown'));
|
|
127
|
+
const active = (flight.products?.[0]?.rows ?? []).map((row) => ({ item: row.item, title: oneLine(row.folded?.title ?? '', 120), stage: row.stage, days_since_last_change: row.days_since_last_change, waits_on: row.waits_on ?? null }));
|
|
128
|
+
|
|
129
|
+
// 3. What moved, in the window.
|
|
130
|
+
const merged = pulls.filter((pull) => pull.state === 'merged' && String(pull.merged_at) >= since)
|
|
131
|
+
.map((pull) => ({ pr: pull.url, title: pull.title, merged_at: pull.merged_at, item: owner.get(pull.url) ?? null }))
|
|
132
|
+
.sort((a, b) => String(b.merged_at).localeCompare(String(a.merged_at)));
|
|
133
|
+
const deliveries = (flight.delivered ?? []).filter((row) => String(row.delivered_at) >= since)
|
|
134
|
+
.map((row) => ({ item: row.item, title: row.item_title, verdict: row.verdict, delivered_at: row.delivered_at, prs: row.prs }));
|
|
135
|
+
const created = items.filter((row) => String(row.created_at ?? '') >= since)
|
|
136
|
+
.map((row) => ({ item: row.id, title: oneLine(row.title, 120), lifecycle: row.fields?.lifecycle ?? 'unknown', created_at: row.created_at }))
|
|
137
|
+
.sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
|
|
138
|
+
|
|
139
|
+
// 4. Stuck or drifting.
|
|
140
|
+
const stale = active.filter((row) => row.days_since_last_change !== null && row.days_since_last_change >= STUCK_DAYS);
|
|
141
|
+
const notMarked = (flight.merged_not_marked ?? []).map((row) => ({ item: row.item, pr: row.pr, title: row.title, days: row.days }));
|
|
142
|
+
const openNoItem = pulls.filter((pull) => pull.state === 'open' && !owner.has(pull.url)).map((pull) => ({ pr: pull.url, title: pull.title, updated_at: pull.updated_at }));
|
|
143
|
+
const longWaits = needRows.filter((row) => row.days_waiting !== null && row.days_waiting >= STUCK_DAYS);
|
|
144
|
+
|
|
145
|
+
// 5. Trend, per week; week 1 is the last seven days.
|
|
146
|
+
const weeks = Array.from({ length: TREND_WEEKS }, (_, index) => ({ week: index + 1, from: dayOf(minus(readAt, (index + 1) * 7)), to: dayOf(minus(readAt, index * 7)), merged: 0, delivered: 0, new_items: 0 }));
|
|
147
|
+
const bump = (at, key) => { const week = weekOf(at, readAt); if (week) weeks[week - 1][key] += 1; };
|
|
148
|
+
for (const pull of pulls) if (pull.state === 'merged') bump(pull.merged_at, 'merged');
|
|
149
|
+
for (const row of flight.delivered ?? []) bump(row.delivered_at, 'delivered');
|
|
150
|
+
for (const row of items) bump(row.created_at, 'new_items');
|
|
151
|
+
const oldest = needRows.reduce((max, row) => Math.max(max, row.days_waiting ?? 0), 0);
|
|
152
|
+
|
|
153
|
+
const text = render({ product, readAt, windowDays, needRows, ladder, lifecycles, active, merged, deliveries, created, stale, notMarked, openNoItem, longWaits, weeks, oldest, repoErrors });
|
|
154
|
+
return {
|
|
155
|
+
verb: 'roundup', read_at: readAt, product, window_days: windowDays, since,
|
|
156
|
+
needs_you: needRows,
|
|
157
|
+
where_we_stand: { ladder, lifecycles, active },
|
|
158
|
+
what_moved: { merged, delivered: deliveries, new_items: created },
|
|
159
|
+
stuck_or_drifting: { stale, merged_not_marked: notMarked, open_prs_with_no_item: openNoItem, long_waits: longWaits, stuck_days: STUCK_DAYS },
|
|
160
|
+
trend: { weeks, open_questions: needRows.length, oldest_question_days: oldest },
|
|
161
|
+
repos_read: repoIds, repo_errors: repoErrors,
|
|
162
|
+
citation: { product, read_at: readAt, items: items.length, repos: repoIds },
|
|
163
|
+
text,
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function render(view) {
|
|
170
|
+
const { product, readAt, windowDays, needRows, ladder, lifecycles, active, merged, deliveries, created, stale, notMarked, openNoItem, longWaits, weeks, oldest, repoErrors } = view;
|
|
171
|
+
const more = (list) => (list.length > SHOWN ? [` and ${list.length - SHOWN} more`] : []);
|
|
172
|
+
const out = [`${product} — roundup, ${dayOf(readAt)}`];
|
|
173
|
+
|
|
174
|
+
out.push([`1. Needs you (${needRows.length})`,
|
|
175
|
+
...(needRows.length ? needRows.slice(0, SHOWN).map((row) => listLine(row.text, `${row.question} · asked ${dayOf(row.asked_at)}, ${plural(row.days_waiting ?? 0, 'day')}`)) : [' nothing']),
|
|
176
|
+
...more(needRows)].join('\n'));
|
|
177
|
+
|
|
178
|
+
const stand = [`2. Where we stand · items: ${countLine(lifecycles)}`];
|
|
179
|
+
if (ladder.length) for (const step of ladder) stand.push(` ${oneLine(`Stage ${step.stage}: ${step.state} · ${countLine(step.counts)} · ${step.rows.filter((row) => LIVE_LIFECYCLES.includes(row.lifecycle)).map((row) => `${row.title} (${row.lifecycle}, ${row.item.slice(0, 8)})`).join('; ') || 'all closed'}`, ROW_LINE_MAX - 3)}`);
|
|
180
|
+
else stand.push(' no item carries a stage-<n> tag');
|
|
181
|
+
stand.push(` in flight (${active.length})`);
|
|
182
|
+
stand.push(...(active.length ? active.slice(0, SHOWN).map((row) => listLine(row.title, `${row.item} · ${row.stage || 'no stage'} · ${row.days_since_last_change ?? '?'} days since last change`, ' ')) : [' nothing']));
|
|
183
|
+
out.push(stand.join('\n'));
|
|
184
|
+
|
|
185
|
+
out.push([`3. What moved, last ${plural(windowDays, 'day')}`,
|
|
186
|
+
` ${plural(deliveries.length, 'delivery')} · ${plural(merged.length, 'merged pull request')} · ${plural(created.length, 'new item')}`,
|
|
187
|
+
...deliveries.slice(0, SHOWN).map((row) => listLine(`delivered: ${row.title}`, `${row.item} · ${dayOf(row.delivered_at)}`)),
|
|
188
|
+
...merged.slice(0, SHOWN).map((row) => listLine(`merged: ${row.title}`, `${row.pr} · ${dayOf(row.merged_at)}${row.item ? '' : ' · no item'}`)),
|
|
189
|
+
...more(merged),
|
|
190
|
+
...created.slice(0, SHOWN).map((row) => listLine(`new: ${row.title}`, `${row.item} · ${row.lifecycle}`)),
|
|
191
|
+
...more(created)].join('\n'));
|
|
192
|
+
|
|
193
|
+
const stuck = [`4. Stuck or drifting`];
|
|
194
|
+
stuck.push(` no change in ${STUCK_DAYS} days or more (${stale.length})`, ...stale.slice(0, SHOWN).map((row) => listLine(row.title, `${row.item} · ${row.days_since_last_change} days`, ' ')));
|
|
195
|
+
stuck.push(` merged, not marked (${notMarked.length})`, ...notMarked.slice(0, SHOWN).map((row) => listLine(row.title, `${row.pr} · ${row.item}`, ' ')));
|
|
196
|
+
stuck.push(` open pull requests with no item (${openNoItem.length})`, ...openNoItem.slice(0, SHOWN).map((row) => listLine(row.title, row.pr, ' ')));
|
|
197
|
+
stuck.push(` questions waiting ${STUCK_DAYS} days or more (${longWaits.length})`, ...longWaits.slice(0, SHOWN).map((row) => listLine(row.text, `${row.question} · ${row.days_waiting} days`, ' ')));
|
|
198
|
+
out.push(stuck.join('\n'));
|
|
199
|
+
|
|
200
|
+
out.push([`5. Trend, per week (week 1 is the last seven days)`,
|
|
201
|
+
...weeks.map((week) => ` week ${week.week} (${week.from} to ${week.to}): ${plural(week.merged, 'merge')} · ${plural(week.delivered, 'delivery')} · ${plural(week.new_items, 'new item')}`),
|
|
202
|
+
` waiting on you: ${plural(needRows.length, 'question')}${needRows.length ? `, oldest ${plural(oldest, 'day')}` : ''}`].join('\n'));
|
|
203
|
+
|
|
204
|
+
if (repoErrors.length) out.push([`GitHub did not answer for ${plural(repoErrors.length, 'repo')}; parts 3 to 5 count from the graph only`, ...repoErrors.map((row) => oneLine(` ${row.repo} — ${row.reason}`, ROW_LINE_MAX))].join('\n'));
|
|
205
|
+
return out.join('\n\n');
|
|
206
|
+
}
|
package/work/lib/verbs.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { createQuestionVerbs, COSTS, QUESTION_WRITE_VERBS, QUESTION_READ_VERBS,
|
|
|
20
20
|
import { createDeliveryVerbs, DELIVERY_WRITE_VERBS, DELIVERY_READ_VERBS } from './delivery.mjs';
|
|
21
21
|
import { createCapabilityVerbs, CAPABILITY_READ_VERBS, hubOf } from './capability.mjs';
|
|
22
22
|
import { createSweepVerbs, SWEEP_WRITE_VERBS, SWEEP_READ_VERBS } from './sweep.mjs';
|
|
23
|
+
import { createRoundupVerbs, ROUNDUP_READ_VERBS } from './roundup.mjs';
|
|
23
24
|
import { createActivityVerbs, diffOf, LOG_VERBS, ENTRY_VERBS, ACTIVITY_WRITE_VERBS, ACTIVITY_READ_VERBS } from './activity.mjs';
|
|
24
25
|
|
|
25
26
|
export { VerbError, canonicalJson, sha256, COSTS, LIVE_LIFECYCLES, REPO_KINDS, LIVE_KINDS, repoKinds };
|
|
@@ -751,11 +752,12 @@ export function createVerbs({ client, store, writer, session, agent = 'lead', bo
|
|
|
751
752
|
Object.assign(verbs, createDeliveryVerbs({ ...core, ownerPresent, readGitHub, readLive }));
|
|
752
753
|
Object.assign(verbs, createCapabilityVerbs({ ...core, readGitHub }));
|
|
753
754
|
Object.assign(verbs, createSweepVerbs({ ...core, requireRepos, inFlight: (args) => verbs.in_flight(args), stages: STAGES, tiers: TIERS, drivers: DRIVERS }));
|
|
755
|
+
Object.assign(verbs, createRoundupVerbs({ ...core, readGitHub, inFlight: (args) => verbs.in_flight(args), needsMe: (args) => verbs.needs_me(args) }));
|
|
754
756
|
const activity = createActivityVerbs(core);
|
|
755
757
|
Object.assign(verbs, activity.verbs);
|
|
756
758
|
|
|
757
759
|
const WRITE_VERBS = Object.freeze(['setup', 'product_add', 'product_edit', 'repo_add', 'repo_edit', 'item_propose', 'item_lock', 'item_split', 'item_park', 'item_resume', 'item_drop', 'item_edit', 'item_block', 'item_unblock', 'item_stage', 'item_link', ...QUESTION_WRITE_VERBS, ...DELIVERY_WRITE_VERBS, ...SWEEP_WRITE_VERBS, ...ACTIVITY_WRITE_VERBS]);
|
|
758
|
-
const READ_VERBS = Object.freeze(['registry_list', ...QUESTION_READ_VERBS, ...DELIVERY_READ_VERBS, ...CAPABILITY_READ_VERBS, ...SWEEP_READ_VERBS, ...ACTIVITY_READ_VERBS]);
|
|
760
|
+
const READ_VERBS = Object.freeze(['registry_list', ...QUESTION_READ_VERBS, ...DELIVERY_READ_VERBS, ...CAPABILITY_READ_VERBS, ...SWEEP_READ_VERBS, ...ROUNDUP_READ_VERBS, ...ACTIVITY_READ_VERBS]);
|
|
759
761
|
|
|
760
762
|
async function run(verb, args = {}) {
|
|
761
763
|
if (!verbs[verb]) refuse(`unknown verb: ${verb}`);
|
package/work/mcp.mjs
CHANGED
|
@@ -69,6 +69,7 @@ export const TOOLS = [
|
|
|
69
69
|
{ name: 'in_flight', description: 'Answer "what are we working on, and what changed since a date?". Active items per product, ordered by the owner\'s current priority decision first, when it carries an order, then by days since the last change with the latest on top, and the days shown on each row. Each row says what the item waits on. Movement is read from the marks and from each registered repo\'s recent pull requests: the item\'s own pr links, plus any pull request whose title, body or branch names the item id. Merged pull requests with no delivery mark are listed as "merged, not marked". A legacy seal reads as a delivery mark with verdict "legacy". Computed at read time and never stored. Scope: read, no owner flag needed.', inputSchema: { type: 'object', properties: { product: S('Limit the view to one product id.'), since: S('ISO date. Adds what was delivered since then.') }, additionalProperties: false } },
|
|
70
70
|
{ name: 'sweep_check', description: 'Check the sweep against the "In flight" view: the newest decision record of kind sweep names the items the owner called in flight, and this reads the view and names every difference — what is listed and not in flight, and what is in flight and not listed. A product with no sweep on record is named too. It answers two things apart: `matches` says each product\'s view holds exactly what its sweep lists, and `complete` says that and that no item in the tenant sits outside every product. An item nobody adopted is missing from both sides, so `matches` alone can read "equal" over an unfinished sweep. Computed at read time and never stored. Scope: read, no owner flag needed.', inputSchema: { type: 'object', properties: { product: S('Limit the check to one product id.') }, additionalProperties: false } },
|
|
71
71
|
{ name: 'needs_me', description: 'Answer "what needs me?": every open question on a proposed, active or paused item, per product, ordered by what waiting costs. A row on an item that is not active names its lifecycle. Each row is three lines, the question, the item and the cost, with the context folded under it. Computed at read time and never stored. Scope: read, no owner flag needed. Pass the row text back as rendered when the owner answers.', inputSchema: { type: 'object', properties: { product: S('Limit the view to one product id.') }, additionalProperties: false } },
|
|
72
|
+
{ name: 'roundup', description: 'Answer "where does this product stand, and where is it going?" in five parts: 1 needs you (the needs_me read); 2 where we stand (items tagged stage-<n> in ladder order, a count of every item by lifecycle, and the active items); 3 what moved in the window (delivery marks, merged pull requests, new items); 4 stuck or drifting (active items with no change for 14 days, merged but not marked, open pull requests that name no item, questions waiting 14 days); 5 the trend per week for four weeks. Every row cites an item, a question or a pull request. Computed at read time and never stored. Scope: read, no owner flag needed.', inputSchema: { type: 'object', required: ['product'], properties: { product: S('Product id.'), days: { type: 'integer', minimum: 1, maximum: 90, description: 'The window for part 3, in days. Default 7.' } }, additionalProperties: false } },
|
|
72
73
|
{ name: 'code_read_brief', description: 'The brief for the sub-agents that read the code for "where are we with X", one per registered repo of the product. It carries the never-read and never-quote rules, the citation contract and the JSON shape of the digest. Give one brief to one sub-agent; sub-agents never receive this server or the Engram MCP. Pass each digest back in where_are_we.code. When: before answering "where are we with X". Scope: read, no owner flag needed; it opens no repo and writes nothing.', inputSchema: { type: 'object', required: ['product', 'capability'], properties: { product: S('Product registry id.'), capability: S('The capability to read for, the X of the question.'), repos: { type: 'array', description: 'Limit the briefs to these registered repo ids. Default: every repo of the product.', items: { type: 'string' } }, roots: { type: 'object', description: 'Repo id to the local checkout path the sub-agent reads.', additionalProperties: { type: 'string' } }, commits: { type: 'object', description: 'Repo id to the commit the read runs against.', additionalProperties: { type: 'string' } }, focus: { type: 'array', description: 'Extra things the read should look for.', items: { type: 'string' } } }, additionalProperties: false } },
|
|
73
74
|
{ name: 'where_are_we', description: 'Answer "where are we with X" in six parts: live support and known limits from the code read at question time, then in progress, delivered and proposed from the items, then the current priority decision. Every row carries a citation: a path and a line, an entity id, a pull request, or a decision date. The code half comes from the sub-agent digests, which are refused when they cite a never-read path or quote a never-quote path, and every string is scrubbed by pattern. Computed at read time and never stored. Scope: read, no owner flag needed.', inputSchema: { type: 'object', required: ['product', 'capability', 'code'], properties: { product: S('Product registry id.'), capability: S('The capability the question is about, the X.'), code: { type: 'array', description: 'One digest per repo, from the sub-agents briefed by code_read_brief. At least one is required: an answer with no code read says nothing about what the code does today.', items: CODE_DIGEST }, items: { type: 'array', description: 'Item ids the lead judges to answer this question.', items: { type: 'string' } }, match: { type: 'array', description: 'Terms to match against item titles, summaries and tags. Give items, match, or both.', items: { type: 'string' } }, movement_repos: { type: 'array', description: "Narrow the movement read to these registered repo ids. Default: every repo of the product. The digests' own repos are always read.", items: { type: 'string' } }, since: S('ISO date. Adds what changed since then.') }, additionalProperties: false } },
|
|
74
75
|
{ name: 'built_pending_possible', description: 'Answer "what are we building, what is built, what is pending and what can be done": delivered items by date, active items, paused items with their reason, proposed items with their age, and the decisions that set the order. Item records, delivery marks and decisions only; no code read and no GitHub read. Computed at read time and never stored. Scope: read, no owner flag needed.', inputSchema: { type: 'object', properties: { product: S('Limit the view to one product id.'), since: S('ISO date. Limits built and the decisions to what came after it.') }, additionalProperties: false } },
|