@arjunkhera/atlas 0.4.52 → 0.4.53

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.4.52",
3
+ "version": "0.4.53",
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/door/cli.mjs CHANGED
@@ -155,6 +155,8 @@ const HELP = `atlas — the door into a repo's Atlas files
155
155
  --json print the whole answer as JSON
156
156
  atlas runner claim --repo <owner/name> --issue <n> --holder <run id> claim one atlas-job issue; the oldest live claim holds it
157
157
  atlas runner line --from <file> print the last line of a run, from a JSON file of ids and counts
158
+ --notice print the comment that hands the run facts to the notice workflow
159
+ atlas runner notice --repo <owner/name> --issue <n> --comment <id> in the notice workflow only: post the run line of one run comment as the bot
158
160
  Until the A2 exit test, the runner works on the scratch repo only
159
161
 
160
162
  atlas upkeep check --root <repo> the upkeep check: the docs index, dead doc links, the code each doc names, stale pages, and in a pull request its tracker item, its changed designs and STE on its changed lines
@@ -236,7 +238,7 @@ export const FLAGS = Object.freeze({
236
238
  view: { product: 'value', name: 'value', out: 'value' },
237
239
  tests: { root: 'optional', halves: 'value', evidence: 'value', 'guards-from': 'value', tests: 'value', 'dry-run': 'switch', run: 'value', own: 'value', base: 'value', head: 'value', list: 'value', side: 'value', old: 'value', new: 'value', covers: 'switch', findings: 'value', repo: 'value', max: 'value', bot: 'value', since: 'value', out: 'value', text: 'switch', json: 'switch', scenario: 'value', language: 'value' },
238
240
  jobs: { root: 'optional', repo: 'value', branch: 'value', out: 'value', findings: 'value', max: 'value', 'dry-run': 'switch' },
239
- runner: { repo: 'value', issue: 'value', holder: 'value', from: 'value', json: 'switch' },
241
+ runner: { repo: 'value', issue: 'value', holder: 'value', from: 'value', comment: 'value', notice: 'switch', json: 'switch' },
240
242
  upkeep: { root: 'optional', event: 'value', json: 'switch', last: 'value', base: 'value' },
241
243
  gate: { repo: 'value', root: 'optional', 'dry-run': 'switch', off: 'switch', on: 'switch', pr: 'value', since: 'value', commit: 'value', from: 'value', base: 'value', 'base-ref': 'value', head: 'value', body: 'value', json: 'switch', summary: 'value', output: 'value', days: 'value', branch: 'value', errors: 'switch', forced: 'switch', findings: 'value' },
242
244
  approve: { repo: 'value', root: 'optional', key: 'value', provider: 'value', yes: 'switch', out: 'value', file: 'value', signers: 'value', commit: 'value', item: 'value', card: 'value', words: 'value', pr: 'value', head: 'value', decision: 'value', check: 'value' },
@@ -497,7 +499,7 @@ function jobsVerb(chosen) {
497
499
  }
498
500
 
499
501
  function runnerVerb(chosen) {
500
- return runnerCommand({ what: chosen._[1], repo: chosen.repo, issue: chosen.issue, holder: chosen.holder, from: chosen.from, json: Boolean(chosen.json), args: [...chosen._, ...Object.values(chosen).filter((v) => typeof v === 'string')] }, line);
502
+ return runnerCommand({ what: chosen._[1], repo: chosen.repo, issue: chosen.issue, holder: chosen.holder, from: chosen.from, comment: chosen.comment, notice: Boolean(chosen.notice), json: Boolean(chosen.json), args: [...chosen._, ...Object.values(chosen).filter((v) => typeof v === 'string')] }, line);
501
503
  }
502
504
 
503
505
  function upkeepVerb(chosen) {
@@ -3,7 +3,8 @@
3
3
  //
4
4
  // atlas runner start --repo <owner/name> [--json]
5
5
  // atlas runner claim --repo <owner/name> --issue <n> --holder <run id>
6
- // atlas runner line --from <file>
6
+ // atlas runner line --from <file> [--notice]
7
+ // atlas runner notice --repo <owner/name> --issue <n> --comment <id>
7
8
  //
8
9
  // The runner is one scheduled Claude session for each repo. It follows the
9
10
  // atlas:runner skill, and that skill calls these commands. The commands read
@@ -15,6 +16,12 @@
15
16
  // claim posts one claim on an issue, then reads the claims back. The
16
17
  // oldest live claim holds the issue, so two runs build it once.
17
18
  // line prints the last line of a run, built by code from ids and counts.
19
+ // With --notice it prints the comment that hands the run facts to
20
+ // the notice workflow.
21
+ // notice runs in the notice workflow, as github-actions[bot]. It reads one
22
+ // run comment of the owner's login, builds the run line again from
23
+ // its facts, and posts the line, so GitHub tells the owner's phone
24
+ // (R25 line 10). It never copies the comment's text.
18
25
  //
19
26
  // Rules that hold everywhere here:
20
27
  // 1. Until the owner records the A2 exit test as passed, the runner works on
@@ -28,7 +35,7 @@
28
35
  // 4. No call here merges, pushes or changes a branch.
29
36
  import { readFileSync } from 'node:fs';
30
37
  import { jobMarker, typedFindings, JOBS_LABEL, atlasJobs } from './jobs.mjs';
31
- import { repoOk, TOKEN_SHAPE, DEFAULT_BOT } from './issues.mjs';
38
+ import { repoOk, TOKEN_SHAPE, DEFAULT_BOT, api, apiBase } from './issues.mjs';
32
39
  import { ghApi } from './gate.mjs';
33
40
  import { PACKAGE_NAME } from './gate-audit.mjs';
34
41
 
@@ -253,14 +260,56 @@ export function runLine({ repo, at, you = [], landed = [], opened = [], open_job
253
260
  return parts.join(' · ');
254
261
  }
255
262
 
263
+ // ------------------------------------------------------------ the notice
264
+
265
+ // R25 line 10. The runner session acts as the owner's login, and GitHub does
266
+ // not tell a person about his own comment. So the session posts its run facts
267
+ // in one comment on the notice issue, and the notice workflow posts the run
268
+ // line as the bot. The workflow trusts no text: it reads the facts as JSON,
269
+ // and runLine refuses any field that is not an id, a count or a known form.
270
+ export const RUN_MARKER = '<!-- atlas-runner:run -->';
271
+ export const NOTICE_LABEL = 'atlas-notice';
272
+ export const noticeBody = (facts) => [RUN_MARKER, '```json', JSON.stringify(facts), '```'].join('\n');
273
+
274
+ export function factsOf(body) {
275
+ const text = String(body ?? '');
276
+ if (!text.startsWith(`${RUN_MARKER}\n`)) return null;
277
+ const blocks = [...text.matchAll(/```json\n([\s\S]*?)\n```/g)];
278
+ if (blocks.length !== 1) return null;
279
+ try { return JSON.parse(blocks[0][1]); } catch { return null; }
280
+ }
281
+
282
+ export async function runnerNotice({ repo, issue, comment, env = process.env, call = null, allowed = RUNNER_REPOS }) {
283
+ if (!repoAllowed(repo, allowed)) throw usage(`${repo} is not a runner repo`);
284
+ const gh = call ?? ((method, path, body) => api(apiBase(env), env.GITHUB_TOKEN, method, path, body));
285
+ if (!call && !env.GITHUB_TOKEN) throw usage('the notice needs GITHUB_TOKEN, the token of the workflow run');
286
+ const got = await gh('GET', `/repos/${repo}/issues/comments/${comment}`);
287
+ if (!got.ok) return { posted: false, why: `GitHub answered ${got.status} for comment ${comment}` };
288
+ const note = got.data;
289
+ if (!String(note?.issue_url ?? '').endsWith(`/repos/${repo}/issues/${issue}`)) return { posted: false, why: `comment ${comment} is not on issue #${issue}` };
290
+ if (note?.author_association !== 'OWNER') return { posted: false, why: `comment ${comment} is not by the owner's login, so no runner wrote it` };
291
+ const thread = await gh('GET', `/repos/${repo}/issues/${issue}`);
292
+ const labels = (thread.data?.labels ?? []).map((one) => (typeof one === 'string' ? one : one?.name));
293
+ if (!thread.ok || !labels.includes(NOTICE_LABEL)) return { posted: false, why: `issue #${issue} is not the notice issue (label ${NOTICE_LABEL})` };
294
+ const facts = factsOf(note.body);
295
+ if (!facts) return { posted: false, why: `comment ${comment} holds no run facts` };
296
+ if (String(facts.repo ?? '').toLowerCase() !== repo.toLowerCase()) return { posted: false, why: `the run facts name ${facts.repo}, not ${repo}` };
297
+ const runLineText = runLine(facts);
298
+ const posted = await gh('POST', `/repos/${repo}/issues/${issue}/comments`, { body: runLineText });
299
+ if (!posted.ok) return { posted: false, why: `GitHub answered ${posted.status} to the notice` };
300
+ return { posted: true, line: runLineText };
301
+ }
302
+
256
303
  // ------------------------------------------------------------ the command
257
304
 
258
- export async function runnerCommand({ what, repo, issue, holder, from, json = false, args = [] }, line, env = process.env) {
305
+ export async function runnerCommand({ what, repo, issue, holder, from, comment, notice = false, json = false, args = [] }, line, env = process.env) {
259
306
  try {
260
307
  if ([...args, repo, holder, from].some((a) => typeof a === 'string' && TOKEN_SHAPE.test(a))) throw usage('a token must never be an argument');
261
308
  if (what === 'line') {
262
309
  if (!from) throw usage('give the run as --from <file>, a JSON object of ids and counts');
263
- line(runLine(JSON.parse(readFileSync(from, 'utf8'))));
310
+ const facts = JSON.parse(readFileSync(from, 'utf8'));
311
+ const text = runLine(facts);
312
+ line(notice ? noticeBody(facts) : text);
264
313
  return 0;
265
314
  }
266
315
  if (!repoOk(repo)) throw usage('give the repo as --repo <owner/name>');
@@ -278,7 +327,14 @@ export async function runnerCommand({ what, repo, issue, holder, from, json = fa
278
327
  line(result.held ? `HELD: issue #${issue} is this run's` : `TAKEN: issue #${issue} is held by ${result.holder}; leave it`);
279
328
  return result.held ? 0 : 1;
280
329
  }
281
- throw usage('use atlas runner start, atlas runner claim or atlas runner line');
330
+ if (what === 'notice') {
331
+ if (!/^[1-9][0-9]{0,8}$/.test(String(issue ?? ''))) throw usage('give the notice issue as --issue <number>');
332
+ if (!/^[1-9][0-9]{0,11}$/.test(String(comment ?? ''))) throw usage('give the run comment as --comment <id>');
333
+ const result = await runnerNotice({ repo, issue: Number(issue), comment: Number(comment), env });
334
+ line(result.posted ? `POSTED: ${result.line}` : `NOT POSTED: ${result.why}`);
335
+ return result.posted ? 0 : 1;
336
+ }
337
+ throw usage('use atlas runner start, claim, line or notice');
282
338
  } catch (error) {
283
339
  line(`atlas runner: ${error.message}`);
284
340
  return error.usage ? 2 : 1;
@@ -63,6 +63,11 @@ function packagedPinned(packageRoot, source) {
63
63
  export const packagedJobsWorkflow = (packageRoot) => packagedPinned(packageRoot, PACKAGE_JOBS_SOURCE);
64
64
  export const packagedUpkeepWorkflow = (packageRoot) => packagedPinned(packageRoot, PACKAGE_UPKEEP_SOURCE);
65
65
  export const packagedProceduresWorkflow = (packageRoot) => packagedPinned(packageRoot, PACKAGE_PROCEDURES_SOURCE);
66
+ // The notice workflow of the runner (R25 line 10). It is not in PINNED: until
67
+ // the owner records the A2 exit test, only the scratch repo gets it, by hand.
68
+ export const RUNNER_NOTICE_PATH = '.github/workflows/atlas-runner-notice.yml';
69
+ export const PACKAGE_RUNNER_NOTICE_SOURCE = 'door/templates/atlas-runner-notice.yml';
70
+ export const packagedRunnerNotice = (packageRoot) => packagedPinned(packageRoot, PACKAGE_RUNNER_NOTICE_SOURCE);
66
71
  const pinOf = (text) => /@arjunkhera\/atlas@(\d+)\.(\d+)\.(\d+)\b/.exec(text)?.slice(1).map(Number) ?? null;
67
72
  const older = (a, b) => { for (let i = 0; i < 3; i += 1) if (a[i] !== b[i]) return a[i] < b[i]; return false; };
68
73
  // written, refreshed, unchanged or kept: what the writer does with one pinned workflow.
@@ -0,0 +1,41 @@
1
+ name: atlas-runner-notice
2
+ # The notice of the Atlas runner (roadmap R25 line 10). Atlas wrote this file
3
+ # from the Atlas package; do not edit it by hand.
4
+ #
5
+ # A runner session acts as the owner's login, and GitHub does not tell a
6
+ # person about his own comment. So the session posts its run facts in one
7
+ # comment on the notice issue (label atlas-notice), and this job posts the run
8
+ # line as github-actions[bot]. GitHub then tells the owner's phone.
9
+ #
10
+ # It holds no secret, only issues write. It never copies the comment's text:
11
+ # Atlas code reads the facts as JSON and builds the line from ids and counts.
12
+ # Until the owner records the A2 exit test, only the scratch repo has it.
13
+ on:
14
+ issue_comment:
15
+ types: [created]
16
+
17
+ permissions: {}
18
+
19
+ jobs:
20
+ notice:
21
+ if: >-
22
+ github.event.comment.author_association == 'OWNER' &&
23
+ startsWith(github.event.comment.body, '<!-- atlas-runner:run -->') &&
24
+ contains(github.event.issue.labels.*.name, 'atlas-notice')
25
+ runs-on: ubuntu-latest
26
+ timeout-minutes: 5
27
+ permissions:
28
+ issues: write
29
+ steps:
30
+ # actions/setup-node v7.0.0
31
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
32
+ with:
33
+ node-version: "22.22.0"
34
+ - name: Post the run line as the bot
35
+ env:
36
+ GITHUB_TOKEN: ${{ github.token }}
37
+ ISSUE: ${{ github.event.issue.number }}
38
+ COMMENT: ${{ github.event.comment.id }}
39
+ run: |
40
+ set -euo pipefail
41
+ npx --yes --ignore-scripts @arjunkhera/atlas@__ATLAS_VERSION__ runner notice --repo "$GITHUB_REPOSITORY" --issue "$ISSUE" --comment "$COMMENT" | tee -a "$GITHUB_STEP_SUMMARY"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.4.52",
3
+ "version": "0.4.53",
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",
@@ -72,9 +72,12 @@ its environment come from `trigger.yaml` beside this file.
72
72
  `the merge door is off in the runner`, `start stopped`. A landed pull
73
73
  request carries its `number`, its `release`, and the id of its proof
74
74
  comment as `proof`. The command refuses any other text.
75
- 8. Run `atlas runner line --from <file>`. Its output is the last message of
76
- the run, word for word. The notice workflow posts it on the repo's notice
77
- issue.
75
+ 8. Run `atlas runner line --from <file> --notice`. Post its output, word for
76
+ word, as one comment on the repo's notice issue (label `atlas-notice`).
77
+ The notice workflow reads the facts and posts the run line as the bot,
78
+ so GitHub tells the owner.
79
+ 9. Run `atlas runner line --from <file>`. Its output is the last message of
80
+ the run, word for word.
78
81
 
79
82
  ## Until the exit test
80
83