@arjunkhera/atlas 0.4.30 → 0.4.32
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/door/lib/fails-before.mjs +14 -5
- package/door/lib/passes-after.mjs +2 -2
- package/package.json +1 -1
- package/skills/sdlc-task/SKILL.md +4 -3
- package/tests/junit.mjs +6 -3
- package/work/lib/delivery.mjs +55 -3
- package/work/mcp.mjs +5 -1
|
@@ -186,17 +186,26 @@ function caseTexts(xml) {
|
|
|
186
186
|
|
|
187
187
|
// The results of one report: Map of case name to pass, assertion, error or skipped.
|
|
188
188
|
// For pytest, a name with parameters ("test_x[1]") counts under its function name.
|
|
189
|
-
|
|
189
|
+
// `known` holds the case names that the case reader found in the file. node --test writes a test
|
|
190
|
+
// whose body holds subtests as a <testsuite> with the subtests as its cases, and no case of its own
|
|
191
|
+
// name. So a subtest counts under the outermost suite around it whose name is known (item
|
|
192
|
+
// 30badd46). A suite of describe() has no known name, so its cases count under their own names.
|
|
193
|
+
// Limit: node writes no failure for a parent test that fails outside its subtests, so that failure
|
|
194
|
+
// does not show here.
|
|
195
|
+
export function readReport(xml, known = []) {
|
|
190
196
|
const cases = readJunit(xml);
|
|
191
197
|
const texts = caseTexts(xml);
|
|
192
198
|
const rank = { pass: 0, skipped: 1, error: 2, assertion: 3 };
|
|
199
|
+
const named = new Set(known);
|
|
200
|
+
// The junit reporter of node writes a double quote as "&quot;", so one
|
|
201
|
+
// unescape leaves """ (f2d07e35). Undo the second escape here.
|
|
202
|
+
const unquote = (text) => text.replaceAll('"', '"');
|
|
193
203
|
const out = new Map();
|
|
194
204
|
cases.forEach((c, i) => {
|
|
195
205
|
let result = c.result;
|
|
196
206
|
if (result === 'fail') result = /AssertionError|ERR_ASSERTION/.test(`${c.message ?? ''}\n${texts[i] ?? ''}`) ? 'assertion' : 'error';
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const name = c.name.replaceAll('"', '"').replace(/\[.*\]$/, '');
|
|
207
|
+
const parent = (c.suites ?? []).map(unquote).find((s) => named.has(s));
|
|
208
|
+
const name = parent ?? unquote(c.name).replace(/\[.*\]$/, '');
|
|
200
209
|
// When one name holds several results, the worst one counts: a fail beats a pass.
|
|
201
210
|
if (!out.has(name) || rank[result] > rank[out.get(name)]) out.set(name, result);
|
|
202
211
|
});
|
|
@@ -336,7 +345,7 @@ export function runSide({ top, changed, plans, side, work = join(tmpdir(), 'atla
|
|
|
336
345
|
if (r.error) why = `the runner did not finish: ${r.error.message}`;
|
|
337
346
|
else if (!existsSync(report)) why = `the runner wrote no report (exit ${r.status})`;
|
|
338
347
|
else {
|
|
339
|
-
try { cases = Object.fromEntries(readReport(readFileSync(report, 'utf8'))); } catch (error) { why = `the report does not read: ${error.message}`; }
|
|
348
|
+
try { cases = Object.fromEntries(readReport(readFileSync(report, 'utf8'), p.known)); } catch (error) { why = `the report does not read: ${error.message}`; }
|
|
340
349
|
}
|
|
341
350
|
out.files[p.path] = { command, cases, why };
|
|
342
351
|
}
|
|
@@ -84,7 +84,7 @@ export function plan({ root, base, head = 'HEAD', list = null }) {
|
|
|
84
84
|
if (!picked.length && !all) continue;
|
|
85
85
|
if (!runner) { files.push({ path, runner: null, result: 'not run', why: 'no runner for this file yet' }); plans.push({ path, runner: null, picked, all }); continue; }
|
|
86
86
|
files.push({ path, runner, result: 'ran', why: null });
|
|
87
|
-
plans.push({ path, runner, picked, all, head: r.status === 'removed' ? null : r.path });
|
|
87
|
+
plans.push({ path, runner, picked, all, head: r.status === 'removed' ? null : r.path, known: before ? [...before.cases.keys()] : [] });
|
|
88
88
|
}
|
|
89
89
|
files.sort((x, y) => (x.path < y.path ? -1 : 1));
|
|
90
90
|
return {
|
|
@@ -131,7 +131,7 @@ export function runOld({ top, changed, plans, work = join(tmpdir(), 'atlas-after
|
|
|
131
131
|
if (r.error) why = `the runner did not finish: ${r.error.message}`;
|
|
132
132
|
else if (!existsSync(report)) why = `the runner wrote no report (exit ${r.status})`;
|
|
133
133
|
else {
|
|
134
|
-
try { cases = Object.fromEntries(readReport(readFileSync(report, 'utf8'))); } catch (error) { why = `the report does not read: ${error.message}`; }
|
|
134
|
+
try { cases = Object.fromEntries(readReport(readFileSync(report, 'utf8'), p.known)); } catch (error) { why = `the report does not read: ${error.message}`; }
|
|
135
135
|
}
|
|
136
136
|
out.files[p.path] = { command, cases, why };
|
|
137
137
|
}
|
package/package.json
CHANGED
|
@@ -202,9 +202,10 @@ next action. One short paragraph.
|
|
|
202
202
|
option (owner, 22 October 2026). Fix the test first. If the owner still
|
|
203
203
|
approved a merge with one known red test, record the words
|
|
204
204
|
with `decision_record` on the item. The words must name the pull request
|
|
205
|
-
and quote the exact test. One decision accepts one red check.
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
and quote the exact test. One decision accepts one red check. The owner
|
|
206
|
+
then signs a waiver act on his Mac with `atlas approve` and posts the
|
|
207
|
+
block on the pull request. Then pass `accepted_red` to `item_done`; it
|
|
208
|
+
needs the owner present. Any other red check still refuses the mark.
|
|
208
209
|
2. If anything was a struggle, run the `atlas:learnings` skill. It picks the
|
|
209
210
|
home and the form of the fix, and records the lesson.
|
|
210
211
|
3. Rebuild the design's page and the hub, so both read "Shipped". The
|
package/tests/junit.mjs
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// write this report: node --test (--test-reporter=junit), pytest
|
|
4
4
|
// (--junitxml), TestNG, JUnit, Vitest and Jest.
|
|
5
5
|
//
|
|
6
|
-
// readJunit(xml) -> [{ name, classname, file, suite, result, message }]
|
|
6
|
+
// readJunit(xml) -> [{ name, classname, file, suite, suites, result, message }]
|
|
7
7
|
// result: "pass", "fail" or "skipped". An <error> is a fail too.
|
|
8
|
+
// suites: the names of the <testsuite> elements around the case, outermost
|
|
9
|
+
// first. node --test writes a test with subtests as a <testsuite>.
|
|
8
10
|
//
|
|
9
11
|
// The reader has no dependency. It reads only the parts it needs: the
|
|
10
12
|
// <testsuite> nesting, each <testcase> and its <failure>, <error> and
|
|
@@ -44,9 +46,10 @@ export function readJunit(xml) {
|
|
|
44
46
|
if (tag === 'testcase') {
|
|
45
47
|
if (closing) { current = null; continue; }
|
|
46
48
|
const a = attributes(attrs);
|
|
47
|
-
const
|
|
49
|
+
const names = suites.map((s) => s.name).filter(Boolean);
|
|
50
|
+
const suite = names.join(' > ');
|
|
48
51
|
const file = a.file ?? suites.map((s) => s.file).filter(Boolean).at(-1) ?? null;
|
|
49
|
-
const one = { name: a.name ?? '', classname: a.classname ?? null, file, suite: suite || null, result: 'pass', message: null };
|
|
52
|
+
const one = { name: a.name ?? '', classname: a.classname ?? null, file, suite: suite || null, suites: names, result: 'pass', message: null };
|
|
50
53
|
cases.push(one);
|
|
51
54
|
current = selfClosing ? null : one;
|
|
52
55
|
continue;
|
package/work/lib/delivery.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { refuse, text, isoDate, url, stringList, sha256, repoKinds, LIVE_KINDS }
|
|
|
16
16
|
import { oneLine, ROW_LINE_MAX, priorityOrderOf, byPriority, orderNote, waitsOnLine, blockersOf, blocksOf, blockedPhrase, waitsShown } from './questions.mjs';
|
|
17
17
|
import { parseYaml } from '../../shape/check.mjs';
|
|
18
18
|
import { ACTIONS_APP_ID } from '../../door/lib/gate.mjs';
|
|
19
|
+
import { MARKER as SIGNED_MARKER, SIGNERS_FILE, parseSigners, verifyTexts, findAct } from '../../door/lib/signed-act.mjs';
|
|
19
20
|
|
|
20
21
|
export const DELIVERY_WRITE_VERBS = Object.freeze(['item_done', 'item_reopen']);
|
|
21
22
|
export const DELIVERY_READ_VERBS = Object.freeze(['in_flight']);
|
|
@@ -147,10 +148,20 @@ export function requiredStates(names, headRuns, mergeRuns) {
|
|
|
147
148
|
});
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
// R17 line 2: a skipped check names what each commit saw. A check can be
|
|
152
|
+
// skipped on the head and never run on the merge commit.
|
|
153
|
+
const SIDE = { head: 'the head', merge: 'the merge commit' };
|
|
154
|
+
function skippedSides(row) {
|
|
155
|
+
const seen = Array.isArray(row.seen) ? row.seen : [];
|
|
156
|
+
if (seen.every((one) => one.state === 'skipped')) return 'was skipped on the head and on the merge commit';
|
|
157
|
+
return seen.map((one) => (one.state === 'skipped' ? `was skipped on ${SIDE[one.on] ?? one.on}`
|
|
158
|
+
: one.state === 'missing' ? `never ran on ${SIDE[one.on] ?? one.on}` : `ended ${one.conclusion ?? one.state} on ${SIDE[one.on] ?? one.on}`)).join(' and ');
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
export const requiredWhy = (row) => ({
|
|
151
162
|
pending: `${row.name} is still running`,
|
|
152
163
|
red: `${row.name} is red (${row.seen.map((one) => `${one.on} ${one.conclusion ?? one.state}`).join(', ')})`,
|
|
153
|
-
skipped: `${row.name}
|
|
164
|
+
skipped: `${row.name} ${skippedSides(row)}: a path filter or a condition skipped it, so it proved nothing`,
|
|
154
165
|
missing: `${row.name} never ran on the head or on the merge commit`,
|
|
155
166
|
}[row.state] ?? `${row.name} ended ${row.state}`);
|
|
156
167
|
|
|
@@ -424,6 +435,46 @@ export function createDeliveryVerbs(core) {
|
|
|
424
435
|
// passing accepted_red needs the owner-present flag, the same flag that
|
|
425
436
|
// decision_record needs. A forged record then gives a caller nothing it
|
|
426
437
|
// could not write itself (security review of this change, finding 1).
|
|
438
|
+
// R17 line 6: a decision record alone does not waive a red check. The owner
|
|
439
|
+
// signs a waiver act with his security key (R72, design
|
|
440
|
+
// docs/design-docs/signed-act/) and posts the block on the pull request.
|
|
441
|
+
// The act names the repo, the pull request, its head, the decision and the
|
|
442
|
+
// check. The key must be in atlas/owner-signers on the base of the merge,
|
|
443
|
+
// its first parent, so a key that the pull request added does not count.
|
|
444
|
+
async function signedWaiver(pull, decisionId, check) {
|
|
445
|
+
const head = String(pull.head_sha ?? '').toLowerCase();
|
|
446
|
+
if (!/^[0-9a-f]{40}$/.test(head)) refuse(`GitHub names no head commit of ${pull.url}, so no signed waiver can match it`);
|
|
447
|
+
const commit = await github(`repos/${pull.repo}/commits/${pull.merge_commit_sha}`);
|
|
448
|
+
const parent = commit?.parents?.[0]?.sha;
|
|
449
|
+
if (typeof parent !== 'string' || !/^[0-9a-f]{40}$/i.test(parent)) refuse(`GitHub names no parent of the merge commit ${pull.merge_commit_sha} of ${pull.url}`);
|
|
450
|
+
// A rebase merge makes the first parent a commit of the pull request, so
|
|
451
|
+
// the key must also be on the merge base of that parent and the head: a
|
|
452
|
+
// commit the pull request did not write (verifier of atlas#240).
|
|
453
|
+
const compared = await github(`repos/${pull.repo}/compare/${parent}...${head}`);
|
|
454
|
+
const forkPoint = compared?.merge_base_commit?.sha;
|
|
455
|
+
if (typeof forkPoint !== 'string' || !/^[0-9a-f]{40}$/i.test(forkPoint)) refuse(`GitHub names no merge base of ${pull.url}, so no signed waiver can be checked`);
|
|
456
|
+
const keysAt = async (ref) => {
|
|
457
|
+
const file = await githubOrNull(`repos/${pull.repo}/contents/${SIGNERS_FILE}?ref=${ref}`);
|
|
458
|
+
return parseSigners(file && typeof file.content === 'string' ? Buffer.from(file.content, 'base64').toString('utf8') : '').keys;
|
|
459
|
+
};
|
|
460
|
+
const atFork = forkPoint.toLowerCase() === parent.toLowerCase() ? null : new Set((await keysAt(forkPoint)).map((key) => key.fingerprint));
|
|
461
|
+
const keys = (await keysAt(parent)).filter((key) => !atFork || atFork.has(key.fingerprint));
|
|
462
|
+
if (!keys.length) refuse(`accepted_red needs the owner's signed waiver, and ${SIGNERS_FILE} on the base of ${pull.url} holds no owner key`);
|
|
463
|
+
const texts = [];
|
|
464
|
+
for (let page = 1; page <= 10; page += 1) {
|
|
465
|
+
const rows = await github(`repos/${pull.repo}/issues/${pull.number}/comments?per_page=100&page=${page}`);
|
|
466
|
+
for (const row of Array.isArray(rows) ? rows : []) if (typeof row?.body === 'string' && row.body.includes(SIGNED_MARKER)) texts.push(row.body);
|
|
467
|
+
if (!Array.isArray(rows) || rows.length < 100) break;
|
|
468
|
+
}
|
|
469
|
+
const verified = verifyTexts(texts, keys);
|
|
470
|
+
const found = findAct(verified, { kind: 'waiver', repo: pull.repo, pr: pull.number, head, decision: decisionId, check });
|
|
471
|
+
if (!found) {
|
|
472
|
+
const why = verified.find((row) => row.error)?.error;
|
|
473
|
+
refuse(`accepted_red needs the owner's signed waiver on ${pull.url} for check ${check}, head ${head.slice(0, 12)} and decision ${decisionId}; none counts${why ? ` (a block was refused: ${why})` : ''}. The owner runs: atlas approve act waiver, and posts the block on the pull request`);
|
|
474
|
+
}
|
|
475
|
+
return { key: found.key, signed_at: found.signed_at };
|
|
476
|
+
}
|
|
477
|
+
|
|
427
478
|
async function acceptedRed(value, pulls, item) {
|
|
428
479
|
if (value === undefined) return [];
|
|
429
480
|
if (!Array.isArray(value)) refuse('accepted_red must be an array of { pr, check, test, decision_id }');
|
|
@@ -471,7 +522,8 @@ export function createDeliveryVerbs(core) {
|
|
|
471
522
|
|| new RegExp(`(?:^|[^A-Za-z0-9_/.-])#${pull.number}(?![0-9])`).test(words);
|
|
472
523
|
if (!namesPull) refuse(`decision ${decisionId} does not name ${pull.url}; the owner's words must name the pull request (its URL or #${pull.number})`);
|
|
473
524
|
if (!words.includes(testName)) refuse(`decision ${decisionId} does not quote the test "${testName}"; the owner's own words must name the exact test`);
|
|
474
|
-
|
|
525
|
+
const waiver = await signedWaiver(pull, decision.id, check);
|
|
526
|
+
rows.push({ pr: pull.url, check, test: testName, decision_id: decision.id, decision_version: decision.version ?? null, waiver, decision });
|
|
475
527
|
}
|
|
476
528
|
return rows;
|
|
477
529
|
}
|
|
@@ -639,7 +691,7 @@ export function createDeliveryVerbs(core) {
|
|
|
639
691
|
if (!run) refuse(`accepted_red names check ${row.check} on ${pull.url}, but that check is not red in the merge gate; drop the entry`);
|
|
640
692
|
const { decision: _record, ...kept } = row;
|
|
641
693
|
acceptedRows.push({ ...kept, conclusion: run.conclusion, run_url: run.url,
|
|
642
|
-
detail: `${row.check} concluded ${run.conclusion} for ${pull.url}. The owner accepted the red test "${row.test}" in decision ${row.decision_id}. The gate names the check, not the test, so the verb proved the check level only.` });
|
|
694
|
+
detail: `${row.check} concluded ${run.conclusion} for ${pull.url}. The owner accepted the red test "${row.test}" in decision ${row.decision_id}, and signed the waiver with key ${row.waiver.key}. The gate names the check, not the test, so the verb proved the check level only.` });
|
|
643
695
|
}
|
|
644
696
|
if (!required.length && !passed.length) refuse(`no check run succeeded for ${pull.url}; the merge gate is the independent record and it says nothing`);
|
|
645
697
|
// A check name whose LATEST run succeeded is green. A name whose earlier
|
package/work/mcp.mjs
CHANGED
|
@@ -68,7 +68,7 @@ export const TOOLS = [
|
|
|
68
68
|
{ name: 'question_ask', description: `Ask the owner one question that blocks work on an item, and say what waiting costs. When: an agent is blocked and cannot decide by itself. Any in-session agent may call it; no owner flag. The question shows in the "Needs me" view until it is answered. ${RULES}`, inputSchema: { type: 'object', required: ['request_id', 'text', 'item_id', 'product', 'cost'], properties: { request_id: REQUEST_ID, text: S('The question, in plain words, answerable without opening a source.'), item_id: S('Entity id of the work item the question blocks.'), product: S('Product id of that item.'), cost: S(`What waiting costs. One of: ${COSTS.map((cost) => `${cost} (${COST_MEANING[cost]})`).join('; ')}.`), context: { type: 'object', description: 'What the owner needs to answer without opening a source.', properties: { current_behavior: S('What happens today.'), change: S('What would change.'), affected: { type: 'array', items: { type: 'string' }, description: 'Parts that change.' }, options: { type: 'array', items: { type: 'string' }, description: 'The options, one per entry.' }, consequences: S('What follows from each option.'), sources: { type: 'array', items: { type: 'string' }, description: 'Absolute http or https URLs that back the context.' } } } }, additionalProperties: false } },
|
|
69
69
|
{ name: 'question_answer', description: `Record the owner's answer to one open question. One call writes the decision in the owner's words, keeps the rendered answer as a receipt, and closes the question, so the "Needs me" view loses the row. Refuses an unknown question and one that is already answered. Owner present only. ${RULES}`, inputSchema: { type: 'object', required: ['request_id', 'question_id', 'text', 'date'], properties: { request_id: REQUEST_ID, question_id: S('Entity id of the open question.'), text: S("The owner's answer, verbatim."), date: S('ISO date the owner said it, in the form YYYY-MM-DD.'), source: S('Where the words are on record: a chat turn, a doc URL, an issue.'), kind: S(`One of ${DECISION_KINDS.join(', ')}. Default answer.`), items: { type: 'array', items: { type: 'string' }, description: 'Item ids the decision touches. Default: the question\'s item.' }, priority_order: { type: 'array', items: { type: 'string' }, description: 'Item ids in the order the owner set.' }, replaces: S('Entity id of the decision this one replaces.'), rendered: S('The view text the owner acted on. Default: the question and the answer.'), view: S('Which view the owner read, for example needs-me.') }, additionalProperties: false } },
|
|
70
70
|
{ name: 'decision_record', description: `Record what the owner decided outside a question, in their own words. Refuses missing text and a missing date. Pass rendered to keep the view the owner acted on as a receipt in the same call. Owner present only. ${RULES}`, inputSchema: { type: 'object', required: ['request_id', 'text', 'date'], properties: { request_id: REQUEST_ID, text: S("The owner's words, verbatim."), date: S('ISO date the owner said it.'), source: S('Where the words are on record.'), kind: S(`One of ${DECISION_KINDS.join(', ')}. Default decision.`), product: S('Product id the decision is about.'), items: { type: 'array', items: { type: 'string' }, description: 'Item ids the decision touches.' }, priority_order: { type: 'array', items: { type: 'string' }, description: 'Item ids in the order the owner set.' }, replaces: S('Entity id of the decision this one replaces.'), rendered: S('The view text the owner acted on.'), view: S('Which view the owner read.') }, additionalProperties: false } },
|
|
71
|
-
{ name: 'item_done', description: `Mark an item delivered. What proves it follows the repo kind (amendment A1.7). A service or schema repo gets the three read-only live checks: the deployed commit carries the pull requests, the health endpoint answers, and a functional check scoped to the change passes. Every check is a GET. The merge state of each pull request and the gate runs come from GitHub; the caller supplies none of them, and a red gate (${FATAL_CONCLUSIONS.join(', ')}) refuses the mark. One exception, a last resort for an extraordinary case with no other option: the owner can accept a known red test on one merged pull request. Name it in accepted_red with the decision that records the owner's words; those words must name the pull request and quote the exact test, and one decision accepts one red check. accepted_red needs the owner present. The gate names a check, not a test, so the verb proves the check level, and every other red check still refuses the mark. A library repo has no deploy target, so its delivery is proven by the merge and the gate alone: pass no functional, and the product needs no health_url. A schema repo keeps the three checks, because its migration runs after the merge. When atlas.yaml on the base of a merge lists required_checks, each one needs a successful newest GitHub Actions run on the pull request head or on the merge commit; a skipped check and a check that never ran are named apart. When atlas.yaml says live: release, a published release must hold each merge commit, and the mark stores that version; a red run on the merge commit is then a warning on the mark, because the release was built from a later green main. An item with no registered repo, or a repo whose kind was never set, keeps the three checks. The delivering agent calls this; the lead calls it to catch up a delivery made outside a session. No owner flag. ${RULES}`, inputSchema: { type: 'object', required: ['request_id', 'item_id', 'prs'], properties: { request_id: REQUEST_ID, item_id: S('Item id. The item must be active. Unless every repo it names is a library, its product must carry a health_url.'), prs: { type: 'array', items: { type: 'string' }, description: 'The merged GitHub pull request URLs this delivery shipped. Required: the item\'s own pr links are not read, because a link can be a design or a lock, not a delivery.' }, functional: { type: 'object', required: ['url', 'contains'], description: 'The read-only functional check. Required for a service or schema repo, and refused for a library repo. The verb GETs this URL once. No method, body or headers are accepted.', properties: { url: S('Absolute URL on the product health endpoint\'s origin, under one of the product\'s check_paths, and not the health endpoint itself.'), contains: { type: 'array', items: { type: 'string' }, description: 'Required. Strings the answer must carry, chosen so they exist only because of this change. Only whether each was found is recorded; the answer itself never enters the graph.' }, status: { type: 'number', description: 'Expected HTTP status, 2xx only. Default 200.' } } }, note: S('One line on what was delivered.'), accepted_red: { type: 'array', description: 'Red checks the owner accepted, one entry for each pull request and check. Owner present only. The decision must be a decision or an answer on this item, written by decision_record or question_answer and not replaced. Its words must name the pull request and quote the test. One decision accepts one check on one pull request. A listed check that is not red is refused.', items: { type: 'object', required: ['pr', 'check', 'test', 'decision_id'], additionalProperties: false, properties: { pr: S('One of the prs of this delivery.'), check: S('The name of the red check run in the merge gate, for example "test".'), test: S('The exact name of the red test, as the owner\'s words quote it.'), decision_id: S('The decision that records the owner\'s acceptance.') } } } }, additionalProperties: false } },
|
|
71
|
+
{ name: 'item_done', description: `Mark an item delivered. What proves it follows the repo kind (amendment A1.7). A service or schema repo gets the three read-only live checks: the deployed commit carries the pull requests, the health endpoint answers, and a functional check scoped to the change passes. Every check is a GET. The merge state of each pull request and the gate runs come from GitHub; the caller supplies none of them, and a red gate (${FATAL_CONCLUSIONS.join(', ')}) refuses the mark. One exception, a last resort for an extraordinary case with no other option: the owner can accept a known red test on one merged pull request. Name it in accepted_red with the decision that records the owner's words; those words must name the pull request and quote the exact test, and one decision accepts one red check. The owner must also sign a waiver act for the repo, the pull request, its head, the decision and the check (atlas approve act waiver) and post the block on the pull request; the key must be in atlas/owner-signers on the base of the merge. accepted_red needs the owner present. The gate names a check, not a test, so the verb proves the check level, and every other red check still refuses the mark. A library repo has no deploy target, so its delivery is proven by the merge and the gate alone: pass no functional, and the product needs no health_url. A schema repo keeps the three checks, because its migration runs after the merge. When atlas.yaml on the base of a merge lists required_checks, each one needs a successful newest GitHub Actions run on the pull request head or on the merge commit; a skipped check and a check that never ran are named apart. When atlas.yaml says live: release, a published release must hold each merge commit, and the mark stores that version; a red run on the merge commit is then a warning on the mark, because the release was built from a later green main. An item with no registered repo, or a repo whose kind was never set, keeps the three checks. The delivering agent calls this; the lead calls it to catch up a delivery made outside a session. No owner flag. ${RULES}`, inputSchema: { type: 'object', required: ['request_id', 'item_id', 'prs'], properties: { request_id: REQUEST_ID, item_id: S('Item id. The item must be active. Unless every repo it names is a library, its product must carry a health_url.'), prs: { type: 'array', items: { type: 'string' }, description: 'The merged GitHub pull request URLs this delivery shipped. Required: the item\'s own pr links are not read, because a link can be a design or a lock, not a delivery.' }, functional: { type: 'object', required: ['url', 'contains'], description: 'The read-only functional check. Required for a service or schema repo, and refused for a library repo. The verb GETs this URL once. No method, body or headers are accepted.', properties: { url: S('Absolute URL on the product health endpoint\'s origin, under one of the product\'s check_paths, and not the health endpoint itself.'), contains: { type: 'array', items: { type: 'string' }, description: 'Required. Strings the answer must carry, chosen so they exist only because of this change. Only whether each was found is recorded; the answer itself never enters the graph.' }, status: { type: 'number', description: 'Expected HTTP status, 2xx only. Default 200.' } } }, note: S('One line on what was delivered.'), accepted_red: { type: 'array', description: 'Red checks the owner accepted, one entry for each pull request and check. Owner present only. The decision must be a decision or an answer on this item, written by decision_record or question_answer and not replaced. Its words must name the pull request and quote the test. One decision accepts one check on one pull request. A listed check that is not red is refused.', items: { type: 'object', required: ['pr', 'check', 'test', 'decision_id'], additionalProperties: false, properties: { pr: S('One of the prs of this delivery.'), check: S('The name of the red check run in the merge gate, for example "test".'), test: S('The exact name of the red test, as the owner\'s words quote it.'), decision_id: S('The decision that records the owner\'s acceptance.') } } } }, additionalProperties: false } },
|
|
72
72
|
{ name: 'item_reopen', description: `Reopen a delivered item: the lifecycle goes back to active and a reopen mark is appended, citing the failed check by URL. The delivery mark is never erased; the reopen points at it with the corrects relation. The owner, or the lead in this session, calls it. ${RULES}`, inputSchema: { type: 'object', required: ['request_id', 'item_id', 'reason', 'failed_check'], properties: { request_id: REQUEST_ID, item_id: S('Item id carrying at least one delivery mark.'), reason: S('Why it is reopened, in plain words.'), failed_check: S('Absolute URL of the check that failed: a CI run, a monitor, an issue.'), corrects: S('Mark id of the delivery this reopen corrects. Default: the item\'s latest delivery mark.') }, additionalProperties: false } },
|
|
73
73
|
{ 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 } },
|
|
74
74
|
{ 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 } },
|
|
@@ -213,6 +213,10 @@ export const GITHUB_READS = Object.freeze([
|
|
|
213
213
|
new RegExp(`^repos/${REPO}/contents/atlas\\.yaml\\?ref=(?:${SHA}|${BRANCH})$`),
|
|
214
214
|
new RegExp(`^repos/${REPO}/releases\\?per_page=\\d{1,3}$`),
|
|
215
215
|
new RegExp(`^repos/${REPO}/compare/${SHA}\\.\\.\\.${TAG}$`),
|
|
216
|
+
// R17 line 6: the signed waiver on the comments of a pull request, and the
|
|
217
|
+
// owner's key file on the base of its merge.
|
|
218
|
+
new RegExp(`^repos/${REPO}/issues/\\d{1,9}/comments\\?per_page=100&page=\\d{1,2}$`),
|
|
219
|
+
new RegExp(`^repos/${REPO}/contents/atlas/owner-signers\\?ref=[0-9a-fA-F]{40}$`),
|
|
216
220
|
// Roadmap R16: the numbers page past 100 pull requests, and read the
|
|
217
221
|
// files of each merge to find a changed test.
|
|
218
222
|
new RegExp(`^repos/${REPO}/pulls\\?state=all&sort=updated&direction=desc&per_page=\\d{1,3}&page=\\d{1,3}$`),
|