@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +15 -9
- package/agents/artifact-renderer.md +7 -2
- package/agents/capability-reader.md +1 -1
- package/agents/code-explorer.md +3 -2
- package/agents/reviewer-architect.md +3 -3
- package/agents/reviewer-pm.md +4 -4
- package/agents/reviewer-security.md +11 -8
- package/agents/verifier.md +97 -0
- package/door/cli.mjs +33 -108
- package/door/lib/kind-drift.mjs +16 -6
- package/door/lib/privacy.mjs +61 -0
- package/door/lib/pull-request.mjs +55 -56
- package/door/lib/ste-words.json +51 -0
- package/door/lib/ste.mjs +199 -0
- package/door/lib/tooling.mjs +2 -2
- package/package.json +1 -1
- package/shape/check.mjs +33 -33
- package/shape/releases.json +5 -0
- package/skills/feedback/SKILL.md +39 -0
- package/skills/lead/SKILL.md +103 -0
- package/skills/learnings/SKILL.md +48 -0
- package/skills/repo-skills/SKILL.md +64 -0
- package/skills/sdlc-task/SKILL.md +62 -69
- package/skills/sdlc-task/lifecycle.yaml +8 -4
- package/skills/sdlc-task/templates/design-doc.md +23 -18
- package/skills/sdlc-task/templates/resume-anchor.md +9 -7
- package/work/lib/capability.mjs +34 -23
- package/work/lib/delivery.mjs +26 -12
- package/work/lib/questions.mjs +54 -10
- package/work/lib/verb-fields.mjs +34 -0
- package/work/lib/verbs.mjs +100 -19
- package/work/{manifest-0.5.0.json → manifest-0.6.0.json} +9 -3
- package/work/mcp.mjs +13 -10
- package/work/protected-paths.json +4 -2
- package/door/lib/helpers.mjs +0 -139
- package/door/lib/onboarding.mjs +0 -130
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
|
|
6
|
-
|
|
7
|
-
2. **
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
install or upgrade
|
|
13
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
package/agents/code-explorer.md
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
-
(
|
|
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 (
|
|
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
|
|
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
|
|
package/agents/reviewer-pm.md
CHANGED
|
@@ -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
|
-
(
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
(
|
|
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 (
|
|
9
|
-
|
|
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 (
|
|
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
|
|
44
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
5
|
-
//
|
|
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
|
|
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 {
|
|
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 <
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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,
|
|
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 '
|
|
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
|
}
|
package/door/lib/kind-drift.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
+
}
|