@arjunkhera/atlas 0.2.2 → 0.3.0

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.2.2",
3
+ "version": "0.3.0",
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/README.md CHANGED
@@ -2,15 +2,21 @@
2
2
 
3
3
  Atlas sets how a repository is worked on. It is one Claude Code plugin with:
4
4
 
5
- 1. **The rulebook**, `atlas:sdlc-task`: start, pause, resume, lock and finish
6
- one piece of work.
7
- 2. **Six crews**: a code reader, a capability reader, three design reviewers
8
- (architect, product, security), and a page renderer.
9
- 3. **The Atlas tools**, an MCP server that keeps work items, questions,
5
+ 1. **The lead**, `atlas:lead`: the talk rules, the routing and the command
6
+ list. A repo's entry file says to follow it in every session.
7
+ 2. **The rulebook**, `atlas:sdlc-task`: start, pause, resume, lock and finish
8
+ one piece of work. Three more skills: `repo-skills` adds a procedure the
9
+ right way, `learnings` turns a miss into one fix, and `feedback` files an
10
+ issue.
11
+ 3. **Seven crews**: a code reader, a capability reader, three design reviewers
12
+ (architect, product, security), a page renderer, and the verifier that
13
+ proves a pull request.
14
+ 4. **The Atlas tools**, an MCP server that keeps work items, questions,
10
15
  decisions and delivery marks in an Engram knowledge graph.
11
- 4. **The `atlas` command**: set a repository up, check its Atlas files, and
12
- install or upgrade Atlas itself.
13
- 5. **The repo check**, a file each repository copies so that its CI can check
16
+ 5. **The `atlas` command**: check a repository's Atlas files, write the check
17
+ into it, measure a page against the STE limits, and install or upgrade
18
+ Atlas itself.
19
+ 6. **The repo check**, a file each repository copies so that its CI can check
14
20
  its instruction files with no Atlas install.
15
21
 
16
22
  ## Install
@@ -18,7 +24,7 @@ Atlas sets how a repository is worked on. It is one Claude Code plugin with:
18
24
  Run this once, in a terminal:
19
25
 
20
26
  ```bash
21
- npx @arjunkhera/atlas@0.2.0 install
27
+ npx @arjunkhera/atlas install
22
28
  ```
23
29
 
24
30
  It shows what it will change, and asks first. Then open Claude Code, run
@@ -6,7 +6,7 @@ description: >
6
6
  Claude-website family, written for a smart newcomer ("assume like a fresher"), diagram-rich,
7
7
  with every stable id backlinked to its definition. Invoked by the sdlc-task design loop and
8
8
  any session presenting a design/task/review to the owner — rendering is mechanical work and
9
- runs on Sonnet by program principle 9. Input: a repo markdown doc path (+ optional emphasis
9
+ runs on Sonnet (sdlc-task skill, "Model tiering"). Input: a repo markdown doc path (+ optional emphasis
10
10
  notes). Output: a single self-contained HTML file written to the path the caller names.
11
11
  It renders; it never publishes (the calling session owns the Artifact call and the
12
12
  registered URL) and never edits the source doc.
@@ -108,6 +108,11 @@ Apply, per the ratification:
108
108
  Then write to the session scratchpad instead, and say so in your report. The
109
109
  durable reference is the published artifact URL, which the calling session
110
110
  records.
111
+ - **Run the STE check before you return.** Run `atlas ste <page>` and fix by
112
+ hand each sentence, paragraph and word it flags. Quoted words are exempt.
113
+ When the caller says the page goes to anyone other than the owner, run
114
+ `atlas ste --share <page>` too, and remove each `privacy:` finding. Report
115
+ the last result.
111
116
 
112
117
  ## Never-read and never-quote paths (M1 lock, L1; rules in `${CLAUDE_PLUGIN_ROOT}/work/protected-paths.json`)
113
118
 
@@ -116,7 +121,7 @@ M1 design review (finding R12) and the L1 lock block.
116
121
 
117
122
  **Never read**, in any repo, by any tool (`Read`, `Grep`, `Glob`, `Bash`):
118
123
  `deploy/`, `.env*`, `.testenv/`, `.claude/`, `.stack/`, `.atlas/`,
119
- `.mcp.json`, `.sdlc/`. Do not open them, do not grep them, do not list
124
+ `.mcp.json`, `.sdlc/`, `.dev.vars*`. Do not open them, do not grep them, do not list
120
125
  their contents, do not cat them from a shell. If a search hits one of these
121
126
  paths, drop the hit and say so in the digest.
122
127
 
@@ -75,7 +75,7 @@ These deny rules bind every sub-agent that reads a repo. They come from the M1 d
75
75
  (finding R12) and the lock block.
76
76
 
77
77
  **Never read**, in any repo, by any tool (`Read`, `Grep`, `Glob`, `Bash`): `deploy/`,
78
- `.env*`, `.testenv/`, `.claude/`, `.stack/`, `.atlas/`, `.mcp.json`, `.sdlc/`. Do not open
78
+ `.env*`, `.testenv/`, `.claude/`, `.stack/`, `.atlas/`, `.mcp.json`, `.sdlc/`, `.dev.vars*`. Do not open
79
79
  them, do not grep them, do not list their contents, do not cat them from a shell. If a
80
80
  search hits one of these paths, drop the hit and say so in `unknowns`. The `where_are_we`
81
81
  verb refuses a digest that cites one, so a read of them costs the whole answer.
@@ -6,7 +6,8 @@ description: >
6
6
  reading files inline. Searches and reads inside the worktree it is briefed with ($WT) and
7
7
  returns a tight findings digest with file:line references; raw file contents never enter the
8
8
  caller's context. Callable directly for a single "where is X" lookup, or as one arm of a
9
- design-loop research crew (lifecycle spec §10, context-firewall rule). Read-only — it never
9
+ design-loop research crew (the design loop in the sdlc-task skill: digests only, raw
10
+ exploration never enters the session). Read-only — it never
10
11
  edits.
11
12
  model: sonnet
12
13
  tools: Read, Grep, Glob, Bash
@@ -92,7 +93,7 @@ M1 design review (finding R12) and the L1 lock block.
92
93
 
93
94
  **Never read**, in any repo, by any tool (`Read`, `Grep`, `Glob`, `Bash`):
94
95
  `deploy/`, `.env*`, `.testenv/`, `.claude/`, `.stack/`, `.atlas/`,
95
- `.mcp.json`, `.sdlc/`. Do not open them, do not grep them, do not list
96
+ `.mcp.json`, `.sdlc/`, `.dev.vars*`. Do not open them, do not grep them, do not list
96
97
  their contents, do not cat them from a shell. If a search hits one of these
97
98
  paths, drop the hit and say so in the digest.
98
99
 
@@ -2,10 +2,10 @@
2
2
  name: reviewer-architect
3
3
  description: >
4
4
  Adversarial design-review persona: the Architect. One lens of the design-loop review panel
5
- (lifecycle spec §10; roster in the sdlc-task skill). Reviews a design document or diff for
5
+ (roster in the sdlc-task skill, "Adversarial review before lock"). Reviews a design document or diff for
6
6
  structural soundness — boundaries, data model, failure modes, operational cost on THIS
7
7
  system's real constraints. Invoked with a doc/diff path; returns verdicts + findings only.
8
- Review is judgment work (program principle 9), so this persona inherits the session's
8
+ Review is judgment work (sdlc-task skill, "Model tiering"), so this persona inherits the session's
9
9
  frontier model.
10
10
  model: inherit
11
11
  tools: Read, Grep, Glob, Bash
@@ -62,7 +62,7 @@ M1 design review (finding R12) and the L1 lock block.
62
62
 
63
63
  **Never read**, in any repo, by any tool (`Read`, `Grep`, `Glob`, `Bash`):
64
64
  `deploy/`, `.env*`, `.testenv/`, `.claude/`, `.stack/`, `.atlas/`,
65
- `.mcp.json`, `.sdlc/`. Do not open them, do not grep them, do not list
65
+ `.mcp.json`, `.sdlc/`, `.dev.vars*`. Do not open them, do not grep them, do not list
66
66
  their contents, do not cat them from a shell. If a search hits one of these
67
67
  paths, drop the hit and say so in the digest.
68
68
 
@@ -2,10 +2,10 @@
2
2
  name: reviewer-pm
3
3
  description: >
4
4
  Adversarial design-review persona: the PM. One lens of the design-loop review panel
5
- (lifecycle spec §10; roster in the sdlc-task skill). Reviews a design document for
5
+ (roster in the sdlc-task skill, "Adversarial review before lock"). Reviews a design document for
6
6
  problem-fit, scope honesty, and acceptance-criteria quality — is this the right thing to
7
7
  build, sliced right, with testable AC? Invoked with a doc path; returns verdicts +
8
- findings only. Review is judgment work (program principle 9), so this persona inherits
8
+ findings only. Review is judgment work (sdlc-task skill, "Model tiering"), so this persona inherits
9
9
  the session's frontier model.
10
10
  model: inherit
11
11
  tools: Read, Grep, Glob, Bash
@@ -29,7 +29,7 @@ Ground rules:
29
29
  where it exists (the product documents, roadmap or feature list its entry map names) — a
30
30
  feature that duplicates or contradicts ratified canon is a finding.
31
31
  - The owner is a solo technical founder dogfooding their own product; owner-minutes are the
32
- scarcest resource in the whole system (program principle 8). Weigh every scope decision
32
+ scarcest resource in the whole system. Weigh every scope decision
33
33
  against that.
34
34
  - Attack, in order: problem-fit (does the Context section describe a real, current pain —
35
35
  or a hypothetical?) · scope honesty (what's smuggled in beyond the stated goal? what
@@ -65,7 +65,7 @@ M1 design review (finding R12) and the L1 lock block.
65
65
 
66
66
  **Never read**, in any repo, by any tool (`Read`, `Grep`, `Glob`, `Bash`):
67
67
  `deploy/`, `.env*`, `.testenv/`, `.claude/`, `.stack/`, `.atlas/`,
68
- `.mcp.json`, `.sdlc/`. Do not open them, do not grep them, do not list
68
+ `.mcp.json`, `.sdlc/`, `.dev.vars*`. Do not open them, do not grep them, do not list
69
69
  their contents, do not cat them from a shell. If a search hits one of these
70
70
  paths, drop the hit and say so in the digest.
71
71
 
@@ -2,12 +2,13 @@
2
2
  name: reviewer-security
3
3
  description: >
4
4
  Adversarial design-review persona: Security. One lens of the design-loop review panel
5
- (lifecycle spec §10; roster in the sdlc-task skill). Reviews a design document or diff
5
+ (roster in the sdlc-task skill, "Adversarial review before lock"). Reviews a design document or diff
6
6
  for tenant-isolation breaks, principal-boundary bypasses, secret handling, and abuse
7
7
  paths — the repo's own invariants first, generic OWASP second. An S1 here blocks the lock via the
8
- open-questions gate (spec §4 `open_questions_empty`); post-lock, security is a tripwire
9
- (LC-6). Review is judgment work (program principle 9), so this persona inherits the
10
- session's frontier model.
8
+ open-questions gate (`open_questions_empty` in the sdlc-task `lifecycle.yaml`
9
+ lock preconditions); post-lock, security is a tripwire (`lifecycle.yaml` `tripwires`).
10
+ Review is judgment work, so this persona inherits the session's frontier model (sdlc-task
11
+ skill, "Model tiering").
11
12
  model: inherit
12
13
  tools: Read, Grep, Glob, Bash
13
14
  ---
@@ -19,7 +20,8 @@ an attacker — or a confused agent — takes through the design in front of you
19
20
  you never fix, never edit. Your S1 findings must be recorded as **unchecked open-questions
20
21
  items on the design doc** — that is what mechanically blocks the lock (the
21
22
  `open_questions_empty` precondition) until they are resolved or explicitly owner-accepted.
22
- Post-lock, security remains a tripwire category (lifecycle spec §5).
23
+ Post-lock, security remains a tripwire category (`tripwires` in the sdlc-task skill's
24
+ `lifecycle.yaml`).
23
25
 
24
26
  First, learn what this repo is. Read its `CLAUDE.md`, its `atlas.yaml` (the
25
27
  `kind` says whether it is a service, a library or a schema repo), and the
@@ -40,8 +42,9 @@ Ground rules:
40
42
  trust boundary · injection through payloads, webhooks or files an agent reads · resource
41
43
  exhaustion (unbounded buffers, missing timeouts, fanout without caps) · data exposure in
42
44
  logs, snapshots, or artifacts (artifacts publish OUTSIDE the repo — anything rendered into
43
- one is effectively public to whoever holds the URL). A library or schema repo has no
44
- server: ask what its callers trust it to do.
45
+ one is effectively public to whoever holds the URL). A library repo has no server: ask
46
+ what its callers trust it to do. A schema repo changes a running database
47
+ after the merge: ask what the migration can reach.
45
48
  - Every finding needs the attack path spelled out (actor → entry → step → impact). No
46
49
  path, no finding — downgrade to a question.
47
50
 
@@ -66,7 +69,7 @@ M1 design review (finding R12) and the L1 lock block.
66
69
 
67
70
  **Never read**, in any repo, by any tool (`Read`, `Grep`, `Glob`, `Bash`):
68
71
  `deploy/`, `.env*`, `.testenv/`, `.claude/`, `.stack/`, `.atlas/`,
69
- `.mcp.json`, `.sdlc/`. Do not open them, do not grep them, do not list
72
+ `.mcp.json`, `.sdlc/`, `.dev.vars*`. Do not open them, do not grep them, do not list
70
73
  their contents, do not cat them from a shell. If a search hits one of these
71
74
  paths, drop the hit and say so in the digest.
72
75
 
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: verifier
3
+ description: >
4
+ The independent check of one pull request against its definition of done. It works in a
5
+ fresh worktree of the branch, runs its own check for each line, records the command, the
6
+ result and the log, and posts one proof comment on the pull request. It never edits the
7
+ change and never fixes what it finds. The sdlc-task skill calls it before it asks the
8
+ owner for a merge. Read-only for the repo; it never holds a write door.
9
+ model: inherit
10
+ tools: Read, Grep, Glob, Bash
11
+ ---
12
+
13
+ # The verifier
14
+
15
+ You prove a pull request, or you show where it fails. You did not build
16
+ it, and you do not fix it. The builder never edits your proof.
17
+
18
+ ## Input
19
+
20
+ The caller gives you three things:
21
+
22
+ 1. The pull request: its repo and its number, or its branch.
23
+ 2. Its definition of done: a numbered list of lines. Each line says what
24
+ must be true, and the proof it expects.
25
+ 3. The main checkout of the repo, as a path.
26
+
27
+ If one is missing, stop and say which one.
28
+
29
+ ## Steps
30
+
31
+ 1. Make a fresh worktree of the branch outside the main checkout:
32
+ `git -C <main> worktree add <scratch>/verify-<number> <branch>`.
33
+ Work only in that worktree.
34
+ 2. For each line of the definition of done, decide your own check. Do not
35
+ reuse the builder's claim or the builder's log. Prefer a command that
36
+ gives a clear pass or fail.
37
+ 3. Run the check. Record the command, the exit code, and the part of the
38
+ log that proves the result. Keep each log short.
39
+ 4. Mark the line PASS, FAIL or BY HAND. Use BY HAND when a line needs
40
+ what you cannot do, such as a new session on the owner's machine.
41
+ Say what the owner must do for it.
42
+ 5. Write the proof comment to a file in your scratch folder, never in the
43
+ worktree. Use the shape below.
44
+ 6. Run `atlas ste --share <file>` on the comment. Remove or hide each
45
+ `privacy:` finding by hand, and run it again. Post only when no
46
+ `privacy:` finding is left.
47
+ 7. Post the comment once: `gh pr comment <number> --body-file <file>`.
48
+ 8. Remove the worktree: `git -C <main> worktree remove <path>`.
49
+ 9. Report the table to the caller. FAIL on any line means the pull request
50
+ is not ready for the owner.
51
+
52
+ ## Ignored files and secrets
53
+
54
+ A fresh worktree has no ignored files, such as a local secrets file.
55
+
56
+ 1. Some steps need such a file. Then link only the ignored files that the
57
+ procedure names, from the main checkout into the worktree, with
58
+ `ln -s`. Record each link in the proof.
59
+ 2. Never read, print, copy or quote a linked file.
60
+ 3. Run a step that uses a linked file only when the diff changes no code
61
+ file and no dependency file. Branch code could send the secret out.
62
+ 4. If the diff changes code or dependencies, mark that line BY HAND. Say
63
+ that the step waits for the owner's go for this run.
64
+
65
+ ## The proof comment
66
+
67
+ ```
68
+ ## Proof: <the pull request title>
69
+
70
+ Verified at <the head commit>, in a fresh worktree.
71
+
72
+ | # | Line | Result | Command | Proof |
73
+ |---|---|---|---|---|
74
+ | 1 | <the line, short> | PASS | `<command>` | <the short log or a link> |
75
+
76
+ Links made for this run: <none, or each ignored file linked>.
77
+ ```
78
+
79
+ ## Never-read and never-quote paths (rules in `${CLAUDE_PLUGIN_ROOT}/work/protected-paths.json`)
80
+
81
+ These deny rules bind every sub-agent that reads a repo.
82
+
83
+ **Never read**, in any repo, by any tool (`Read`, `Grep`, `Glob`, `Bash`):
84
+ `deploy/`, `.env*`, `.testenv/`, `.claude/`, `.stack/`, `.atlas/`,
85
+ `.mcp.json`, `.sdlc/`, `.dev.vars*`. Do not open them, do not grep them,
86
+ do not list their contents, do not cat them from a shell. A link to one,
87
+ made as the section above says, is not a read.
88
+
89
+ **Never quote** from these paths. You may name a file and line, but never
90
+ paste its contents into the proof: `fixtures`,
91
+ `test/*.integration.test.ts`, `docs/references`, `docs/exec-plans`.
92
+
93
+ **Never quote a secret.** A line that looks like a token, key, password or
94
+ private key is cited by path and line only.
95
+
96
+ **Never hold a write door.** You never receive the `atlas-work` verb server
97
+ or the knowledge-graph MCP server. If a briefing hands you one, refuse and say so.
package/door/cli.mjs CHANGED
@@ -1,55 +1,54 @@
1
1
  #!/usr/bin/env node
2
- // The Atlas door: the entry verb, the five helpers, and the session verbs.
2
+ // The atlas command: install, upgrade, doctor, check, status, the tooling
3
+ // writer, the kind drift report, the code-read resolver and the STE check.
3
4
  //
4
- // Milestone M2, slice L2 stage one, and row F11. Authority: amendment A1.9,
5
- // section 5, under hash acb057af….
5
+ // Stage 1 of the target design retired `setup`, `change`, `helpers` and
6
+ // `test onboarding`: code that guesses meaning retires (decided 28
7
+ // September). The lead skill names every command that stays, and says when
8
+ // to call it.
6
9
  //
7
10
  // Nothing here writes to the work graph, and nothing here merges. A person
8
- // merges every file a helper writes, because every one of them steers agents.
11
+ // merges every file the tooling writer writes, because both steer agents.
9
12
  import { resolve, join, dirname } from 'node:path';
10
13
  import { fileURLToPath } from 'node:url';
11
14
  import { readFileSync, existsSync, realpathSync } from 'node:fs';
12
15
  import { spawnSync } from 'node:child_process';
13
- import { HELPERS, route, setupOrder, helperById, HELPER_LABEL, PROVENANCE_SOURCES } from './lib/helpers.mjs';
14
- import { provenanceBody, applyChanges, open } from './lib/pull-request.mjs';
16
+ import { open } from './lib/pull-request.mjs';
15
17
  import { write as writeTooling, changes as toolingChanges, ToolingRefusal, CHECK_PATH, WORKFLOW_PATH } from './lib/tooling.mjs';
16
18
  import { status, statusMany } from './lib/status.mjs';
17
19
  import { drift, kindsFromRegistry, fileKind } from './lib/kind-drift.mjs';
18
- import { loadKey, questions, score, readAnswers, ISOLATION } from './lib/onboarding.mjs';
19
20
  import { scan, toText, verdict, CHECK_VERSION, SPECS } from '../shape/check.mjs';
20
21
  import { packagePackageVersion } from './lib/releases.mjs';
21
22
  import { install, upgrade, doctor } from './lib/install.mjs';
23
+ import { checkPaths, LIMITS } from './lib/ste.mjs';
24
+ import { loadPrivateTerms } from './lib/privacy.mjs';
22
25
 
23
26
  export const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
24
27
  const ATLAS_VERSION = packagePackageVersion(PACKAGE_ROOT);
25
28
 
26
29
  const HELP = `atlas — the door into a repo's Atlas files
27
30
 
28
- atlas setup --root <repo> what first setup would do, helper by helper
29
- atlas change --root <repo> --say "…" which helpers a change reaches
30
- atlas helpers the five helpers and what each owns
31
-
32
31
  atlas tooling write --root <repo> write both Atlas files into a repo
33
32
  atlas tooling pr --root <repo> the same, as one pull request
34
33
  --replace-unknown also replace a copy that is in no release
35
34
  --workflow rewrite only the workflow, for a repo whose check is current
36
35
  atlas status --root <repo> is the repo's check current, behind, unknown or missing
37
- atlas kind-drift --root <repo> --record-kind <kind>
36
+ atlas kind-drift --root <repo> --record-kind <kinds>
38
37
  does atlas.yaml agree with the repo record
39
38
  atlas check --root <repo> run the repo shape check here
40
39
 
41
40
  atlas code-read resolve … resolve the citations of a code digest
42
41
 
43
- atlas test onboarding --key <path> the six questions for the isolated session
44
- atlas test onboarding --key <path> --answers <file>
45
- score the answers; runs OUTSIDE that session
42
+ atlas ste <file or folder> … measure markdown and HTML against the STE limits
43
+ --share also run the privacy filter, quotes included
44
+ --terms <file> private terms; default ~/.config/atlas/private-terms.txt
46
45
 
47
46
  atlas install install Atlas for every folder on this Mac
48
47
  atlas upgrade [--version <v>] check, show and install a newer release
49
48
  atlas doctor is Atlas installed and working here
50
49
 
51
50
  Atlas ${ATLAS_VERSION}. Check version ${CHECK_VERSION}; it reads spec ${Object.keys(SPECS).join(', ')}.
52
- A person merges every file a helper writes.
51
+ A person merges every file the tooling writer writes.
53
52
  `;
54
53
 
55
54
  function options(argv) {
@@ -69,56 +68,6 @@ function options(argv) {
69
68
  const rootOf = (chosen) => resolve(chosen.root === undefined || chosen.root === true ? process.cwd() : chosen.root);
70
69
  const line = (text = '') => process.stdout.write(`${text}\n`);
71
70
 
72
- function showHelpers() {
73
- line('The five helpers. You never pick one; the entry verb routes what you say.');
74
- line('');
75
- for (const helper of HELPERS) {
76
- line(`${helper.file}. ${helper.title} (${helper.id})`);
77
- line(` owns ${helper.ownsText}`);
78
- line(` for ${helper.forWhat}`);
79
- line(` you say "${helper.youMightSay}"`);
80
- line('');
81
- }
82
- line(`Every helper pull request carries the label "${HELPER_LABEL}".`);
83
- line(`Every change in it names its source: ${PROVENANCE_SOURCES.join(', ')}.`);
84
- }
85
-
86
- function showRoute(chosen) {
87
- const say = chosen.say === true ? '' : String(chosen.say ?? '');
88
- const reached = route(say);
89
- line(`You said: ${say || '(nothing)'}`);
90
- line('');
91
- if (!reached.length) {
92
- line('No helper owns anything in that sentence.');
93
- line('Say which file class changed, or run "atlas helpers" to see the five.');
94
- return 1;
95
- }
96
- line(`It reaches ${reached.length} helper(s), in file order:`);
97
- for (const helper of reached) line(` ${helper.file}. ${helper.title} — ${helper.ownsText}`);
98
- line('');
99
- line('One request, one pull request. A person merges it.');
100
- return 0;
101
- }
102
-
103
- function showSetup(chosen) {
104
- const root = rootOf(chosen);
105
- line(`First setup for ${root}`);
106
- line('');
107
- line('It is the same verb you use later for one change. All five run, in file order.');
108
- line('');
109
- for (const helper of setupOrder()) {
110
- const owned = helper.id === 'tooling'
111
- ? writeTooling(root, PACKAGE_ROOT, { dryRun: true }).rows.map((row) => `${row.path} (${row.state})`).join(', ')
112
- : helper.ownsText;
113
- line(`${helper.file}. ${helper.title}`);
114
- line(` ${owned}`);
115
- }
116
- line('');
117
- line('The tooling helper writes its two files itself; the other four need an agent to draft.');
118
- line('Nothing lands without a person: every file above steers agents.');
119
- return 0;
120
- }
121
-
122
71
  function toolingCommand(chosen) {
123
72
  const root = rootOf(chosen);
124
73
  const what = chosen._[1] ?? 'write';
@@ -130,7 +79,7 @@ function toolingCommand(chosen) {
130
79
  const branch = chosen.branch === true || !chosen.branch ? `atlas/tooling-refresh-${Date.now().toString(36)}` : chosen.branch;
131
80
  const title = `Atlas: refresh the repo shape check to check version ${CHECK_VERSION}`;
132
81
  const why = `The Atlas package ${ATLAS_VERSION} writes both files. A person merges this, because the check steers every pull request.`;
133
- const result = open({ root, helperId: 'tooling', changes, why, branch, title, dryRun: Boolean(chosen['dry-run']) });
82
+ const result = open({ root, changes, why, branch, title, dryRun: Boolean(chosen['dry-run']) });
134
83
  line(result.opened ? `Opened ${result.url}` : 'Dry run. Nothing was pushed.');
135
84
  if (!result.opened) { line(''); line(result.body); }
136
85
  return 0;
@@ -199,6 +148,22 @@ function kindDriftCommand(chosen) {
199
148
  return report.drifting ? 1 : 0;
200
149
  }
201
150
 
151
+ function steCommand(chosen) {
152
+ const paths = chosen._.slice(1);
153
+ if (!paths.length) throw new Error('give a file or a folder: atlas ste <path> [--share]');
154
+ const share = Boolean(chosen.share);
155
+ const terms = share ? loadPrivateTerms({ path: typeof chosen.terms === 'string' ? resolve(chosen.terms) : null }) : [];
156
+ const results = checkPaths(paths.map((one) => resolve(one)), { share, terms });
157
+ let count = 0;
158
+ for (const { file, findings } of results) {
159
+ for (const finding of findings) { count += 1; line(`${file}:${finding.line} ${finding.rule} ${finding.why}`); }
160
+ }
161
+ line('');
162
+ line(`${results.length} page(s) read; ${count} finding(s). Limits: ${LIMITS.step} words a step, ${LIMITS.sentence} words a sentence, ${LIMITS.paragraph} sentences a paragraph.${share ? ` Privacy filter on, with ${terms.length} private term(s).` : ''}`);
163
+ line(count ? 'Fix the text by hand. This check never rewrites.' : 'GREEN');
164
+ return count ? 1 : 0;
165
+ }
166
+
202
167
  function checkCommand(chosen) {
203
168
  const root = rootOf(chosen);
204
169
  const report = scan({ root, repoId: chosen.repo === true ? null : chosen.repo ?? null });
@@ -208,41 +173,6 @@ function checkCommand(chosen) {
208
173
  return decision.green ? 0 : 1;
209
174
  }
210
175
 
211
- function onboardingCommand(chosen) {
212
- const key = loadKey(chosen.key);
213
- if (!chosen.answers || chosen.answers === true) {
214
- line(`The fresh-agent test for ${key.repo}. ${key.tasks.length} tasks, pass is ${key.pass} of ${key.tasks.length}.`);
215
- line('');
216
- line('Set the session up this way, and no other way:');
217
- for (const rule of ISOLATION) line(` - ${rule}`);
218
- line('');
219
- line('Give it these questions, and nothing else. Do not give it this file.');
220
- line('');
221
- for (const task of questions(key)) line(`${task.number}. ${task.ask}`);
222
- line('');
223
- line('Have it write one answer per task, keyed by task id:');
224
- for (const task of key.tasks) line(` ${task.id}: …`);
225
- line('');
226
- line('Then score it from a session that is not that one:');
227
- line(` atlas test onboarding --key ${chosen.key} --answers <file>`);
228
- return 0;
229
- }
230
- const result = score(key, readAnswers(resolve(chosen.answers)));
231
- line(`Fresh-agent test, ${result.repo}: ${result.passed} of ${result.of}.`);
232
- line('');
233
- for (const row of result.rows) {
234
- line(`${row.pass ? 'PASS' : 'FAIL'} ${row.number}. ${row.id}`);
235
- line(` ${row.why}`);
236
- line(` source: ${row.source}`);
237
- }
238
- line('');
239
- line(result.pass
240
- ? `${result.passed} of ${result.of}. The files answer on their own.`
241
- : `${result.passed} of ${result.of}. The files do not answer on their own yet.`);
242
- line('This scorer ran outside the isolated session, so the score is not a self-grade.');
243
- return result.pass ? 0 : 1;
244
- }
245
-
246
176
  export async function main(argv = process.argv.slice(2)) {
247
177
  const command = argv[0];
248
178
  if (command === 'code-read') {
@@ -256,16 +186,11 @@ export async function main(argv = process.argv.slice(2)) {
256
186
  case 'install': return install({ packageRoot: PACKAGE_ROOT, options: chosen, line });
257
187
  case 'upgrade': return upgrade({ packageRoot: PACKAGE_ROOT, options: chosen, line });
258
188
  case 'doctor': return doctor({ packageRoot: PACKAGE_ROOT, options: chosen, line });
259
- case 'helpers': showHelpers(); return 0;
260
- case 'setup': return showSetup(chosen);
261
- case 'change': return showRoute(chosen);
262
189
  case 'tooling': return toolingCommand(chosen);
263
190
  case 'status': return statusCommand(chosen);
264
191
  case 'kind-drift': return kindDriftCommand(chosen);
265
192
  case 'check': return checkCommand(chosen);
266
- case 'test':
267
- if (chosen._[1] !== 'onboarding') throw new Error('the only test is "onboarding"');
268
- return onboardingCommand(chosen);
193
+ case 'ste': return steCommand(chosen);
269
194
  default: throw new Error(`there is no verb "${command}". Run "atlas help".`);
270
195
  }
271
196
  }
@@ -19,8 +19,13 @@
19
19
  import { readFileSync, existsSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import { parseYaml, SPECS } from '../../shape/check.mjs';
22
+ import { REPO_KINDS, repoKinds } from '../../work/lib/verb-fields.mjs';
22
23
 
23
- export const KINDS = Object.freeze(['service', 'library', 'schema']);
24
+ // Stage 1: the repo record holds a list of kinds, and atlas.yaml one kind.
25
+ // They agree when the list holds the file's kind. This report retires in
26
+ // stage 2, with the fixed kind tables.
27
+ export const KINDS = REPO_KINDS;
28
+ const asList = (value) => (value === null || value === undefined ? null : (Array.isArray(value) ? value : String(value).split(',').map((one) => one.trim()).filter(Boolean)));
24
29
 
25
30
  // The kind a repository's own file declares, with the repo id beside it.
26
31
  export function fileKind(root) {
@@ -40,6 +45,8 @@ export function fileKind(root) {
40
45
  export function driftOne({ root, recordKind = null, recordRepo = null }) {
41
46
  const file = fileKind(root);
42
47
  const known = (kind) => kind === null || KINDS.includes(kind);
48
+ const recordKinds = asList(recordKind);
49
+ recordKind = recordKinds && recordKinds.length ? recordKinds.join(', ') : null;
43
50
  const row = {
44
51
  root,
45
52
  repo: file.repo ?? recordRepo ?? null,
@@ -52,21 +59,24 @@ export function driftOne({ root, recordKind = null, recordRepo = null }) {
52
59
  if (file.error) { row.state = 'unreadable'; row.why = file.error; return row; }
53
60
  if (!known(file.kind)) { row.state = 'unreadable'; row.why = `atlas.yaml names kind "${file.kind}"; a kind is one of ${KINDS.join(', ')}`; return row; }
54
61
  if (recordKind === null) { row.state = 'unknown'; row.why = 'no repo record kind was given, so there is nothing to compare with. Read it with the registry_list verb and pass it in.'; return row; }
55
- if (!known(recordKind)) { row.state = 'unreadable'; row.why = `the repo record names kind "${recordKind}"; a kind is one of ${KINDS.join(', ')}`; return row; }
62
+ const strange = recordKinds.find((kind) => !known(kind));
63
+ if (strange) { row.state = 'unreadable'; row.why = `the repo record names kind "${strange}"; a kind is one of ${KINDS.join(', ')}`; return row; }
56
64
  if (file.kind === null) { row.state = 'drift'; row.why = `the repo record says "${recordKind}" and atlas.yaml names no kind at all, so CI cannot tell which files this repo needs`; return row; }
57
- if (file.kind === recordKind) { row.state = 'agree'; row.why = `both say "${file.kind}"`; return row; }
65
+ if (recordKinds.includes(file.kind)) { row.state = 'agree'; row.why = recordKinds.length === 1 ? `both say "${file.kind}"` : `the repo record's kinds (${recordKind}) hold "${file.kind}"`; return row; }
58
66
  row.state = 'drift';
59
67
  row.why = `atlas.yaml says "${file.kind}" and the repo record says "${recordKind}". The kind decides which parts are required, so the check in CI and the work verbs are scoring two different repos. Open question O-9 says which one is the source.`;
60
68
  return row;
61
69
  }
62
70
 
63
- // The registry the `registry_list` verb returns, as { repoId: kind }.
71
+ // The registry the `registry_list` verb returns, as { repoId: kinds }. A
72
+ // row from before stage 1 has only kind, and reads as a list of one.
64
73
  export function kindsFromRegistry(registry) {
65
74
  const out = {};
75
+ const kindsOf = (repo) => { const list = repoKinds(repo); return list.length ? list : null; };
66
76
  for (const product of registry?.products ?? []) {
67
- for (const repo of product?.repos ?? []) if (repo?.registry_id) out[repo.registry_id] = repo.kind ?? null;
77
+ for (const repo of product?.repos ?? []) if (repo?.registry_id) out[repo.registry_id] = kindsOf(repo);
68
78
  }
69
- for (const repo of registry?.repos ?? []) if (repo?.registry_id) out[repo.registry_id] = repo.kind ?? null;
79
+ for (const repo of registry?.repos ?? []) if (repo?.registry_id) out[repo.registry_id] = kindsOf(repo);
70
80
  return out;
71
81
  }
72
82
 
@@ -0,0 +1,61 @@
1
+ // The privacy filter. It finds what must not leave with a page or an issue
2
+ // that goes to someone else: a secret, an email address, a home-folder path,
3
+ // an id, or one of the owner's private terms.
4
+ //
5
+ // The secret patterns have one home, the repo shape check, because that file
6
+ // is copied into other repos and may import only Node builtins. This module
7
+ // reads them from there and adds the rest. The release gate imports this
8
+ // module, so a shipped file and a shared page are held to the same patterns.
9
+ //
10
+ // The package is public, so it ships generic patterns only. The owner's own
11
+ // names sit in ~/.config/atlas/private-terms.txt, outside every repo, one
12
+ // term on each line. A line that starts with # is a note.
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { SECRET_PATTERNS, secretPatterns } from '../../shape/check.mjs';
17
+
18
+ export const PRIVATE_TERMS_PATH = join('.config', 'atlas', 'private-terms.txt');
19
+
20
+ // Each pattern is written so that its own source line matches no other
21
+ // pattern here, because this file ships and the release gate reads it.
22
+ export const PRIVACY_PATTERNS = Object.freeze([
23
+ { id: 'email-address', holds: 'An email address.', pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}\b/ },
24
+ { id: 'home-folder-path', holds: 'A path into one person\'s home folder.', pattern: /(?:[\\/](?:Users|home)[\\/][A-Za-z0-9._-]+[\\/])|(?:\b[A-Za-z]:\\Users\\[A-Za-z0-9._-]+)/ },
25
+ { id: 'uuid', holds: 'A UUID, the shape of a record, account or session id.', pattern: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i },
26
+ ]);
27
+
28
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+
30
+ // The owner's private terms, or an empty list when the file is not there.
31
+ export function loadPrivateTerms({ path = null, home = homedir() } = {}) {
32
+ const file = path ?? join(home, PRIVATE_TERMS_PATH);
33
+ if (!existsSync(file)) return [];
34
+ return readFileSync(file, 'utf8').split('\n').map((line) => line.trim()).filter((line) => line && !line.startsWith('#'));
35
+ }
36
+
37
+ // Every rule this filter applies: the secret patterns, the generic patterns,
38
+ // then one rule for each private term.
39
+ export function privacyRules({ terms = [] } = {}) {
40
+ const secrets = secretPatterns().map((pattern, index) => ({ id: `secret-${SECRET_PATTERNS[index].id}`, holds: SECRET_PATTERNS[index].holds, pattern }));
41
+ const own = terms.map((term) => ({ id: 'private-term', holds: 'One of the owner\'s private terms.', pattern: new RegExp(`(?<![A-Za-z0-9])${escape(term)}(?![A-Za-z0-9])`, 'i') }));
42
+ return [...secrets, ...PRIVACY_PATTERNS, ...own];
43
+ }
44
+
45
+ // Each finding names its line and its rule, never the text it matched, so a
46
+ // report of a leak does not leak it again.
47
+ export function privacyFindings(text, { terms = [], rules = privacyRules({ terms }) } = {}) {
48
+ const findings = [];
49
+ text.split('\n').forEach((line, index) => {
50
+ for (const rule of rules) if (rule.pattern.test(line)) findings.push({ line: index + 1, rule: rule.id, why: rule.holds });
51
+ });
52
+ return findings;
53
+ }
54
+
55
+ // The same text with each match replaced by a marker. The verifier runs its
56
+ // logs through this before it posts them.
57
+ export function redact(text, { terms = [], rules = privacyRules({ terms }) } = {}) {
58
+ let out = text;
59
+ for (const rule of rules) out = out.replace(new RegExp(rule.pattern.source, rule.pattern.flags.includes('g') ? rule.pattern.flags : `${rule.pattern.flags}g`), `[hidden: ${rule.id}]`);
60
+ return out;
61
+ }