@arjunkhera/atlas 0.3.30 → 0.3.31

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas",
3
- "version": "0.3.30",
3
+ "version": "0.3.31",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, for any repository.",
5
5
  "author": {
6
6
  "name": "Arjun Khera"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.3.30",
3
+ "version": "0.3.31",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, as a Claude Code plugin for any repository.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -175,6 +175,13 @@ next action. One short paragraph.
175
175
  `library` repo proves it by merged pull requests and a green gate. A
176
176
  `service` or `schema` repo also needs its live checks, because a schema
177
177
  migration reaches the running node after the merge.
178
+ A red check is a last resort, for an extraordinary case with no other
179
+ option (owner, 22 October 2026). Fix the test first. If the owner still
180
+ approved a merge with one known red test, record the words
181
+ with `decision_record` on the item. The words must name the pull request
182
+ and quote the exact test. One decision accepts one red check. Then pass
183
+ `accepted_red` to `item_done`; it needs the owner present. Any other red
184
+ check still refuses the mark.
178
185
  2. If anything was a struggle, encode the fix where it stops a repeat — a lint,
179
186
  then a tool, then a skill, then a doc — and say which.
180
187
  3. Rebuild the design's page and the hub, so both read "Shipped". The
@@ -24,6 +24,12 @@ export const FATAL_CONCLUSIONS = Object.freeze(['failure', 'timed_out', 'action_
24
24
  const PASSING_CONCLUSION = 'success';
25
25
 
26
26
  const MAX_PRS = 32;
27
+ const MAX_ACCEPTED_RED = 16;
28
+ // The decision kinds that can carry the owner's acceptance of a red check. A
29
+ // sweep or a priority order says nothing about one merge.
30
+ const ACCEPTING_KINDS = Object.freeze(['decision', 'answer']);
31
+ const OWNER_DECISION_VERBS = Object.freeze(['decision_record', 'question_answer']);
32
+ const MIN_TEST_NAME = 8;
27
33
  const MAX_REPOS = 16;
28
34
  const MAX_ITEMS = 500;
29
35
  const PRS_PER_REPO = 100;
@@ -193,7 +199,7 @@ export function correctionsOf(marks = []) {
193
199
  }
194
200
 
195
201
  export function createDeliveryVerbs(core) {
196
- const { client, label, now, typeNamed, registry, requireProduct, requireItem, ownerPresent, patch, create, edge, landed, alreadyPatched, readGitHub, readLive } = core;
202
+ const { client, label, now, typeNamed, kindOf, registry, requireProduct, requireItem, ownerPresent, patch, create, edge, landed, alreadyPatched, readGitHub, readLive } = core;
197
203
 
198
204
  const github = async (path) => {
199
205
  if (typeof readGitHub !== 'function') refuse('this verb needs a GitHub reader; the server supplies one');
@@ -304,11 +310,76 @@ export function createDeliveryVerbs(core) {
304
310
  };
305
311
  }
306
312
 
313
+ // Issue #158: the owner can accept one known red check on one merged pull
314
+ // request, for example a test that only a run after the merge can turn
315
+ // green. The gate never changes after the merge, so without this the item
316
+ // could never be marked. GitHub's gate names a check run, not a test, so the
317
+ // verb proves the check level, and the owner's own recorded words must name
318
+ // the pull request and the exact test.
319
+ //
320
+ // A decision record is an Engram entity, and any Engram writer can make one
321
+ // that looks right. So the acceptance is not taken from the record alone:
322
+ // passing accepted_red needs the owner-present flag, the same flag that
323
+ // decision_record needs. A forged record then gives a caller nothing it
324
+ // could not write itself (security review of this change, finding 1).
325
+ async function acceptedRed(value, pulls, item) {
326
+ if (value === undefined) return [];
327
+ if (!Array.isArray(value)) refuse('accepted_red must be an array of { pr, check, test, decision_id }');
328
+ if (value.length > MAX_ACCEPTED_RED) refuse(`accepted_red holds at most ${MAX_ACCEPTED_RED} entries`);
329
+ const decisionType = await typeNamed('decision');
330
+ let replacedIds = null;
331
+ const rows = [], seen = new Set();
332
+ for (const entry of value) {
333
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) refuse('each accepted_red entry must be an object { pr, check, test, decision_id }');
334
+ for (const key of Object.keys(entry)) if (!['pr', 'check', 'test', 'decision_id'].includes(key)) refuse(`accepted_red does not take ${key}`);
335
+ const parsed = parsePullUrl(entry.pr, 'accepted_red pr');
336
+ const pull = pulls.find((one) => one.url === parsed.url);
337
+ if (!pull) refuse(`accepted_red names ${parsed.url}, which is not one of the prs of this delivery`);
338
+ const check = text(entry.check, 'accepted_red check', { max: 200 }).trim();
339
+ const testName = text(entry.test, 'accepted_red test', { max: 500 }).trim();
340
+ if (!check) refuse('accepted_red check must not be empty');
341
+ if (testName.length < MIN_TEST_NAME) refuse(`accepted_red test must be the full test name, at least ${MIN_TEST_NAME} characters`);
342
+ if (seen.has(`${pull.url} check ${check}`)) refuse(`accepted_red names check ${check} on ${pull.url} twice`);
343
+ seen.add(`${pull.url} check ${check}`);
344
+ const decisionId = text(entry.decision_id, 'accepted_red decision_id', { max: 200 });
345
+ // One decision accepts one red check on one pull request. A second
346
+ // entry with the same decision would stretch the owner's words over a
347
+ // check the owner never named (finding 2).
348
+ const key = `${pull.url} ${decisionId}`;
349
+ if (seen.has(key)) refuse(`accepted_red uses decision ${decisionId} twice on ${pull.url}; one decision accepts one red check`);
350
+ seen.add(key);
351
+ let decision;
352
+ try { decision = await client.getEntity(decisionId); }
353
+ catch (error) { if (error?.status === 404) refuse(`unknown decision id: ${decisionId}`); throw error; }
354
+ if ((await kindOf(decision)) !== 'decision') refuse(`entity ${decisionId} is not a decision record`);
355
+ if (decision.state && decision.state !== 'active') refuse(`decision ${decisionId} is ${decision.state}, not active in the graph`);
356
+ const said = decision.fields ?? {};
357
+ const madeBy = Object.values(said.atlas_receipts ?? {}).map((receipt) => receipt?.verb);
358
+ if (!madeBy.some((verb) => OWNER_DECISION_VERBS.includes(verb))) refuse(`decision ${decisionId} was not written by decision_record or question_answer`);
359
+ if (!ACCEPTING_KINDS.includes(said.kind)) refuse(`decision ${decisionId} is a ${said.kind} record; the owner accepts a red check with a decision or an answer`);
360
+ if (!(Array.isArray(said.items) ? said.items : []).includes(item.id)) refuse(`decision ${decisionId} does not name item ${item.id}; the owner's acceptance is recorded on the item it delivers`);
361
+ // A decision the owner later replaced no longer stands (finding 4).
362
+ replacedIds ??= new Set((await client.listAllEntities({ type: decisionType.name, state: 'any' })).map((row) => row.fields?.replaces).filter(Boolean));
363
+ if (replacedIds.has(decision.id)) refuse(`decision ${decisionId} is replaced by a later decision; it no longer stands`);
364
+ const words = typeof said.text === 'string' ? said.text : '';
365
+ const source = typeof said.source === 'string' ? said.source : '';
366
+ // The owner's words name the pull request by its URL, by repo#N, or by
367
+ // #N; the source counts only when it holds the full URL (finding 5).
368
+ const namesPull = words.includes(pull.url) || source.includes(pull.url) || words.includes(`${pull.repo}#${pull.number}`)
369
+ || new RegExp(`(?:^|[^A-Za-z0-9_/.-])#${pull.number}(?![0-9])`).test(words);
370
+ if (!namesPull) refuse(`decision ${decisionId} does not name ${pull.url}; the owner's words must name the pull request (its URL or #${pull.number})`);
371
+ if (!words.includes(testName)) refuse(`decision ${decisionId} does not quote the test "${testName}"; the owner's own words must name the exact test`);
372
+ rows.push({ pr: pull.url, check, test: testName, decision_id: decision.id, decision_version: decision.version ?? null, decision });
373
+ }
374
+ return rows;
375
+ }
376
+
307
377
  const verbs = {
308
378
  // The delivering agent marks its own change done, after the verb runs the
309
379
  // three live checks itself. No owner flag: the agent that shipped it calls
310
380
  // this, and the lead catches up a delivery made outside a session (R2).
311
381
  async item_done(args, requestId, meta) {
382
+ if (args.accepted_red !== undefined && !ownerPresent) refuse('accepted_red needs the owner-present flag on the server host (ATLAS_WORK_OWNER_PRESENT=1): only the owner waives a red check');
312
383
  const item = await requireItem(args.item_id);
313
384
  const fields = item.fields ?? {};
314
385
  const replay = alreadyPatched(item, requestId, meta);
@@ -336,6 +407,7 @@ export function createDeliveryVerbs(core) {
336
407
  delivered_at: standing.fields.marked_at, prs: (standing.fields.prs ?? []).map((pull) => pull.url), deployed_commit: standing.fields.deployed?.commit ?? null,
337
408
  checks: (standing.fields.checks ?? []).map((check) => ({ name: check.name, result: check.result, detail: check.detail })),
338
409
  verified_by: (standing.fields.verified_by ?? []).map((row) => ({ pr: row.pr, passed: row.passed })), gate_warnings: standing.fields.gate_warnings ?? [],
410
+ accepted_red: (standing.fields.accepted_red ?? []).map((row) => ({ pr: row.pr, check: row.check, test: row.test, decision_id: row.decision_id, detail: row.detail })),
339
411
  replayed_from_graph: true };
340
412
  }
341
413
 
@@ -394,6 +466,7 @@ export function createDeliveryVerbs(core) {
394
466
  const mergeCommit = text(pr.merge_commit_sha, `merge_commit_sha of ${parsed.url}`, { max: 64 });
395
467
  pulls.push({ ...parsed, title: oneLine(pr.title ?? '', 200), merged_at: pr.merged_at ?? '', merge_commit_sha: mergeCommit, branch: pr.head?.ref ?? '', base: pr.base?.ref ?? '' });
396
468
  }
469
+ const accepted = await acceptedRed(args.accepted_red, pulls, item);
397
470
 
398
471
  // The three live checks, for a delivery that reaches a running service.
399
472
  let health = null, contains = null, functional = null;
@@ -410,13 +483,25 @@ export function createDeliveryVerbs(core) {
410
483
  }
411
484
 
412
485
  // The gate run, fetched from GitHub for each merge commit (R10).
413
- const verified = [], warnings = [];
486
+ const verified = [], warnings = [], acceptedRows = [];
414
487
  for (const pull of pulls) {
415
488
  const answer = await github(`repos/${pull.repo}/commits/${pull.merge_commit_sha}/check-runs?per_page=${CHECK_RUNS}`);
416
489
  const runs = latestCheckRuns(answer?.check_runs);
417
490
  const failed = runs.filter((run) => FATAL_CONCLUSIONS.includes(run.conclusion ?? ''));
418
491
  const passed = runs.filter((run) => run.conclusion === PASSING_CONCLUSION);
419
- if (failed.length) refuse(`the merge gate is red for ${pull.url}: ${failed.map((run) => `${run.name} ${run.conclusion} (${run.url})`).join(', ')}`);
492
+ const mine = accepted.filter((row) => row.pr === pull.url);
493
+ const unaccepted = failed.filter((run) => !mine.some((row) => row.check === run.name));
494
+ if (unaccepted.length) refuse(`the merge gate is red for ${pull.url}: ${unaccepted.map((run) => `${run.name} ${run.conclusion} (${run.url})`).join(', ')}${mine.length ? '; accepted_red does not cover it' : ''}`);
495
+ // An acceptance of a check that is not red is stale: the owner
496
+ // accepted a red that the gate does not show, so the verb refuses
497
+ // rather than write an acceptance nobody needed.
498
+ for (const row of mine) {
499
+ const run = failed.find((one) => one.name === row.check);
500
+ 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`);
501
+ const { decision: _record, ...kept } = row;
502
+ acceptedRows.push({ ...kept, conclusion: run.conclusion, run_url: run.url,
503
+ 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.` });
504
+ }
420
505
  if (!passed.length) refuse(`no check run succeeded for ${pull.url}; the merge gate is the independent record and it says nothing`);
421
506
  // A check name whose LATEST run succeeded is green. A name whose earlier
422
507
  // run was red and whose later run was skipped or neutral is not red and
@@ -431,7 +516,7 @@ export function createDeliveryVerbs(core) {
431
516
  warnings.push({ pr: pull.url, check: run.name, earlier: last.conclusion, earlier_url: last.url, latest: run.conclusion, latest_url: run.url,
432
517
  detail: `${run.name} concluded ${last.conclusion} for ${pull.url}, and its later run concluded ${run.conclusion}, not success. The verb cannot tell a fixed gate from a gate that was switched off.` });
433
518
  }
434
- verified.push({ pr: pull.url, commit: pull.merge_commit_sha, runs, passed: passed.map((run) => run.name) });
519
+ verified.push({ pr: pull.url, commit: pull.merge_commit_sha, runs, passed: passed.map((run) => run.name), ...(mine.length ? { accepted_red: mine.map((row) => row.check) } : {}) });
435
520
  }
436
521
 
437
522
  // One row per check that ran. For a repo with no deploy target the row
@@ -451,11 +536,12 @@ export function createDeliveryVerbs(core) {
451
536
  kind: 'delivery', verdict: 'delivered', item: item.id, product: product.fields.registry_id, marked_at: now(),
452
537
  prs: pulls.map((pull) => ({ url: pull.url, repo: pull.repo, number: pull.number, title: pull.title, branch: pull.branch, merged_at: pull.merged_at, merge_commit_sha: pull.merge_commit_sha })),
453
538
  ...(live ? { deployed: { health_url: healthUrl, commit: health.deployed_commit, reported: health.reported, status: health.status, checked_at: health.checked_at } } : {}),
454
- checks, verified_by: verified, ...(warnings.length ? { gate_warnings: warnings } : {}), ...(note ? { note } : {}),
539
+ checks, verified_by: verified, ...(warnings.length ? { gate_warnings: warnings } : {}), ...(acceptedRows.length ? { accepted_red: acceptedRows } : {}), ...(note ? { note } : {}),
455
540
  },
456
541
  tags: ['atlas-mark', `product:${product.fields.registry_id}`, ...pulls.map((pull) => `repo:${pull.repo}`)],
457
542
  });
458
543
  await edge(mark, item, 'references', requestId);
544
+ for (const row of accepted) await edge(mark, row.decision, 'references', requestId);
459
545
  const marks = Array.isArray(fields.delivery_marks) ? fields.delivery_marks : [];
460
546
  const patched = replay ? item : await patch(item, 'item_done', requestId, {
461
547
  lifecycle: 'done', delivery_state: 'delivered', delivered_at: mark.fields.marked_at,
@@ -467,6 +553,7 @@ export function createDeliveryVerbs(core) {
467
553
  proof: live ? 'live-checks' : 'merge-and-gate',
468
554
  checks: checks.map((check) => ({ name: check.name, result: check.result, detail: check.detail })),
469
555
  verified_by: verified.map((row) => ({ pr: row.pr, passed: row.passed })), gate_warnings: warnings,
556
+ accepted_red: acceptedRows.map((row) => ({ pr: row.pr, check: row.check, test: row.test, decision_id: row.decision_id, detail: row.detail })),
470
557
  ...(replay ? { replayed_from_graph: true } : {}),
471
558
  };
472
559
  },
package/work/mcp.mjs CHANGED
@@ -64,7 +64,7 @@ export const TOOLS = [
64
64
  { 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 } },
65
65
  { 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 } },
66
66
  { name: 'receipt_keep', description: `Keep the rendered answer the owner acted on and attach it to a decision. Refuses when there is no decision to attach to. Secret patterns are redacted before the write. Owner present only. ${RULES}`, inputSchema: { type: 'object', required: ['request_id', 'decision_id', 'rendered'], properties: { request_id: REQUEST_ID, decision_id: S('Entity id of the decision this receipt belongs to.'), rendered: S('The answer as the owner read it.'), view: S('Which view it came from, for example needs-me.') }, additionalProperties: false } },
67
- { 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. 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. 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.') }, additionalProperties: false } },
67
+ { 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. 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 } },
68
68
  { 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 } },
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 } },