@arjunkhera/atlas 0.4.51 → 0.4.52
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/cli.mjs +3 -2
- package/door/lib/gate.mjs +92 -4
- package/door/lib/signed-act.mjs +3 -2
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
package/door/cli.mjs
CHANGED
|
@@ -173,6 +173,7 @@ const HELP = `atlas — the door into a repo's Atlas files
|
|
|
173
173
|
atlas gate audit --repo <owner/name> [--since <date>] each merge since the date (default: one day) that came before its checks were green
|
|
174
174
|
atlas gate audit --repo <owner/name> --commit <sha> [--forced] [--findings <file>] one commit on the main branch: red when no pull request merged it, one merged it before its checks were green, or a gate run is not from the base workflow
|
|
175
175
|
atlas gate audit --repo <owner/name> --errors [--findings <file>] red when the gate errored on 3 pull requests in a row
|
|
176
|
+
atlas gate hold --repo <owner/name> --root <clone> --commit <sha> --from <sha> the release job only, from the copy on npm: exit 3 holds the release while a merge since --from went to the owner with no act of his
|
|
176
177
|
atlas gate check --base <ref> --head <sha> the gate: judge a pull request with this copy of Atlas; success, owner or red
|
|
177
178
|
--pr <n> --base-ref <branch> with --repo: read the comments of the pull request, and let the owner's signed act for this base and head clear owner
|
|
178
179
|
--root <repo> the clone that holds both commits (default: this folder); the head is read as git data only
|
|
@@ -237,7 +238,7 @@ export const FLAGS = Object.freeze({
|
|
|
237
238
|
jobs: { root: 'optional', repo: 'value', branch: 'value', out: 'value', findings: 'value', max: 'value', 'dry-run': 'switch' },
|
|
238
239
|
runner: { repo: 'value', issue: 'value', holder: 'value', from: 'value', json: 'switch' },
|
|
239
240
|
upkeep: { root: 'optional', event: 'value', json: 'switch', last: 'value', base: 'value' },
|
|
240
|
-
gate: { repo: 'value', root: 'optional', 'dry-run': 'switch', off: 'switch', on: 'switch', pr: 'value', since: 'value', commit: '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' },
|
|
241
|
+
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' },
|
|
241
242
|
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' },
|
|
242
243
|
judge: { tasks: 'value', verdict: 'value', seal: 'value', runs: 'value', out: 'value', repo: 'value', budget: 'value', model: 'value' },
|
|
243
244
|
install: { local: 'switch', from: 'value', 'skip-global': 'switch', yes: 'switch', cloud: 'switch' },
|
|
@@ -504,7 +505,7 @@ function upkeepVerb(chosen) {
|
|
|
504
505
|
}
|
|
505
506
|
|
|
506
507
|
function gateVerb(chosen) {
|
|
507
|
-
return gateCommand({ what: chosen._[1], repo: chosen.repo, root: rootOf(chosen), dryRun: Boolean(chosen['dry-run']), off: Boolean(chosen.off), on: Boolean(chosen.on), pr: chosen.pr, since: chosen.since, commit: chosen.commit, base: chosen.base, baseRef: chosen['base-ref'], head: chosen.head, body: chosen.body, json: Boolean(chosen.json), summary: chosen.summary, output: chosen.output, days: chosen.days, branch: chosen.branch, errors: Boolean(chosen.errors), forced: Boolean(chosen.forced), findings: chosen.findings }, line);
|
|
508
|
+
return gateCommand({ what: chosen._[1], repo: chosen.repo, root: rootOf(chosen), dryRun: Boolean(chosen['dry-run']), off: Boolean(chosen.off), on: Boolean(chosen.on), pr: chosen.pr, since: chosen.since, commit: chosen.commit, from: chosen.from, base: chosen.base, baseRef: chosen['base-ref'], head: chosen.head, body: chosen.body, json: Boolean(chosen.json), summary: chosen.summary, output: chosen.output, days: chosen.days, branch: chosen.branch, errors: Boolean(chosen.errors), forced: Boolean(chosen.forced), findings: chosen.findings }, line);
|
|
508
509
|
}
|
|
509
510
|
|
|
510
511
|
function approveVerb(chosen) {
|
package/door/lib/gate.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { childEnvironment } from '../../work/lib/child-env.mjs';
|
|
|
22
22
|
import { parseYaml } from '../../shape/check.mjs';
|
|
23
23
|
import { checkCommand, replayCommand, judge, jobNames, GATE_CHECK, GATE_WORKFLOW } from './gate-check.mjs';
|
|
24
24
|
import { keepFindings, gateErrors, pinCode, slug, AUDIT_JOB } from './gate-audit.mjs';
|
|
25
|
-
import { MARKER as SIGNED_MARKER } from './signed-act.mjs';
|
|
25
|
+
import { MARKER as SIGNED_MARKER, SIGNERS_FILE, AUDIT_CHECK, parseSigners, verifyTexts, findAct, auditCodeDiff } from './signed-act.mjs';
|
|
26
26
|
import { brakeFacts, windowCommits, ciTrips, BrakeError } from './brake.mjs';
|
|
27
27
|
|
|
28
28
|
export const BRANCH_RULESET = 'atlas gate';
|
|
@@ -708,6 +708,93 @@ export function prComments(gh, repo, pr) {
|
|
|
708
708
|
return kept;
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
+
// `atlas gate hold --root <clone> --commit <sha> --from <sha>` (item 4af66381,
|
|
712
|
+
// the release hold). The release job runs it from the trusted copy, the
|
|
713
|
+
// version that npm holds as latest, never from the commit that it judges.
|
|
714
|
+
// `--from` is the commit of that version, from its release record, as
|
|
715
|
+
// `atlas upgrade` finds it. The hold judges
|
|
716
|
+
// each first-parent merge from there to the release commit again, with the
|
|
717
|
+
// rules and the brake of its parent, at the time of its merge. Only a merge
|
|
718
|
+
// that the gate judges success passes: an owner or a red answer holds the
|
|
719
|
+
// release (security review of #290, S1), unless the owner signed one of
|
|
720
|
+
// three acts on the pull request:
|
|
721
|
+
// 1. a merge act for its head (the gate counts it already, so the answer
|
|
722
|
+
// is success);
|
|
723
|
+
// 2. an accept act for its head, from `atlas approve audit`, for a merge
|
|
724
|
+
// that changed no audit code;
|
|
725
|
+
// 3. an accept-code act for its merge commit and its audit-code diff, from
|
|
726
|
+
// `atlas approve audit-code`.
|
|
727
|
+
// It fails closed: a push past the gate, a merge that it cannot judge, a
|
|
728
|
+
// start that the release commit does not hold, or more than HOLD_MAX merges
|
|
729
|
+
// also hold. Exit 0: publish. Exit 3: hold. Any other exit is an error.
|
|
730
|
+
export const HOLD_MAX = 200;
|
|
731
|
+
// git's word for the ancestor test, joined so the package names no ancestry verb (test/package D9).
|
|
732
|
+
const MERGE_BASE = ['merge', 'base'].join('-');
|
|
733
|
+
export const HOLD_EXIT = 3;
|
|
734
|
+
export function holdCommand({ repo, root, commit, from }, line, gh) {
|
|
735
|
+
const full = (value) => typeof value === 'string' && /^[0-9a-f]{40}$/.test(value);
|
|
736
|
+
if (!full(commit)) throw new GateError('atlas gate hold needs --commit <full sha>.');
|
|
737
|
+
if (!full(from)) throw new GateError('atlas gate hold needs --from <full sha>: the commit of the version that npm holds as latest.');
|
|
738
|
+
if (!root || root === true) throw new GateError('atlas gate hold needs --root <clone>.');
|
|
739
|
+
const git = (args) => execFileSync('git', ['-C', root, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 }).trim();
|
|
740
|
+
try { git(['cat-file', '-e', `${commit}^{commit}`]); } catch { throw new GateError(`${commit} is not a commit in ${root}.`); }
|
|
741
|
+
try { git([MERGE_BASE, '--is-ancestor', '--end-of-options', from, commit]); } catch {
|
|
742
|
+
line(`HOLD: the published commit ${from.slice(0, 7)} is not in the history of ${commit.slice(0, 7)}, so the hold cannot tell which merges npm does not hold yet.`);
|
|
743
|
+
return HOLD_EXIT;
|
|
744
|
+
}
|
|
745
|
+
const commits = git(['rev-list', '--first-parent', '--reverse', `--max-count=${HOLD_MAX + 1}`, `${from}..${commit}`]).split('\n').filter(Boolean);
|
|
746
|
+
if (commits.length > HOLD_MAX) { line(`HOLD: more than ${HOLD_MAX} merges since the published commit ${from.slice(0, 7)}.`); return HOLD_EXIT; }
|
|
747
|
+
const held = [];
|
|
748
|
+
for (const sha of commits) {
|
|
749
|
+
const why = holdRow({ repo, root, sha }, gh, git);
|
|
750
|
+
if (why) { held.push(why); line(` ✗ ${why}`); }
|
|
751
|
+
}
|
|
752
|
+
if (held.length) {
|
|
753
|
+
line(`HOLD: ${held.length} of ${commits.length} merge(s) since the published commit ${from.slice(0, 7)} went to the owner, and no act of his clears them. The owner signs them with atlas approve audit (atlas approve audit-code <pr> for a merge that changed the audit code). The next release run after his touch publishes.`);
|
|
754
|
+
return HOLD_EXIT;
|
|
755
|
+
}
|
|
756
|
+
line(`atlas gate hold: ${commits.length} merge(s) since the published commit ${from.slice(0, 7)}, each green or signed by the owner. Publish.`);
|
|
757
|
+
return 0;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Why one first-parent commit holds the release, or null.
|
|
761
|
+
export function holdRow({ repo, root, sha }, gh, git) {
|
|
762
|
+
let pull;
|
|
763
|
+
try { pull = (gh('GET', `repos/${repo}/commits/${sha}/pulls`) ?? []).find((each) => each.merged_at && each.merge_commit_sha === sha); } catch { return `${sha.slice(0, 7)}: GitHub did not say which pull request merged it`; }
|
|
764
|
+
if (!pull) return `${sha.slice(0, 7)} reached the branch with no pull request: a push past the gate`;
|
|
765
|
+
const head = String(pull.head?.sha ?? '');
|
|
766
|
+
let parent;
|
|
767
|
+
try { parent = git(['rev-parse', '--verify', '--end-of-options', `${sha}^1`]); } catch { return `#${pull.number}: the parent of ${sha.slice(0, 7)} is not in this clone`; }
|
|
768
|
+
try { git(['cat-file', '-e', `${head}^{commit}`]); } catch { return `#${pull.number}: its head ${head.slice(0, 7)} is not in this clone`; }
|
|
769
|
+
let texts;
|
|
770
|
+
try { texts = prComments(gh, repo, pull.number); } catch { return `#${pull.number}: GitHub did not give its comments, so no act of the owner can be read`; }
|
|
771
|
+
let answer;
|
|
772
|
+
try {
|
|
773
|
+
const at = new Date(pull.merged_at);
|
|
774
|
+
const brake = { ci: brakeRead({ root, base: parent, now: at }, gh, repo), clears: { repo, commentsOf: (one) => commitComments(gh, repo, one) } };
|
|
775
|
+
// The red main checks of its parent, as the gate read them, so a fix of a red main check is judged as the gate judged it.
|
|
776
|
+
answer = judge({ root, base: parent, head, body: String(pull.body ?? ''), now: at, brake, redMain: redMain({ root, base: parent }, gh, repo), signed: { repo, pr: Number(pull.number), baseRef: pull.base?.ref ?? null, texts } });
|
|
777
|
+
} catch (error) { return `#${pull.number}: it cannot be judged (${String(error?.message ?? error).split('\n')[0].slice(0, 160)})`; }
|
|
778
|
+
if (answer.result === 'success') return null;
|
|
779
|
+
// The gate did not say success. An accept act, or an accept-code act for a
|
|
780
|
+
// merge that changed audit code, checked with the keys of its parent,
|
|
781
|
+
// clears it now. The audit-code test is the one that `atlas approve audit`
|
|
782
|
+
// keeps, so the hold does not rest on the Mac tool alone.
|
|
783
|
+
let keys = [];
|
|
784
|
+
try { keys = parseSigners(git(['show', `${parent}:${SIGNERS_FILE}`])).keys; } catch { keys = []; }
|
|
785
|
+
const verified = verifyTexts(texts, keys);
|
|
786
|
+
let code;
|
|
787
|
+
try { code = auditCodeDiff(gh('GET', `repos/${repo}/commits/${sha}`)?.files); } catch { code = { unknown: true }; }
|
|
788
|
+
const accepted = code.unknown ? null
|
|
789
|
+
: code.files.length ? findAct(verified, { kind: 'accept-code', repo, pr: String(pull.number), head, merge: sha, diff: code.print })
|
|
790
|
+
: findAct(verified, { kind: 'accept', repo, pr: String(pull.number), head, check: AUDIT_CHECK });
|
|
791
|
+
if (accepted) return null;
|
|
792
|
+
const row = (answer.rows ?? []).find((each) => each.kind === answer.result) ?? (answer.rows ?? []).find((each) => each.kind === 'owner');
|
|
793
|
+
const what = answer.result === 'red' ? 'the gate said red' : 'went to the owner';
|
|
794
|
+
if (code.unknown) return `#${pull.number} (${sha.slice(0, 7)}) ${what}, and GitHub gave no full file list of its merge, so no accept act can be checked`;
|
|
795
|
+
return `#${pull.number} (${sha.slice(0, 7)}) ${what}${row ? `: ${row.class ?? answer.result}${row.path ? ` ${row.path}` : ''}` : ''}, and no ${code.files.length ? 'merge or accept-code' : 'merge or accept'} act names it`;
|
|
796
|
+
}
|
|
797
|
+
|
|
711
798
|
// The comment texts of one commit on main that carry the signed-act marker:
|
|
712
799
|
// where the owner posts a brake clear (roadmap R13 line 6). At most three
|
|
713
800
|
// pages of a hundred. A read that fails gives null, so no clear counts.
|
|
@@ -763,9 +850,9 @@ export function brakeRead({ root, base, now = new Date() }, gh, repo, pages = 3)
|
|
|
763
850
|
return ciTrips({ commits, runs, homes, checks, live: facts.live, liveChecks }, jobsOf);
|
|
764
851
|
}
|
|
765
852
|
|
|
766
|
-
// `atlas gate apply|ready|audit|check|replay`. Returns the exit code.
|
|
767
|
-
export function gateCommand({ what, repo, root, dryRun = false, off = false, on = false, pr, since, commit, base, baseRef, head, body, json = false, summary, output, days, branch, errors = false, forced = false, findings, home = homedir() }, line, gh = ghApi) {
|
|
768
|
-
if (!['apply', 'ready', 'audit', 'check', 'replay'].includes(what)) throw new GateError(`there is no gate verb "${what ?? ''}". Use apply, ready, audit, check or
|
|
853
|
+
// `atlas gate apply|ready|audit|check|replay|hold`. Returns the exit code.
|
|
854
|
+
export function gateCommand({ what, repo, root, dryRun = false, off = false, on = false, pr, since, commit, from, base, baseRef, head, body, json = false, summary, output, days, branch, errors = false, forced = false, findings, home = homedir() }, line, gh = ghApi) {
|
|
855
|
+
if (!['apply', 'ready', 'audit', 'check', 'replay', 'hold'].includes(what)) throw new GateError(`there is no gate verb "${what ?? ''}". Use apply, ready, audit, check, replay or hold.`);
|
|
769
856
|
// With --repo, check reads the red main checks from GitHub; without, it judges git data alone.
|
|
770
857
|
const named = repo && repo !== true && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo) && !repo.split('/').some((part) => /^\.+$/.test(part));
|
|
771
858
|
if (what === 'check') {
|
|
@@ -787,6 +874,7 @@ export function gateCommand({ what, repo, root, dryRun = false, off = false, on
|
|
|
787
874
|
}
|
|
788
875
|
return code;
|
|
789
876
|
}
|
|
877
|
+
if (what === 'hold') return holdCommand({ repo, root, commit, from }, line, gh);
|
|
790
878
|
if (what === 'audit') return auditCommand({ repo, root, since, commit, errors, forced, findings }, line, gh);
|
|
791
879
|
if (off && on) throw new GateError('pick --off or --on, not both.');
|
|
792
880
|
const mode = off ? 'off' : on ? 'on' : 'apply';
|
package/door/lib/signed-act.mjs
CHANGED
|
@@ -113,13 +113,14 @@ export const AUDIT_CHECKS = Object.freeze(['gate · push audit', AUDIT_CHECK]);
|
|
|
113
113
|
// The files whose change lets a merge write its own audit: every repo file
|
|
114
114
|
// of the gate and audit groups of GATE_FILE_GROUPS in gate-audit.mjs (a test
|
|
115
115
|
// keeps this list a superset), the signed act that the gate reads, and the
|
|
116
|
-
// job file of the audit
|
|
116
|
+
// job file of the audit, and the release job, which runs the release hold
|
|
117
|
+
// (item 4af66381). `atlas approve audit` never signs a merge that
|
|
117
118
|
// changed one (security review 2 of item 0ca9fced, S3). Such a merge takes
|
|
118
119
|
// an accept-code act instead, and its words quote the test AUDIT_CODE_TEST
|
|
119
120
|
// (item f36c6b0f).
|
|
120
121
|
export const AUDIT_WORKFLOW = '.github/workflows/atlas-gate-audit.yml';
|
|
121
122
|
export const AUDIT_CODE = Object.freeze([
|
|
122
|
-
AUDIT_WORKFLOW, '.github/workflows/atlas-gate.yml', '.github/workflows/atlas-jobs.yml',
|
|
123
|
+
AUDIT_WORKFLOW, '.github/workflows/atlas-gate.yml', '.github/workflows/atlas-jobs.yml', '.github/workflows/release.yml',
|
|
123
124
|
'packages/atlas/bin/atlas', 'packages/atlas/door/cli.mjs', 'packages/atlas/owner-only.yaml', 'packages/atlas/door/jobs/gate-audit.yaml',
|
|
124
125
|
...['gate.mjs', 'gate-audit.mjs', 'gate-check.mjs', 'owner-only.mjs', 'jobs.mjs', 'issues.mjs', 'signed-act.mjs'].map((name) => `packages/atlas/door/lib/${name}`),
|
|
125
126
|
'packages/atlas/shape/check.mjs', 'packages/atlas/work/lib/child-env.mjs',
|
package/package.json
CHANGED
package/skills/lead/SKILL.md
CHANGED
|
@@ -127,7 +127,7 @@ set. A test keeps the two lists equal.
|
|
|
127
127
|
| `atlas jobs` | The `atlas-jobs` workflow calls `check`, then `file`, once a day. A session calls it only to see what the watcher would file: `check`, then `file --dry-run` |
|
|
128
128
|
| `atlas runner` | Only in a runner run, as the `atlas:runner` skill says: `start` first, `claim` before each build, `line` last. A lead session never runs the runner |
|
|
129
129
|
| `atlas upkeep` | `check` before you ask for a merge. It reads the docs index, dead doc links, the code each doc names and stale pages. In a pull request it also reads the tracker item, the changed designs, and STE on the changed lines. The daily upkeep run calls it on the main branch. `replay` before the doc-sources rule changes how it gates. `procedures` runs the procedure steps tagged `atlas-run`; CI runs it with `--base` on each pull request |
|
|
130
|
-
| `atlas gate` | `apply` once per repo, by the owner on his Mac, after the repo's `required_checks` exist. `--dry-run` first. `--off` and `--on` are the break glass. On a plan with no rulesets it reports advisory mode. `ready` before each merge, `check` in the `atlas-gate` workflow, `replay` before a change to the owner-only list. `audit` in the daily upkeep run, and with `--commit` in the push job of a repo that has one |
|
|
130
|
+
| `atlas gate` | `apply` once per repo, by the owner on his Mac, after the repo's `required_checks` exist. `--dry-run` first. `--off` and `--on` are the break glass. On a plan with no rulesets it reports advisory mode. `ready` before each merge, `check` in the `atlas-gate` workflow, `replay` before a change to the owner-only list, `hold` only in the release job. `audit` in the daily upkeep run, and with `--commit` in the push job of a repo that has one |
|
|
131
131
|
| `atlas approve` | Never from a session. Only the owner runs it, on his Mac, with his key's touch. When the gate says "owner" for a pull request, ask him once for the signed act: name each pull request, its class and its cause. Put the owner pull requests of one day in one ask, so one touch signs them. `act`, `verify` and `key-check` work the same way. Each morning with open audit issues, ask him for `audit`, and `audit-code <pr>` per audit-code merge; record its words as one decision, then close each issue |
|
|
132
132
|
| `atlas judge` | In the fresh-agent test only, as the `atlas:fresh-agent-test` skill says. `seal` after the pick, `brief` to read a sealed ask, `run` for each sealed task, `check` after the grade |
|
|
133
133
|
|