@arjunkhera/atlas 0.0.0 → 0.2.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.
Files changed (43) hide show
  1. package/.claude-plugin/plugin.json +18 -0
  2. package/.mcp.json +18 -0
  3. package/README.md +32 -1
  4. package/agents/artifact-format/example.html +71 -0
  5. package/agents/artifact-format/system.css +273 -0
  6. package/agents/artifact-renderer.md +132 -0
  7. package/agents/capability-reader.md +100 -0
  8. package/agents/code-explorer.md +108 -0
  9. package/agents/reviewer-architect.md +78 -0
  10. package/agents/reviewer-pm.md +81 -0
  11. package/agents/reviewer-security.md +82 -0
  12. package/bin/atlas +5 -0
  13. package/door/cli.mjs +280 -0
  14. package/door/lib/helpers.mjs +139 -0
  15. package/door/lib/install.mjs +260 -0
  16. package/door/lib/kind-drift.mjs +76 -0
  17. package/door/lib/onboarding.mjs +130 -0
  18. package/door/lib/pull-request.mjs +119 -0
  19. package/door/lib/releases.mjs +42 -0
  20. package/door/lib/status.mjs +47 -0
  21. package/door/lib/tooling.mjs +119 -0
  22. package/package.json +24 -3
  23. package/shape/check.mjs +1797 -0
  24. package/shape/releases.json +15 -0
  25. package/skills/sdlc-task/SKILL.md +174 -0
  26. package/skills/sdlc-task/lifecycle.yaml +27 -0
  27. package/skills/sdlc-task/templates/design-doc.md +118 -0
  28. package/skills/sdlc-task/templates/resume-anchor.md +25 -0
  29. package/work/code-read.mjs +65 -0
  30. package/work/lib/action-store.mjs +84 -0
  31. package/work/lib/capability.mjs +487 -0
  32. package/work/lib/child-env.mjs +11 -0
  33. package/work/lib/client.mjs +214 -0
  34. package/work/lib/code-read.mjs +238 -0
  35. package/work/lib/delivery.mjs +624 -0
  36. package/work/lib/mcp-stdio.mjs +68 -0
  37. package/work/lib/questions.mjs +278 -0
  38. package/work/lib/sweep.mjs +189 -0
  39. package/work/lib/verb-fields.mjs +53 -0
  40. package/work/lib/verbs.mjs +511 -0
  41. package/work/manifest-0.5.0.json +609 -0
  42. package/work/mcp.mjs +216 -0
  43. package/work/protected-paths.json +38 -0
@@ -0,0 +1,487 @@
1
+ // Atlas M1 slice L4: "where are we with X" and "Built, pending, possible".
2
+ // Design: the Atlas M1 study, page four (the six-part
3
+ // answer and its sources), page five (what the owner asked Q3 to become), D3
4
+ // (the views), D6 findings R12 and R14, and the frozen L4 paragraph of the
5
+ // lock block.
6
+ //
7
+ // Three rules hold this slice together:
8
+ // 1. The answer is computed at question time and never stored (D3). Nothing
9
+ // here writes a record.
10
+ // 2. Every row carries a citation: a path and a line for a code claim, an
11
+ // entity id for a graph claim, a pull request for a delivery, a date for
12
+ // a decision. A claim with no citation does not enter the answer.
13
+ // 3. The code half comes from sub-agents through acceptCodeRead, which
14
+ // refuses a never-read path, refuses a quote from a never-quoted path and
15
+ // scrubs by pattern. The rendered answer is scrubbed once more before it
16
+ // leaves the door (R12).
17
+ import { refuse, text, isoDate, stringList } from './verb-fields.mjs';
18
+ import { oneLine, scrubSecrets, ROW_LINE_MAX } from './questions.mjs';
19
+ import { listLine, legacySealOf, marksByItem, movementOf, pullsOfItem, readPullsByRepo, correctionsOf, daysBetween } from './delivery.mjs';
20
+ import { acceptCodeRead, codeReadBrief } from './code-read.mjs';
21
+
22
+ export const CAPABILITY_READ_VERBS = Object.freeze(['code_read_brief', 'where_are_we', 'built_pending_possible']);
23
+
24
+ const MAX_DIGESTS = 8;
25
+ const MAX_TERMS = 12;
26
+ const MAX_MATCHED = 60;
27
+ const MAX_ITEMS = 500;
28
+ const MAX_REPOS = 16;
29
+ const MAX_DECISIONS = 300;
30
+ const SHOWN = 12;
31
+ const OMITTED_KEPT = 25;
32
+
33
+ const shortCommit = (commit) => String(commit ?? '').slice(0, 7);
34
+ const day = (value) => (value ? String(value).slice(0, 10) : 'no date on record');
35
+ const plural = (count, word) => `${count} ${word}${count === 1 ? '' : 's'}`;
36
+ // A delivery can carry several pull requests. The line names the first and
37
+ // counts the rest, so a list of URLs never eats the title (review H13).
38
+ const prsOf = (row) => (row.prs?.length ? `${row.prs[0]}${row.prs.length > 1 ? ` +${row.prs.length - 1}` : ''}` : (row.source || 'no pull request on the mark'));
39
+ export const codeCitation = (row) => `${row.repo}@${shortCommit(row.commit)} ${row.path}:${row.line}`;
40
+
41
+ // Parts three, four and five read items. An item belongs to the answer when the
42
+ // lead names it, or when a term the lead gave appears in its title, summary or
43
+ // tags. The lead judges the match, as the owner's cut decided
44
+ // (study page 4.4): there is no link field from a capability to an item.
45
+ export function matchOf(item, terms) {
46
+ const fields = item.fields ?? {};
47
+ const hay = `${item.title ?? ''}\n${fields.summary ?? ''}\n${(item.tags ?? []).join(' ')}`.toLowerCase();
48
+ const hit = terms.find((term) => hay.includes(term.toLowerCase()));
49
+ return hit ? `term "${hit}"` : null;
50
+ }
51
+
52
+ export function createCapabilityVerbs(core) {
53
+ const { client, now, typeNamed, registry, requireProduct, requireItem, readGitHub } = core;
54
+
55
+ const productRepos = (reg, productId) => [...reg.repos.values()].filter((repo) => repo.fields?.product === productId).map((repo) => repo.fields.registry_id).sort();
56
+
57
+ // How a row names its product (D3 rule 2). An item that names no registered
58
+ // product is never given one here: it is carried as "unregistered", and the
59
+ // slice L5 sweep is what gives it a product. An item that names a product the
60
+ // registry does not hold keeps that name too, so a row never says the record
61
+ // is silent when it is not (review H8).
62
+ const productOf = (item, reg) => {
63
+ const named = item.fields?.product;
64
+ if (typeof named === 'string' && reg.products.has(named)) return { product: named, named: null };
65
+ return { product: 'unregistered', named: typeof named === 'string' && named.trim() ? named : null };
66
+ };
67
+ const unregisteredNote = (named) => (named
68
+ ? `the item names product "${named}", which is not in the registry; the slice L5 sweep gives it a registered one`
69
+ : 'the item names no product; the slice L5 sweep gives it one');
70
+
71
+ async function allItems() {
72
+ const container = await typeNamed('container');
73
+ const rows = await client.listAllEntities({ type: container.name, state: 'active' });
74
+ if (rows.length > MAX_ITEMS) refuse(`${rows.length} items are in the graph; a view reads at most ${MAX_ITEMS}`);
75
+ return rows;
76
+ }
77
+
78
+ async function marksFor(itemIds) {
79
+ const type = await typeNamed('mark');
80
+ return marksByItem(await client.listAllEntities({ type: type.name, state: 'active' }), itemIds);
81
+ }
82
+
83
+ async function decisionsFor(productId) {
84
+ const type = await typeNamed('decision');
85
+ const rows = await client.listAllEntities({ type: type.name, state: 'active' });
86
+ if (rows.length > MAX_DECISIONS) refuse(`${rows.length} decisions are recorded; a view reads at most ${MAX_DECISIONS}`);
87
+ return rows.filter((row) => row.fields?.product === productId)
88
+ .sort((a, b) => String(b.fields?.date ?? '').localeCompare(String(a.fields?.date ?? '')) || String(b.created_at ?? '').localeCompare(String(a.created_at ?? '')));
89
+ }
90
+
91
+ // A delivery on an item, as parts four reads it: the standing marks, the
92
+ // marks a reopen corrected (named, never hidden), and the legacy seal of the
93
+ // first slice (R14).
94
+ function deliveriesOf(item, marks) {
95
+ const own = marks.get(item.id) ?? [];
96
+ const correctedBy = correctionsOf(own);
97
+ const rows = own.filter((mark) => mark.fields?.kind === 'delivery').map((mark) => ({
98
+ mark_id: mark.id, verdict: correctedBy.has(mark.id) ? 'corrected' : (mark.fields?.verdict ?? 'delivered'),
99
+ ...(correctedBy.has(mark.id) ? { corrected_by: correctedBy.get(mark.id) } : {}),
100
+ delivered_at: mark.fields?.marked_at ?? null, prs: (mark.fields?.prs ?? []).map((pull) => pull.url),
101
+ deployed_commit: mark.fields?.deployed?.commit ?? null, gate_warnings: mark.fields?.gate_warnings ?? [],
102
+ }));
103
+ // R14: a legacy seal reads as a delivery mark. Only a delivery mark
104
+ // replaces it. A reopen, or any later mark kind, must not erase it
105
+ // (review H9).
106
+ const legacy = rows.length ? null : legacySealOf(item);
107
+ if (legacy) rows.push({ mark_id: null, verdict: 'legacy', delivered_at: legacy.marked_at, prs: [], deployed_commit: null, source: legacy.source });
108
+ return { rows: rows.sort((a, b) => String(b.delivered_at ?? '').localeCompare(String(a.delivered_at ?? ''))), marks: own };
109
+ }
110
+
111
+ const verbs = {
112
+ // The brief the sub-agents are given, one per repo of the product. Read
113
+ // only: it writes nothing and reads no repo. It exists so the deny rules
114
+ // and the citation contract come from one place, and so the evidence of an
115
+ // answer shows what its readers were told (D6 R3, R12).
116
+ async code_read_brief(args = {}) {
117
+ const reg = await registry();
118
+ const product = await requireProduct(args.product, reg);
119
+ const productId = product.fields.registry_id;
120
+ const capability = text(args.capability, 'capability', { max: 200 });
121
+ const known = productRepos(reg, productId);
122
+ if (!known.length) refuse(`product ${productId} has no registered repo; register one with repo_add before a code read`);
123
+ const wanted = args.repos === undefined ? known : stringList(args.repos, 'repos', { max: MAX_REPOS });
124
+ for (const repo of wanted) if (!known.includes(repo)) refuse(`unknown repo id: ${repo}. Registered for ${productId}: ${known.join(', ')}`);
125
+ // The answer takes at most MAX_DIGESTS digests. A brief above that number
126
+ // would spend a sub-agent read that no answer could use, so the cost is
127
+ // refused before it is paid (review H7).
128
+ if (wanted.length > MAX_DIGESTS) refuse(`${wanted.length} repos are registered for ${productId}, and an answer reads at most ${MAX_DIGESTS} digests. Name the repos to read with "repos".`);
129
+ const roots = args.roots === undefined ? {} : args.roots;
130
+ if (roots === null || typeof roots !== 'object' || Array.isArray(roots)) refuse('roots must be an object: repo id to the local checkout path');
131
+ const focus = args.focus === undefined ? [] : stringList(args.focus, 'focus', { max: 16 });
132
+ const commits = args.commits === undefined ? {} : args.commits;
133
+ if (commits === null || typeof commits !== 'object' || Array.isArray(commits)) refuse('commits must be an object: repo id to the commit the read runs against');
134
+ const briefs = wanted.map((repo) => {
135
+ const record = reg.repos.get(repo);
136
+ return codeReadBrief({
137
+ capability, repo, root: roots[repo] ? text(roots[repo], `roots.${repo}`, { max: 1000 }) : null,
138
+ commit: commits[repo] ? text(commits[repo], `commits.${repo}`, { max: 40 }) : null,
139
+ defaultBranch: record?.fields?.default_branch ?? null, focus,
140
+ });
141
+ });
142
+ return {
143
+ verb: 'code_read_brief', read_at: now(), product: productId, capability, repos: wanted, briefs,
144
+ note: 'Give one brief to one sub-agent. Sub-agents never receive the verb server or the Engram MCP. Pass each returned digest back in where_are_we.code.',
145
+ };
146
+ },
147
+
148
+ // D3: "where are we with X". The six parts of study page 4.2, each from the
149
+ // source that page names. Computed at question time, never stored.
150
+ async where_are_we(args = {}) {
151
+ const reg = await registry();
152
+ const product = await requireProduct(args.product, reg);
153
+ const productId = product.fields.registry_id;
154
+ const capability = text(args.capability, 'capability', { max: 200 });
155
+ const since = args.since === undefined ? null : isoDate(args.since, 'since');
156
+ const repos = productRepos(reg, productId);
157
+
158
+ // Parts one and two. An answer with no code read is the failure the study
159
+ // names on page four: the earlier answers came from docs and the graph,
160
+ // and they said nothing about what the code does today.
161
+ if (!Array.isArray(args.code) || !args.code.length) refuse('code must hold at least one sub-agent digest: parts one and two of the answer come from the code, read at question time. Call code_read_brief first.');
162
+ if (args.code.length > MAX_DIGESTS) refuse(`code holds more than ${MAX_DIGESTS} digests`);
163
+ let redactions = 0;
164
+ const digests = args.code.map((entry, index) => {
165
+ const accepted = acceptCodeRead(entry, { repos, name: `code[${index}]` });
166
+ redactions += accepted.redactions;
167
+ return accepted.digest;
168
+ });
169
+ const seen = new Set();
170
+ for (const digest of digests) {
171
+ if (seen.has(digest.repo)) refuse(`code holds two digests for ${digest.repo}; one read per repo`);
172
+ seen.add(digest.repo);
173
+ }
174
+ const supports = digests.flatMap((digest) => digest.supports.map((row) => ({ ...row, commit: digest.commit })));
175
+ const limits = digests.flatMap((digest) => digest.limits.map((row) => ({ ...row, commit: digest.commit })));
176
+
177
+ // Parts three, four and five. The lead names the items, or gives terms to
178
+ // match. One of the two is required: an answer needs a basis.
179
+ const named = args.items === undefined ? [] : stringList(args.items, 'items', { max: MAX_MATCHED });
180
+ const terms = args.match === undefined ? [] : stringList(args.match, 'match', { max: MAX_TERMS }).map((term) => text(term, 'match', { max: 100 }));
181
+ if (!named.length && !terms.length) refuse('name the items with items, or give match terms, or both: the lead judges which items answer this question (study page 4.4)');
182
+ const rows = await allItems();
183
+ const byId = new Map(rows.map((row) => [row.id, row]));
184
+ const picked = new Map(), omitted = [];
185
+ const belongs = (item) => {
186
+ const owner = item.fields?.product;
187
+ return !(typeof owner === 'string' && reg.products.has(owner) && owner !== productId);
188
+ };
189
+ for (const id of named) {
190
+ const item = byId.get(id) ?? await requireItem(id);
191
+ if (!belongs(item)) refuse(`item ${id} belongs to product ${item.fields.product}, not ${productId}`);
192
+ picked.set(item.id, { item, matched_by: 'named by the lead' });
193
+ }
194
+ for (const row of rows) {
195
+ if (picked.has(row.id)) continue;
196
+ const how = matchOf(row, terms);
197
+ if (!how) continue;
198
+ if (!belongs(row)) { omitted.push({ item: row.id, title: row.title, product_named: row.fields.product, reason: null }); continue; }
199
+ picked.set(row.id, { item: row, matched_by: how });
200
+ }
201
+ if (picked.size > MAX_MATCHED) refuse(`${picked.size} items match; the answer reads at most ${MAX_MATCHED}. Narrow the terms or name the items.`);
202
+ const items = [...picked.values()];
203
+ const marks = await marksFor(new Set(picked.keys()));
204
+
205
+ // Movement for part three, from git through the repo ids and the item
206
+ // links, exactly as the "In flight" view reads it (D3 rule 5, R1).
207
+ // Movement reads the product's repos and the repos the code read covered.
208
+ // A product with more than MAX_REPOS repos would otherwise have no answer
209
+ // at all, so the caller may narrow the movement read to the repos that
210
+ // matter (review J2). The digests' own repos are always read.
211
+ const wantedRepos = args.movement_repos === undefined ? null : stringList(args.movement_repos, 'movement_repos', { max: MAX_REPOS });
212
+ if (wantedRepos) for (const repo of wantedRepos) if (!reg.repos.has(repo)) refuse(`unknown repo id: ${repo}. Known: ${[...reg.repos.keys()].sort().join(', ') || 'none'}`);
213
+ const repoIds = [...new Set([...(wantedRepos ?? repos), ...digests.map((digest) => digest.repo)])].filter((repo) => reg.repos.has(repo)).sort();
214
+ if (repoIds.length > MAX_REPOS) refuse(`${repoIds.length} repos would be read for movement; an answer reads at most ${MAX_REPOS}. Name the repos to read with "movement_repos".`);
215
+ const { pullsByRepo, repoErrors } = await readPullsByRepo(repoIds, readGitHub);
216
+ // Which registered repos the code half did not read. Parts one and two
217
+ // are then a partial picture, and the answer says so (review H4).
218
+ const unread = repos.filter((repo) => !digests.some((digest) => digest.repo === repo));
219
+
220
+ const readAt = now();
221
+ const clean = (value, max = 200) => { const out = scrubSecrets(oneLine(value, max)); redactions += out.redactions; return out.text; };
222
+ // A gate warning comes from the mark, so it is graph text like any other
223
+ // and passes the same scrub (review H2).
224
+ const cleanWarnings = (warnings) => (Array.isArray(warnings) ? warnings : []).map((warning) => (warning && typeof warning === 'object'
225
+ ? Object.fromEntries(Object.entries(warning).map(([key, value]) => [key, typeof value === 'string' ? clean(value, 500) : value]))
226
+ : clean(String(warning ?? ''), 500)));
227
+ const inProgress = [], delivered = [], proposed = [];
228
+ for (const { item, matched_by: matchedBy } of items) {
229
+ const fields = item.fields ?? {};
230
+ const lifecycle = fields.lifecycle ?? 'unknown';
231
+ const where = productOf(item, reg);
232
+ const pulls = pullsOfItem(item, pullsByRepo, repoIds);
233
+ const { rows: deliveries, marks: own } = deliveriesOf(item, marks);
234
+ const { last, days } = movementOf({ item, marks: own, pulls, legacy: deliveries.some((row) => row.verdict === 'legacy') ? { marked_at: deliveries.find((row) => row.verdict === 'legacy').delivered_at } : null, readAt });
235
+ const base = {
236
+ item: item.id, title: clean(item.title, 160), product: where.product, ...(where.named ? { product_named: clean(where.named, 64) } : {}), matched_by: matchedBy,
237
+ citation: { item: item.id, read_at: readAt, ...(where.product === 'unregistered' ? { note: unregisteredNote(where.named && clean(where.named, 64)) } : {}) },
238
+ };
239
+ // Part four is every delivery this answer knows of, whatever its date.
240
+ // A date narrows the "changed since" line below, not the answer: the
241
+ // owner asking "where are we with X" is asking about the whole of X
242
+ // (study page 4.2).
243
+ for (const delivery of deliveries) delivered.push({ ...base, ...delivery, prs: delivery.prs.map((url) => clean(url, 500)), gate_warnings: cleanWarnings(delivery.gate_warnings), citation: { ...base.citation, mark: delivery.mark_id, prs: delivery.prs.map((url) => clean(url, 500)) } });
244
+ // A done item with no mark and no legacy seal is still delivered work.
245
+ // The row says what is missing rather than dropping the item, and the
246
+ // done verb is what catches it up (R2).
247
+ // The graph's list rows carry created_at but not always updated_at, so
248
+ // the date falls back rather than reading as no date at all.
249
+ const doneAt = fields.delivered_at ?? item.updated_at ?? item.created_at ?? null;
250
+ if (lifecycle === 'done' && !deliveries.length) delivered.push({ ...base, mark_id: null, verdict: 'done, no delivery mark', delivered_at: doneAt, prs: pulls.filter((pull) => pull.state === 'merged').map((pull) => clean(pull.url, 500)), deployed_commit: null, source: 'no mark yet', note: 'the item record says done; the done verb writes the mark' });
251
+ if (lifecycle === 'active') {
252
+ inProgress.push({
253
+ ...base, stage: clean(fields.stage ?? '', 64), driver: clean(fields.driver ?? '', 64), next_step: clean(fields.next_step ?? '', 200),
254
+ days_since_last_change: days, last_change_at: last?.at ?? null, last_change_source: clean(last?.source ?? 'nothing recorded', 120),
255
+ delivery_state: fields.delivery_state ? clean(fields.delivery_state, 64) : null, prs: pulls.map((pull) => ({ url: clean(pull.url, 500), state: clean(pull.state, 32), matched_by: pull.matched_by })),
256
+ citation: { ...base.citation, repos: [...new Set(pulls.map((pull) => pull.repo))] },
257
+ });
258
+ } else if (lifecycle === 'proposed') {
259
+ const born = fields.created_at ?? item.created_at ?? null;
260
+ proposed.push({ ...base, age_days: born ? daysBetween(born, readAt) : null, proposed_at: born, driver: clean(fields.driver ?? '', 64) });
261
+ } else if (lifecycle !== 'done' && !deliveries.length) {
262
+ omitted.push({ item: item.id, title: clean(item.title, 120), reason: `the item is ${clean(lifecycle, 32)}; paused and archived items are backlog (D3 rule 1)` });
263
+ }
264
+ }
265
+ inProgress.sort((a, b) => (a.days_since_last_change ?? Number.MAX_SAFE_INTEGER) - (b.days_since_last_change ?? Number.MAX_SAFE_INTEGER) || a.item.localeCompare(b.item));
266
+ delivered.sort((a, b) => String(b.delivered_at ?? '').localeCompare(String(a.delivered_at ?? '')));
267
+ proposed.sort((a, b) => (b.age_days ?? 0) - (a.age_days ?? 0));
268
+
269
+ // Part six: the current priority decision, or "none recorded". A decision
270
+ // that touches one of these items is named too, whatever its kind.
271
+ const decisions = await decisionsFor(productId);
272
+ const touched = new Set(picked.keys());
273
+ const priority = decisions.filter((row) => row.fields?.kind === 'priority');
274
+ const related = decisions.filter((row) => Array.isArray(row.fields?.items) && row.fields.items.some((id) => touched.has(id)));
275
+ const decisionRow = (row, why) => ({ decision: row.id, kind: row.fields?.kind ?? 'decision', date: row.fields?.date ?? '', text: clean(row.fields?.text ?? row.title, 400), why, priority_order: row.fields?.priority_order ?? [], citation: { decision: row.id, date: row.fields?.date ?? '', source: row.fields?.source ?? '' } });
276
+ const priorityRows = [
277
+ ...(priority[0] ? [decisionRow(priority[0], 'the latest priority decision for this product')] : []),
278
+ ...related.filter((row) => row.id !== priority[0]?.id).slice(0, SHOWN).map((row) => decisionRow(row, 'related, not a priority decision: it names one of these items')),
279
+ ];
280
+
281
+ const unknowns = [
282
+ ...digests.flatMap((digest) => digest.unknowns.map((line) => `${digest.repo}: ${line}`)),
283
+ ...repoErrors.map((row) => `movement for ${row.repo} is from the graph only: ${row.reason}`),
284
+ // The warning depends on the priority decisions alone. A related
285
+ // decision of another kind must not silence it (review H3).
286
+ ...(priority.length ? [] : ['no priority decision is recorded for this product']),
287
+ // Which registered repos the code half did not read (review H4).
288
+ ...unread.map((repo) => `the code half did not read ${repo}; parts one and two do not cover it`),
289
+ ];
290
+ const unregistered = [...new Set([...inProgress, ...delivered, ...proposed].filter((row) => row.product === 'unregistered').map((row) => row.item))];
291
+ if (unregistered.length) unknowns.push(`${unregistered.length} of these items name no registered product; the slice L5 sweep gives them one`);
292
+
293
+ // The omitted list is scrubbed here, because the match loop runs before
294
+ // the scrub exists, and it is bounded, because a wide term can omit many
295
+ // items and a long list is not a view (reviews H2 and H16).
296
+ // Every string of every row passes the scrub, whatever field it came from
297
+ // and however deep it sits. Field-by-field scrubbing kept missing one
298
+ // (reviews H2, J1, K1), so the rows are walked instead: one rule, no list
299
+ // to keep up to date. The rendered text is scrubbed again at the end.
300
+ // The walk scrubs, but it does not flatten: a path may hold a space, and
301
+ // a flattened path no longer resolves (review L2). Rendering is where
302
+ // whitespace is collapsed, not storage.
303
+ const cleanDeep = (value) => {
304
+ if (typeof value === 'string') {
305
+ const out = scrubSecrets(value.length > 4000 ? `${value.slice(0, 3999)}…` : value);
306
+ redactions += out.redactions;
307
+ return out.text;
308
+ }
309
+ if (Array.isArray(value)) return value.map(cleanDeep);
310
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cleanDeep(entry)]));
311
+ return value;
312
+ };
313
+ // The rendered block keeps its line breaks: it is scrubbed whole, on its
314
+ // own, and it must not pass through the one-line flattener. The count is
315
+ // read after the walk, so the walk's own finds are counted too (L1).
316
+ const scrubbed = ({ text, ...rest }) => { const rows = cleanDeep(rest); return { ...rows, redactions: redactions + rendered.redactions, text }; };
317
+ const omittedTotal = omitted.length;
318
+ const omittedRows = omitted.slice(0, OMITTED_KEPT).map((row) => ({
319
+ item: row.item, title: clean(row.title, 120),
320
+ reason: row.reason ?? `the item belongs to product ${clean(row.product_named ?? '', 64)}`,
321
+ }));
322
+
323
+ const changed = since ? {
324
+ since,
325
+ delivered: delivered.filter((row) => String(row.delivered_at ?? '') >= since).length,
326
+ locked: items.filter(({ item }) => String(item.fields?.locked_at ?? '') >= since).map(({ item }) => item.id),
327
+ merged_prs: [...new Set(items.flatMap(({ item }) => pullsOfItem(item, pullsByRepo, repoIds).filter((pull) => pull.state === 'merged' && String(pull.merged_at ?? '') >= since).map((pull) => pull.url)))],
328
+ } : null;
329
+
330
+ // Why a row is in the answer: the lead named the item, or a term matched
331
+ // it. The owner reads the reason on the row, and never has to open a
332
+ // source to learn why it is there.
333
+ const why = (row) => (row.matched_by === 'named by the lead' ? 'named' : row.matched_by);
334
+ const part = (number, title, count, lines) => [`${number}. ${title} (${count})`, ...(lines.length ? lines : [' nothing recorded'])].join('\n');
335
+ const more = (list) => (list.length > SHOWN ? [` and ${list.length - SHOWN} more, in the rows`] : []);
336
+ const block = [
337
+ `Where are we with ${capability}? — ${productId} · read ${readAt.slice(0, 19)}Z · code at ${digests.map((digest) => `${digest.repo}@${shortCommit(digest.commit)}`).join(', ')}`,
338
+ part(1, 'Live support, from the code', supports.length, [...supports.slice(0, SHOWN).map((row) => listLine(`${row.name} — ${row.statement}`, codeCitation(row))), ...more(supports)]),
339
+ part(2, 'Known limits, from the code', limits.length, [...limits.slice(0, SHOWN).map((row) => listLine(`${row.kind}: ${row.name} — ${row.statement}`, codeCitation(row))), ...more(limits)]),
340
+ part(3, 'In progress', inProgress.length, [...inProgress.slice(0, SHOWN).map((row) => listLine(`${row.title} · ${row.stage || 'no stage'} · next: ${row.next_step || 'not set'}`, `${row.item} · ${row.days_since_last_change === null ? 'no movement on record' : `${row.days_since_last_change} day${row.days_since_last_change === 1 ? '' : 's'} since ${row.last_change_source}`} · ${why(row)}`)), ...more(inProgress)]),
341
+ part(4, 'Delivered', delivered.length, [...delivered.slice(0, SHOWN).map((row) => listLine(row.title, `${row.verdict} ${day(row.delivered_at)} · ${prsOf(row)} · ${row.item}${row.mark_id ? ` · mark ${row.mark_id.slice(0, 8)}` : ''}`)), ...more(delivered)]),
342
+ part(5, 'Proposed, with age', proposed.length, [...proposed.slice(0, SHOWN).map((row) => listLine(row.title, `${row.age_days === null ? 'age unknown' : `${row.age_days} days old`} · ${row.item} · ${why(row)}`)), ...more(proposed)]),
343
+ part(6, 'The current priority decision', priorityRows.length, priorityRows.length ? priorityRows.map((row) => listLine(`${row.kind}: ${row.text}`, `${row.date} · ${row.why} · ${row.decision}`)) : [' none recorded']),
344
+ ...(changed ? [`Changed since ${since}: ${changed.delivered} delivered, ${changed.locked.length} locked, ${plural(changed.merged_prs.length, 'pull request')} merged`] : []),
345
+ ...(unknowns.length ? [['Unknowns', ...unknowns.slice(0, SHOWN).map((line) => oneLine(` ${line}`, ROW_LINE_MAX)), ...(unknowns.length > SHOWN ? [` and ${unknowns.length - SHOWN} more, in the rows`] : [])].join('\n')] : []),
346
+ ...(omittedTotal ? [[`Omitted (${omittedTotal})`, ...omittedRows.slice(0, SHOWN).map((row) => listLine(row.title, row.reason)), ...(omittedTotal > SHOWN ? [` and ${omittedTotal - SHOWN} more, ${Math.max(0, omittedRows.length - SHOWN)} of them in this answer`] : [])].join('\n')] : []),
347
+ ].join('\n\n');
348
+ const rendered = scrubSecrets(block);
349
+ return scrubbed({
350
+ verb: 'where_are_we', read_at: readAt, product: productId, capability, since,
351
+ code: digests.map((digest) => ({ repo: digest.repo, commit: digest.commit, files_read: digest.files_read.length, supports: digest.supports.length, limits: digest.limits.length, read_by: digest.read_by })),
352
+ parts: [
353
+ { part: 1, title: 'Live support, from the code', source: 'code, read at question time by sub-agents', rows: supports },
354
+ { part: 2, title: 'Known limits, from the code', source: 'code, read at question time by sub-agents', rows: limits },
355
+ { part: 3, title: 'In progress', source: 'item records, plus git for movement', rows: inProgress },
356
+ { part: 4, title: 'Delivered', source: 'item records and delivery marks', rows: delivered },
357
+ { part: 5, title: 'Proposed, with age', source: 'item records', rows: proposed },
358
+ { part: 6, title: 'The current priority decision', source: 'decision records', rows: priorityRows },
359
+ ],
360
+ items_matched: items.length, omitted: omittedRows, omitted_total: omittedTotal, unknowns, changed_since: changed, repos_unread: unread,
361
+ repos_read: repoIds, repo_errors: repoErrors, redactions: redactions + rendered.redactions,
362
+ citations: supports.length + limits.length + inProgress.length + delivered.length + proposed.length + priorityRows.length,
363
+ text: rendered.text,
364
+ });
365
+ },
366
+
367
+ // D3: "Built, pending, possible", the view the owner asked Q3 to become on
368
+ // 2026-09-16: "what all are we building, what all is built, what is yet to
369
+ // be done and pending and what can be done". Item records, delivery marks
370
+ // and decisions only. No code read, no GitHub read.
371
+ async built_pending_possible(args = {}) {
372
+ const reg = await registry();
373
+ let only = null;
374
+ if (args.product !== undefined) only = (await requireProduct(args.product, reg)).fields.registry_id;
375
+ const since = args.since === undefined ? null : isoDate(args.since, 'since');
376
+ const rows = await allItems();
377
+ const registered = (row) => typeof row.fields?.product === 'string' && reg.products.has(row.fields.product);
378
+ // A read narrowed to one product keeps that product's items only. A wide
379
+ // read keeps every item, because eighty-eight of the tenant's ninety name
380
+ // no registered product, and a board that drops them answers nothing
381
+ // before the slice L5 sweep (review H5). No row is given a product it
382
+ // does not carry: an item with none reads "unregistered", as the six-part
383
+ // answer already renders it.
384
+ const items = only ? rows.filter((row) => row.fields?.product === only) : rows;
385
+ const unregisteredTotal = rows.filter((row) => !registered(row)).length;
386
+ const unregisteredShown = items.filter((row) => !registered(row)).length;
387
+ const marks = await marksFor(new Set(items.map((row) => row.id)));
388
+ const readAt = now();
389
+ let redactions = 0;
390
+ const clean = (value, max = 200) => { const out = scrubSecrets(oneLine(value, max)); redactions += out.redactions; return out.text; };
391
+ // A gate warning comes from the mark, so it is graph text like any other
392
+ // and passes the same scrub (review H2).
393
+ const cleanWarnings = (warnings) => (Array.isArray(warnings) ? warnings : []).map((warning) => (warning && typeof warning === 'object'
394
+ ? Object.fromEntries(Object.entries(warning).map(([key, value]) => [key, typeof value === 'string' ? clean(value, 500) : value]))
395
+ : clean(String(warning ?? ''), 500)));
396
+
397
+ // Every string of every row passes the scrub, whatever field it came from
398
+ // and however deep it sits. Field-by-field scrubbing kept missing one
399
+ // (reviews H2, J1, K1), so the rows are walked instead: one rule, no list
400
+ // to keep up to date. The rendered text is scrubbed again at the end.
401
+ // The walk scrubs, but it does not flatten: a path may hold a space, and
402
+ // a flattened path no longer resolves (review L2). Rendering is where
403
+ // whitespace is collapsed, not storage.
404
+ const cleanDeep = (value) => {
405
+ if (typeof value === 'string') {
406
+ const out = scrubSecrets(value.length > 4000 ? `${value.slice(0, 3999)}…` : value);
407
+ redactions += out.redactions;
408
+ return out.text;
409
+ }
410
+ if (Array.isArray(value)) return value.map(cleanDeep);
411
+ if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cleanDeep(entry)]));
412
+ return value;
413
+ };
414
+ // The rendered block keeps its line breaks: it is scrubbed whole, on its
415
+ // own, and it must not pass through the one-line flattener. The count is
416
+ // read after the walk, so the walk's own finds are counted too (L1).
417
+ const scrubbed = ({ text, ...rest }) => { const rows = cleanDeep(rest); return { ...rows, redactions: redactions + rendered.redactions, text }; };
418
+ const built = [], building = [], pending = [], possible = [];
419
+ for (const item of items) {
420
+ const fields = item.fields ?? {};
421
+ const { rows: deliveries, marks: own } = deliveriesOf(item, marks);
422
+ const { last, days } = movementOf({ item, marks: own, legacy: deliveries.some((row) => row.verdict === 'legacy') ? { marked_at: deliveries.find((row) => row.verdict === 'legacy').delivered_at } : null, readAt });
423
+ const where = productOf(item, reg);
424
+ const base = {
425
+ item: item.id, title: clean(item.title, 160), product: where.product, ...(where.named ? { product_named: clean(where.named, 64) } : {}),
426
+ citation: { item: item.id, read_at: readAt, ...(where.product === 'unregistered' ? { note: unregisteredNote(where.named && clean(where.named, 64)) } : {}) },
427
+ };
428
+ for (const delivery of deliveries) {
429
+ if (since && String(delivery.delivered_at ?? '') < since) continue;
430
+ built.push({ ...base, ...delivery, prs: delivery.prs.map((url) => clean(url, 500)), gate_warnings: cleanWarnings(delivery.gate_warnings), citation: { ...base.citation, mark: delivery.mark_id, prs: delivery.prs.map((url) => clean(url, 500)) } });
431
+ }
432
+ const doneAt = fields.delivered_at ?? item.updated_at ?? item.created_at ?? null;
433
+ if (fields.lifecycle === 'done' && !deliveries.length && !(since && String(doneAt ?? '') < since)) built.push({ ...base, mark_id: null, verdict: 'done, no delivery mark', delivered_at: doneAt, prs: [], source: 'no mark yet', note: 'the item record says done; the done verb writes the mark' });
434
+ if (fields.lifecycle === 'active') building.push({ ...base, stage: clean(fields.stage ?? '', 64), driver: clean(fields.driver ?? '', 64), next_step: clean(fields.next_step ?? '', 200), days_since_last_change: days, last_change_source: clean(last?.source ?? 'nothing recorded', 120), delivery_state: fields.delivery_state ? clean(fields.delivery_state, 64) : null });
435
+ else if (fields.lifecycle === 'paused') pending.push({ ...base, reason: clean(fields.pause_reason ?? 'no reason on record', 300), days_since_last_change: days, parked_at: fields.parked_at ?? null });
436
+ else if (fields.lifecycle === 'proposed') {
437
+ const born = fields.created_at ?? item.created_at ?? null;
438
+ possible.push({ ...base, age_days: born ? daysBetween(born, readAt) : null, proposed_at: born, driver: clean(fields.driver ?? '', 64) });
439
+ }
440
+ }
441
+ built.sort((a, b) => String(b.delivered_at ?? '').localeCompare(String(a.delivered_at ?? '')));
442
+ building.sort((a, b) => (a.days_since_last_change ?? Number.MAX_SAFE_INTEGER) - (b.days_since_last_change ?? Number.MAX_SAFE_INTEGER) || a.item.localeCompare(b.item));
443
+ pending.sort((a, b) => (a.days_since_last_change ?? Number.MAX_SAFE_INTEGER) - (b.days_since_last_change ?? Number.MAX_SAFE_INTEGER) || a.item.localeCompare(b.item));
444
+ possible.sort((a, b) => (b.age_days ?? 0) - (a.age_days ?? 0));
445
+
446
+ // The decisions that set the order. The owner's words are what ordered
447
+ // this work, so the view ends with them, newest first.
448
+ const products = only ? [only] : [...new Set(items.filter(registered).map((row) => row.fields.product))];
449
+ const order = [];
450
+ for (const productId of products) {
451
+ for (const row of await decisionsFor(productId)) {
452
+ if (since && String(row.fields?.date ?? '') < since) continue;
453
+ if (row.fields?.kind === 'priority' || (Array.isArray(row.fields?.priority_order) && row.fields.priority_order.length)) {
454
+ order.push({ decision: row.id, product: productId, kind: row.fields?.kind ?? 'decision', date: row.fields?.date ?? '', text: clean(row.fields?.text ?? row.title, 400), priority_order: row.fields?.priority_order ?? [], citation: { decision: row.id, date: row.fields?.date ?? '', source: row.fields?.source ?? '' } });
455
+ }
456
+ }
457
+ }
458
+ order.sort((a, b) => String(b.date).localeCompare(String(a.date)));
459
+
460
+ // D3 rule 2: a row names its product. The board does not group, so every
461
+ // line carries the id (review H6).
462
+ const where_ = (row) => (row.product_named ? `${row.product_named} (not registered)` : row.product);
463
+ const section = (title, list, line) => [`${title} (${list.length})`, ...(list.length ? [...list.slice(0, SHOWN).map(line), ...(list.length > SHOWN ? [` and ${list.length - SHOWN} more, in the rows`] : [])] : [' nothing recorded'])].join('\n');
464
+ const block = [
465
+ `Built, pending, possible — ${only ?? `every product${unregisteredShown ? `, and ${plural(unregisteredShown, 'item')} that name${unregisteredShown === 1 ? 's' : ''} none` : ''}`} · read ${readAt.slice(0, 19)}Z${since ? ` · since ${since}` : ''}`,
466
+ section(since ? `Built since ${since}` : 'Built', built, (row) => listLine(row.title, `${where_(row)} · ${row.verdict} ${day(row.delivered_at)} · ${prsOf(row)} · ${row.item}${row.mark_id ? ` · mark ${row.mark_id.slice(0, 8)}` : ''}`)),
467
+ section('Building', building, (row) => listLine(`${row.title} · ${row.stage || 'no stage'} · next: ${row.next_step || 'not set'}`, `${where_(row)} · ${row.item} · ${row.days_since_last_change === null ? 'no movement on record' : `${plural(row.days_since_last_change, 'day')} since ${row.last_change_source}`}`)),
468
+ section('Pending, parked until someone returns to it', pending, (row) => listLine(`${row.title} — ${row.reason}`, `${where_(row)} · ${row.item}`)),
469
+ section('Possible, proposed and waiting for a yes', possible, (row) => listLine(row.title, `${where_(row)} · ${row.age_days === null ? 'age unknown' : `${plural(row.age_days, 'day')} old`} · ${row.item}`)),
470
+ section('The decisions that set the order', order, (row) => listLine(`${row.kind}: ${row.text}`, `${row.product} · ${row.date} · ${row.decision}`)),
471
+ ...(unregisteredTotal ? [[
472
+ `Not in the registry (${unregisteredTotal}${only ? '' : `, ${unregisteredShown} of them in the rows above, named "unregistered"`})`,
473
+ ' The slice L5 sweep gives them a product.',
474
+ ...(only ? [' A read narrowed to one product cannot show them: an item with no product belongs to none.'] : []),
475
+ ].join('\n')] : []),
476
+ ].join('\n\n');
477
+ const rendered = scrubSecrets(block);
478
+ return scrubbed({
479
+ verb: 'built_pending_possible', read_at: readAt, product: only, since,
480
+ built, building, pending, possible, order, unregistered_total: unregisteredTotal, unregistered_in_rows: unregisteredShown,
481
+ rows: built.length + building.length + pending.length + possible.length,
482
+ redactions: redactions + rendered.redactions, text: rendered.text,
483
+ });
484
+ },
485
+ };
486
+ return verbs;
487
+ }
@@ -0,0 +1,11 @@
1
+ // The variables a program started by Atlas receives: a short list, never the
2
+ // Engram connection, never an ATLAS_WORK_* setting (F11 study, fix one).
3
+ const CHILD_VARIABLES = ['PATH', 'HOME', 'USER', 'LOGNAME', 'LANG', 'LC_ALL', 'TMPDIR', 'XDG_CONFIG_HOME', 'SSL_CERT_FILE', 'SSH_AUTH_SOCK'];
4
+
5
+ export function childEnvironment(env = process.env) {
6
+ const out = {};
7
+ for (const [name, value] of Object.entries(env)) {
8
+ if (CHILD_VARIABLES.includes(name) || name.startsWith('GH_') || name === 'GITHUB_TOKEN') out[name] = value;
9
+ }
10
+ return out;
11
+ }