@arjunkhera/atlas 0.0.0 → 0.2.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 +16 -0
- package/.mcp.json +18 -0
- package/README.md +32 -1
- package/agents/artifact-format/example.html +71 -0
- package/agents/artifact-format/system.css +273 -0
- package/agents/artifact-renderer.md +132 -0
- package/agents/capability-reader.md +100 -0
- package/agents/code-explorer.md +108 -0
- package/agents/reviewer-architect.md +78 -0
- package/agents/reviewer-pm.md +81 -0
- package/agents/reviewer-security.md +82 -0
- package/bin/atlas +5 -0
- package/door/cli.mjs +280 -0
- package/door/lib/helpers.mjs +139 -0
- package/door/lib/install.mjs +260 -0
- package/door/lib/kind-drift.mjs +76 -0
- package/door/lib/onboarding.mjs +130 -0
- package/door/lib/pull-request.mjs +115 -0
- package/door/lib/releases.mjs +42 -0
- package/door/lib/status.mjs +47 -0
- package/door/lib/tooling.mjs +119 -0
- package/package.json +24 -3
- package/shape/check.mjs +1797 -0
- package/shape/releases.json +15 -0
- package/skills/sdlc-task/SKILL.md +174 -0
- package/skills/sdlc-task/lifecycle.yaml +27 -0
- package/skills/sdlc-task/templates/design-doc.md +118 -0
- package/skills/sdlc-task/templates/resume-anchor.md +25 -0
- package/work/code-read.mjs +65 -0
- package/work/lib/action-store.mjs +84 -0
- package/work/lib/capability.mjs +487 -0
- package/work/lib/child-env.mjs +11 -0
- package/work/lib/client.mjs +214 -0
- package/work/lib/code-read.mjs +238 -0
- package/work/lib/delivery.mjs +624 -0
- package/work/lib/mcp-stdio.mjs +68 -0
- package/work/lib/questions.mjs +278 -0
- package/work/lib/sweep.mjs +189 -0
- package/work/lib/verb-fields.mjs +53 -0
- package/work/lib/verbs.mjs +511 -0
- package/work/manifest-0.5.0.json +609 -0
- package/work/mcp.mjs +216 -0
- package/work/protected-paths.json +38 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Every copy of shape/check.mjs a release of Atlas has shipped. The last row is the check this package ships. atlas status reads this list; git history is never read.",
|
|
3
|
+
"releases": [
|
|
4
|
+
{
|
|
5
|
+
"check_version": 1,
|
|
6
|
+
"sha256": "3dfd659a7533c51dce11bd0e2d7a9312f041c5955d45ac3cc67d3e29d328b5d2",
|
|
7
|
+
"first_package": "0.1.1"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"check_version": 2,
|
|
11
|
+
"sha256": "a4bc5ab8c75be8f216d460df9233670f5ba4ed9d853236f6b55a7f0d10dfe293",
|
|
12
|
+
"first_package": "0.2.0"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sdlc-task
|
|
3
|
+
description: >
|
|
4
|
+
The Atlas delivery lifecycle in a session — start / pause / resume / status / lock / done.
|
|
5
|
+
Trigger whenever the owner starts new software work in chat ("let's build X", "new
|
|
6
|
+
feature", "start a task"), pauses or resumes work ("park this", "pick up where we left
|
|
7
|
+
off"), asks where a task stands, or is ready to lock a design ("lock it down", "we're done
|
|
8
|
+
specifying"). Every write goes through the atlas-work verbs; this skill says when to call
|
|
9
|
+
which one.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# sdlc-task — the delivery lifecycle
|
|
13
|
+
|
|
14
|
+
Atlas is the package that sets how a repo is worked on. This skill is its
|
|
15
|
+
delivery lifecycle. It travels with the package, not with any one repo.
|
|
16
|
+
|
|
17
|
+
The kernel is data, not prose. Read [`lifecycle.yaml`](lifecycle.yaml), beside
|
|
18
|
+
this file, for the stages, tiers, tripwires and the lock contract before you
|
|
19
|
+
act. A verb reads those lists; it never hard-codes them.
|
|
20
|
+
|
|
21
|
+
## Where the state lives
|
|
22
|
+
|
|
23
|
+
Every work item, question, decision and delivery mark is a record in the graph,
|
|
24
|
+
written through the **atlas-work verbs** (the `atlas-work` MCP server). Do not
|
|
25
|
+
write raw graph records for work. The verbs hold the write rules, the owner
|
|
26
|
+
checks and the transition log; each verb's own description says what it needs.
|
|
27
|
+
|
|
28
|
+
| You want to | Verb |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Record new work, with the owner's words | `item_propose`, then `item_stage` as it moves |
|
|
31
|
+
| Freeze the design | `item_lock` |
|
|
32
|
+
| Park it, or pick it up again | `item_park`, `item_resume` |
|
|
33
|
+
| Split it, or link it to other work | `item_split`, `item_link` |
|
|
34
|
+
| Ask the owner something, or record the answer | `question_ask`, `question_answer` |
|
|
35
|
+
| Record a decision in the owner's words | `decision_record` |
|
|
36
|
+
| Mark it delivered, or reopen it | `item_done`, `item_reopen` |
|
|
37
|
+
| Say where things stand | `where_are_we`, `in_flight`, `needs_me` |
|
|
38
|
+
|
|
39
|
+
The repo's own facts come from `atlas.yaml` and `atlas/`. Its `kind` (`service`,
|
|
40
|
+
`library`, `schema`) decides which procedures it has and what "delivered" means.
|
|
41
|
+
Read it. Never assume a deployed service.
|
|
42
|
+
|
|
43
|
+
A change to the process itself is not a task. It is an amendment to the
|
|
44
|
+
frozen text, and the rule for one is short:
|
|
45
|
+
|
|
46
|
+
1. Say so at once. Never build against a sentence you know is wrong.
|
|
47
|
+
2. Write the amendment as its own dated file: what changes, why, the new
|
|
48
|
+
frozen text in full, one new hash over the whole frozen section, and the old
|
|
49
|
+
hash named as retired.
|
|
50
|
+
3. Never edit frozen text in place. A reviewer's finding is a normal reason to
|
|
51
|
+
amend.
|
|
52
|
+
|
|
53
|
+
(Source: the Atlas lock procedure, amendment A1.6, section 12.)
|
|
54
|
+
|
|
55
|
+
## start
|
|
56
|
+
|
|
57
|
+
1. **Tier** (`lifecycle.yaml` `tiers`): propose one — hotfix, standard or
|
|
58
|
+
initiative — with a one-line reason. The owner can override. The forced
|
|
59
|
+
escalators are `lifecycle.yaml` `tier_escalators`.
|
|
60
|
+
2. **Record the work at once** with `item_propose`: the owner's goal
|
|
61
|
+
**verbatim**, and one line of interpretation that is clearly yours. Move its
|
|
62
|
+
stage with `item_stage` as the design conversation opens.
|
|
63
|
+
3. **Worktree and branch.** Read the repo's `default_branch` from its record
|
|
64
|
+
(`registry_list`); it may be `main` or `master`. Branch from it, and never
|
|
65
|
+
commit to it. Local sessions use `feature/`, `fix/` or `chore/`; cloud
|
|
66
|
+
sessions use `claude/`. The pull request targets that branch.
|
|
67
|
+
4. **Design doc** (standard and above): copy
|
|
68
|
+
[`templates/design-doc.md`](templates/design-doc.md) to
|
|
69
|
+
`docs/design-docs/<slug>.md`, fill Context and goal, and list it in
|
|
70
|
+
`docs/index.md` in the commit that first lands it. Hotfix tier skips the
|
|
71
|
+
doc: the acceptance criteria live on the work item, and a resume anchor
|
|
72
|
+
covers pauses.
|
|
73
|
+
5. Enter the design loop.
|
|
74
|
+
|
|
75
|
+
## The design loop (between start and lock)
|
|
76
|
+
|
|
77
|
+
- **One voice.** The owner talks to one colleague. Crews run behind the
|
|
78
|
+
curtain as subagents and come back as digests — never as a committee
|
|
79
|
+
surfacing question lists.
|
|
80
|
+
- **Guided elicitation** (the owner's words: *"one on one
|
|
81
|
+
conversation that flows… the purpose of agent is to do research and guide my
|
|
82
|
+
thoughts into useful features"*). Research first, then **one pointed question
|
|
83
|
+
at a time**, with a recommendation (the owner: *"Questions if posed
|
|
84
|
+
should be one by one. Just think, if you say 5 things in one go, how do I
|
|
85
|
+
respond."*). Keep the question queue in the design doc; surface only the
|
|
86
|
+
next one. Record each question with `question_ask` and each answer with
|
|
87
|
+
`question_answer`.
|
|
88
|
+
- **Succinct chat; artifacts carry the content** (the owner's words:
|
|
89
|
+
*"the content is very verbose… I won't read that much"* · *"artefacts should
|
|
90
|
+
be the default method of communication… use as many diagrams and examples as
|
|
91
|
+
possible"*). Chat is a few short sentences: the outcome, the artifact link,
|
|
92
|
+
the one question.
|
|
93
|
+
- **Lead with a worked example.** Real input, real process, real output, and a
|
|
94
|
+
sequence diagram where order matters. The owner cannot read dense prose.
|
|
95
|
+
- **Read the reason, not only the rule.** Before you build to a done line, read
|
|
96
|
+
the owner's words behind it and ask whether the thing should exist at all. A
|
|
97
|
+
done line is a measure, not the goal.
|
|
98
|
+
- **Research before opinions.** Fan out `atlas:code-explorer` for how the system
|
|
99
|
+
works today. Raw exploration never enters this session — digests only.
|
|
100
|
+
- **When the task is a cluster of filed bugs, first ask whether the model
|
|
101
|
+
they presume was ever decided.** A bug report says the code surprised
|
|
102
|
+
someone; it does not say what the code should do.
|
|
103
|
+
- **The doc accretes in real time.** Every resolved question, decision and
|
|
104
|
+
approach lands in the design doc as it happens. The doc is the pause and
|
|
105
|
+
resume state. Record each decision with `decision_record`.
|
|
106
|
+
- **HTML is the owner's reading surface.** Any design, review or status the
|
|
107
|
+
owner reads is a styled artifact, rendered by the `atlas:artifact-renderer`
|
|
108
|
+
crew. Its treatment contract lives in that crew; do not restate it.
|
|
109
|
+
- **No bare acronyms in front of the owner.** Spell a thing out on first
|
|
110
|
+
mention. Ids are for the machine; names are for the human.
|
|
111
|
+
- **User stories before acceptance criteria** (the owner's words:
|
|
112
|
+
*"I need to see how a user would use the product, or like how the flow
|
|
113
|
+
works"*). Never draft criteria for a flow no story walks through.
|
|
114
|
+
- **Adversarial review before lock.** Standard tier: at least one persona.
|
|
115
|
+
Initiative: the panel — `atlas:reviewer-pm`, `atlas:reviewer-architect`,
|
|
116
|
+
`atlas:reviewer-security` — each briefed with the doc path, in parallel. Brief each
|
|
117
|
+
one to ask whether the design is the right thing, not only whether it is
|
|
118
|
+
consistent. An unresolved finding goes into the doc's open questions, and
|
|
119
|
+
that blocks the lock.
|
|
120
|
+
|
|
121
|
+
## lock
|
|
122
|
+
|
|
123
|
+
Gate on `lifecycle.yaml` `lock.preconditions`, all four: the open questions
|
|
124
|
+
are empty, the owner ratified the criteria, the approach is chosen, the tier
|
|
125
|
+
is confirmed. Then:
|
|
126
|
+
|
|
127
|
+
1. Stamp the design doc's lock block: date, ratifier, tier, the sha256 of the
|
|
128
|
+
acceptance criteria text, frozen interfaces, the work list. Commit.
|
|
129
|
+
2. Call `item_lock` with the frozen scope and its hash.
|
|
130
|
+
3. Build against the frozen text. **Tripwires** (`lifecycle.yaml`
|
|
131
|
+
`tripwires`): a discovery after lock that touches the criteria, security,
|
|
132
|
+
a schema or cost stops the build and goes back to the owner. If the frozen
|
|
133
|
+
text is wrong, say so at once and write an amendment. Never build against a
|
|
134
|
+
sentence you know is wrong.
|
|
135
|
+
4. Test with the repo's own `test` procedure, which `atlas.yaml` names. Open a
|
|
136
|
+
pull request. Keep a separate acceptance reviewer, and close its findings
|
|
137
|
+
before acceptance.
|
|
138
|
+
|
|
139
|
+
## pause
|
|
140
|
+
|
|
141
|
+
1. Bring the resume state current: the design doc (standard and above), or a
|
|
142
|
+
resume anchor from [`templates/resume-anchor.md`](templates/resume-anchor.md)
|
|
143
|
+
(hotfix). The bar: a fresh session needs nothing else.
|
|
144
|
+
2. Call `item_park` with the reason. A parked item keeps its stage.
|
|
145
|
+
|
|
146
|
+
## resume
|
|
147
|
+
|
|
148
|
+
1. Find it with `where_are_we` or `in_flight`, then read its design doc or its
|
|
149
|
+
newest resume anchor.
|
|
150
|
+
2. **Check the world still matches.** Does the branch exist? Is the blocker
|
|
151
|
+
closed? Are the criteria still true? Write any change into the doc first.
|
|
152
|
+
3. Call `item_resume` and continue at the recorded stage.
|
|
153
|
+
|
|
154
|
+
## status
|
|
155
|
+
|
|
156
|
+
Call `where_are_we` for the item, or `needs_me` for what waits on the owner.
|
|
157
|
+
Report lifecycle, stage, open questions, what is blocked on the owner, and the
|
|
158
|
+
next action. One short paragraph.
|
|
159
|
+
|
|
160
|
+
## done
|
|
161
|
+
|
|
162
|
+
1. Prove delivery the way the repo's `kind` requires, then call `item_done`. A
|
|
163
|
+
`library` or `schema` repo proves it by merged pull requests and a green
|
|
164
|
+
gate. A `service` repo also needs its live checks.
|
|
165
|
+
2. If anything was a struggle, encode the fix where it stops a repeat — a lint,
|
|
166
|
+
then a tool, then a skill, then a doc — and say which.
|
|
167
|
+
3. Design doc status: `SHIPPED <date>`.
|
|
168
|
+
|
|
169
|
+
## Model tiering
|
|
170
|
+
|
|
171
|
+
The design loop and the lock judgement run on the frontier model. Crews state
|
|
172
|
+
their own tier in their own files, which ship with Atlas: `atlas:code-explorer`
|
|
173
|
+
is Sonnet; the reviewer personas inherit the frontier model. Drafting from a
|
|
174
|
+
locked spec goes to cheaper models through subagents.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# The delivery lifecycle kernel, as data.
|
|
2
|
+
#
|
|
3
|
+
# Atlas is the package that sets how a repo is worked on, and this is its
|
|
4
|
+
# lifecycle. It travels with the package, beside the skill that reads it, not
|
|
5
|
+
# with any one repo. A verb reads these lists; it never hard-codes them.
|
|
6
|
+
#
|
|
7
|
+
# The values came from the old ops pack, unchanged. That pack and every script
|
|
8
|
+
# that read it are deleted.
|
|
9
|
+
version: 1
|
|
10
|
+
authority: the Atlas lock procedure, amendment A1.9, section 5, slice L2 stage one.
|
|
11
|
+
|
|
12
|
+
lifecycle_states: [proposed, active, paused, done, dropped, archived]
|
|
13
|
+
stages: [capture, spec, build, verify, ship, learn]
|
|
14
|
+
|
|
15
|
+
lock:
|
|
16
|
+
freezes: [acceptance_criteria, interfaces, approach]
|
|
17
|
+
preconditions: [open_questions_empty, ac_ratified, approach_chosen, tier_confirmed]
|
|
18
|
+
|
|
19
|
+
tripwires: [acceptance_criteria, security, schema_migrations, money_cost]
|
|
20
|
+
|
|
21
|
+
tiers: [hotfix, standard, initiative]
|
|
22
|
+
tier_escalators: [migrations, auth_rls_surface, new_external_dependency, money_cost]
|
|
23
|
+
|
|
24
|
+
# Both templates travel with the package, beside the kernel.
|
|
25
|
+
templates:
|
|
26
|
+
design_doc: templates/design-doc.md
|
|
27
|
+
resume_anchor: templates/resume-anchor.md
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<!-- Living design document — template (lifecycle spec §7, LC-5).
|
|
2
|
+
Copy to docs/design-docs/<slug>.md at `sdlc-task start` (standard/initiative
|
|
3
|
+
tiers). Sections may be trimmed at standard tier; never at initiative.
|
|
4
|
+
The artifact projection republishes on change (D14: git is truth).
|
|
5
|
+
Structure rules that keep the doc easy to read by machine:
|
|
6
|
+
sections addressable by heading, decisions/questions as list items with
|
|
7
|
+
stable ids (D-n / OQ-n / RQ-n), no prose that depends on rendering.
|
|
8
|
+
Reference rule (the owner's directive): ids are for the machine,
|
|
9
|
+
names are for the human — every cross-reference to a stable id links to
|
|
10
|
+
its definition (markdown section link here; anchor + tooltip in the HTML
|
|
11
|
+
artifact, see the atlas:artifact-renderer crew) and spells out what
|
|
12
|
+
it is on first mention. Never leave a bare acronym for the reader to
|
|
13
|
+
decode. -->
|
|
14
|
+
|
|
15
|
+
# <Feature name> — design
|
|
16
|
+
|
|
17
|
+
> Status: DRAFT (in design loop) | LOCKED <date> | SHIPPED <date>
|
|
18
|
+
> Work item: <work item id> · Tier: hotfix | standard | initiative
|
|
19
|
+
> Artifact: <claude.ai artifact url once published>
|
|
20
|
+
|
|
21
|
+
## Context & goal
|
|
22
|
+
|
|
23
|
+
<One paragraph: the problem in the owner's words (verbatim where possible),
|
|
24
|
+
and the one-line goal. Agent interpretation kept separate.>
|
|
25
|
+
|
|
26
|
+
## Goals & non-goals
|
|
27
|
+
|
|
28
|
+
<!-- From the owner's exemplar PRD shape. Bullets, testable
|
|
29
|
+
in spirit: what this change is FOR, and what it deliberately is not.
|
|
30
|
+
Success metrics where they exist — how we'd know it worked. -->
|
|
31
|
+
|
|
32
|
+
- Goals: …
|
|
33
|
+
- Non-goals: …
|
|
34
|
+
- Success metrics: …
|
|
35
|
+
|
|
36
|
+
## Flows
|
|
37
|
+
|
|
38
|
+
<The user-visible flows this touches, before → after. Sequence diagrams
|
|
39
|
+
(mermaid) where they earn their keep.>
|
|
40
|
+
|
|
41
|
+
## User stories
|
|
42
|
+
|
|
43
|
+
<!-- Mandatory at standard/initiative tier (the owner's words: "I need
|
|
44
|
+
to see how a user would use the product, or like how the flow works").
|
|
45
|
+
Concrete scenes with stable ids (US-n): who, the moment, what they do,
|
|
46
|
+
what the system does, what they see — written as narrative the owner can
|
|
47
|
+
react to, each tagged to the slice that ships it. Illustrative dialogue
|
|
48
|
+
is fine; mark invented values as illustrative. Draft these BEFORE
|
|
49
|
+
acceptance criteria — the AC formalize what the stories show. -->
|
|
50
|
+
|
|
51
|
+
- **US-1 — <scene name>** *(slice)*: …
|
|
52
|
+
|
|
53
|
+
## Architecture
|
|
54
|
+
|
|
55
|
+
<High-level shape: components touched, data model changes, boundaries crossed.
|
|
56
|
+
Initiative tier: current-state diagram + target-state diagram.>
|
|
57
|
+
|
|
58
|
+
## Approaches considered
|
|
59
|
+
|
|
60
|
+
<A/B with real depth at initiative tier — costs, risks, why the loser lost.
|
|
61
|
+
Standard tier: may collapse to "approach + rejected alternative, one line".>
|
|
62
|
+
|
|
63
|
+
- **Approach A — <name>**: …
|
|
64
|
+
- **Approach B — <name>**: …
|
|
65
|
+
- **Chosen**: <A/B> because <reason>.
|
|
66
|
+
|
|
67
|
+
## Open questions
|
|
68
|
+
|
|
69
|
+
<!-- The lock precondition: this checklist must be EMPTY (all items moved to
|
|
70
|
+
Resolved) before `sdlc-task lock` will stamp the lock block. -->
|
|
71
|
+
|
|
72
|
+
- [ ] OQ-1 —
|
|
73
|
+
|
|
74
|
+
## Resolved questions
|
|
75
|
+
|
|
76
|
+
- RQ-1 — <question> → <resolution, owner-verbatim where it was an owner call> (<date>)
|
|
77
|
+
|
|
78
|
+
## Decision log
|
|
79
|
+
|
|
80
|
+
- D-1 — <decision> — <rationale> (<date>, <ratifier>)
|
|
81
|
+
|
|
82
|
+
## Risks & failure modes
|
|
83
|
+
|
|
84
|
+
<!-- What breaks, degrades, or bites later; per-risk mitigation or "accepted".
|
|
85
|
+
From the exemplar PRD shape; trimmable at standard tier. -->
|
|
86
|
+
|
|
87
|
+
## Test & eval plan
|
|
88
|
+
|
|
89
|
+
<!-- How each slice proves itself: suites, scenarios, eval questions, prod
|
|
90
|
+
verification. The red spec's table of contents. Trimmable at standard. -->
|
|
91
|
+
|
|
92
|
+
## Acceptance criteria
|
|
93
|
+
|
|
94
|
+
<!-- These freeze at lock (LC-6). Write them testable — the red spec derives
|
|
95
|
+
from this list. -->
|
|
96
|
+
|
|
97
|
+
1. …
|
|
98
|
+
|
|
99
|
+
## Lock block
|
|
100
|
+
|
|
101
|
+
<!-- Stamped by `sdlc-task lock`; never hand-edited afterwards. -->
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
LOCKED: <date> by <ratifier>
|
|
105
|
+
Tier: <tier>
|
|
106
|
+
Frozen: AC (sha256 of the AC section), interfaces: <list>, approach: <chosen>
|
|
107
|
+
Tripwires in force: AC / security / schema / cost (kernel defaults)
|
|
108
|
+
Emitted: <child containers / issues created at lock>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Deviation log (post-lock)
|
|
112
|
+
|
|
113
|
+
<!-- Every post-lock divergence from this document. Review checks this list.
|
|
114
|
+
A deviation touching a tripwire (AC, security, schema, cost) must halt
|
|
115
|
+
and return to the owner instead of landing here. -->
|
|
116
|
+
|
|
117
|
+
| # | What changed vs the doc | Why | Tripwire? | Reviewed |
|
|
118
|
+
|---|---|---|---|---|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!-- Resume anchor — template (lifecycle spec §8).
|
|
2
|
+
For work BELOW the design-doc threshold: hotfix tier, bug investigations,
|
|
3
|
+
any interrupted queue work. Posted as a comment on the issue (queue lane)
|
|
4
|
+
or the container thread (conversation lane) at every pause AND at stage
|
|
5
|
+
transitions of long-running work.
|
|
6
|
+
The bar (and the eval): a fresh session given only this anchor proceeds
|
|
7
|
+
without asking the owner anything the anchor should have contained. -->
|
|
8
|
+
|
|
9
|
+
## ⏸ Resume anchor — <date>
|
|
10
|
+
|
|
11
|
+
**Goal:** <one line, owner-verbatim where possible>
|
|
12
|
+
|
|
13
|
+
**State:** lifecycle `<active|paused>` · stage `<capture|spec|build|verify|ship|learn>`
|
|
14
|
+
|
|
15
|
+
**Findings so far:** <POINTER to where they live — issue comment link, doc
|
|
16
|
+
path, graph entity id. Never duplicate content inline.>
|
|
17
|
+
|
|
18
|
+
**Open questions** (empty = resumable without the owner):
|
|
19
|
+
|
|
20
|
+
- [ ] …
|
|
21
|
+
|
|
22
|
+
**Next action:** <the single highest-leverage sentence for the resuming
|
|
23
|
+
session — imperative, specific, no context required beyond this anchor>
|
|
24
|
+
|
|
25
|
+
**Refs:** branch `<name>` · worktree `<path, if still live>` · PR/issue links
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Atlas M1 slice L4: resolve the citations of a code read.
|
|
3
|
+
// The M1 acceptance criterion says "a reviewer resolves every citation". This
|
|
4
|
+
// is how: each path and line is opened in the commit the digest names, read
|
|
5
|
+
// with `git show`, and checked. Nothing is written, no branch moves, and no
|
|
6
|
+
// working tree is touched, so the resolve is safe on a repo another session is
|
|
7
|
+
// using. A never-read path is never opened. A never-quoted path gives back the
|
|
8
|
+
// digest of its line, not its text.
|
|
9
|
+
//
|
|
10
|
+
// atlas code-read resolve --digest <file.json> --repo <owner/repo> \
|
|
11
|
+
// --root <path to that repo> [--output report.json]
|
|
12
|
+
//
|
|
13
|
+
// The digest file may hold one digest, an array of digests, or the arguments
|
|
14
|
+
// of a where_are_we call ({ code: [...] }).
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { childEnvironment } from './lib/child-env.mjs';
|
|
17
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { resolve as resolvePath } from 'node:path';
|
|
19
|
+
import { acceptCodeRead, resolveCitations } from './lib/code-read.mjs';
|
|
20
|
+
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
const command = args[0];
|
|
23
|
+
const option = (name) => { const index = args.indexOf(`--${name}`); return index === -1 ? undefined : args[index + 1]; };
|
|
24
|
+
const usage = 'usage: atlas code-read resolve --digest <file.json> --root <repo path> [--repo <owner/repo>] [--output <report.json>]';
|
|
25
|
+
if (command !== 'resolve') { process.stderr.write(`${usage}\n`); process.exit(1); }
|
|
26
|
+
|
|
27
|
+
const digestPath = option('digest'), rootOption = option('root');
|
|
28
|
+
if (!digestPath || !rootOption) { process.stderr.write(`${usage}\n`); process.exit(1); }
|
|
29
|
+
const roots = new Map();
|
|
30
|
+
// --root may be given once, or once per repo as "owner/repo=/path".
|
|
31
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
32
|
+
if (args[index] !== '--root') continue;
|
|
33
|
+
const value = args[index + 1] ?? '';
|
|
34
|
+
const split = value.indexOf('=');
|
|
35
|
+
if (split > 0 && value.slice(0, split).includes('/') && value.slice(split + 1).startsWith('/')) roots.set(value.slice(0, split), value.slice(split + 1));
|
|
36
|
+
else roots.set('*', resolvePath(value));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const raw = JSON.parse(await readFile(resolvePath(digestPath), 'utf8'));
|
|
40
|
+
const list = Array.isArray(raw) ? raw : (Array.isArray(raw?.code) ? raw.code : [raw]);
|
|
41
|
+
const only = option('repo');
|
|
42
|
+
const digests = list.map((entry, index) => acceptCodeRead(entry, { name: `code[${index}]` }).digest).filter((digest) => !only || digest.repo === only);
|
|
43
|
+
if (!digests.length) { process.stderr.write('no digest to resolve\n'); process.exit(1); }
|
|
44
|
+
|
|
45
|
+
const reports = [];
|
|
46
|
+
for (const digest of digests) {
|
|
47
|
+
const root = roots.get(digest.repo) ?? roots.get('*');
|
|
48
|
+
if (!root) { process.stderr.write(`no --root for ${digest.repo}\n`); process.exit(1); }
|
|
49
|
+
// git show reads the commit, not the working tree. The checkout may sit on
|
|
50
|
+
// any branch with any uncommitted change; the answer is the same.
|
|
51
|
+
const readFileAt = async (commit, path) => execFileSync('git', ['-C', root, 'show', `${commit}:${path}`], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024, env: childEnvironment() });
|
|
52
|
+
const report = await resolveCitations({ digest, readFileAt });
|
|
53
|
+
reports.push({ ...report, root });
|
|
54
|
+
process.stderr.write(`${digest.repo}@${digest.commit.slice(0, 7)}: ${report.resolved}/${report.citations} citations resolved${report.failed ? `, ${report.failed} failed` : ''}${report.quote_mismatches ? `, ${report.quote_mismatches} quotes do not match` : ''}\n`);
|
|
55
|
+
for (const row of report.rows.filter((entry) => !entry.resolved || entry.quote_matches === false)) process.stderr.write(` ${row.path}:${row.line} — ${row.reason ?? 'the quote is not on that line'}\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const pass = reports.every((report) => report.pass);
|
|
59
|
+
const output = option('output');
|
|
60
|
+
if (output) {
|
|
61
|
+
await writeFile(resolvePath(output), `${JSON.stringify({ resolved_at: new Date().toISOString(), pass, reports }, null, 2)}\n`);
|
|
62
|
+
process.stderr.write(`wrote ${output}\n`);
|
|
63
|
+
}
|
|
64
|
+
process.stderr.write(pass ? 'every citation resolved\n' : 'some citations did not resolve\n');
|
|
65
|
+
process.exitCode = pass ? 0 : 1;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { mkdir, open, lstat, rename, rm, readdir } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join } from 'node:path';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
// Local write-ahead records survive process exit. A stale lock is deliberately
|
|
7
|
+
// held for operator reconciliation; a lost process must not start a second writer.
|
|
8
|
+
export function createActionStore({ directory } = {}) {
|
|
9
|
+
if (typeof directory !== 'string' || !isAbsolute(directory)) throw new TypeError('absolute journal directory is required');
|
|
10
|
+
const key = (id) => createHash('sha256').update(id).digest('hex');
|
|
11
|
+
const path = (id) => join(directory, `${key(id)}.json`);
|
|
12
|
+
async function init() {
|
|
13
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
14
|
+
const stat = await lstat(directory);
|
|
15
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('journal directory must not be a symlink');
|
|
16
|
+
}
|
|
17
|
+
async function assertRecord(id) {
|
|
18
|
+
try { const stat = await lstat(path(id)); if (stat.isSymbolicLink() || !stat.isFile()) throw new Error('journal record must be a regular file, not a symlink'); }
|
|
19
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
20
|
+
}
|
|
21
|
+
async function get(id) {
|
|
22
|
+
await init();
|
|
23
|
+
await assertRecord(id);
|
|
24
|
+
try {
|
|
25
|
+
const file = await open(path(id), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
26
|
+
try { return JSON.parse(await file.readFile('utf8')); } finally { await file.close(); }
|
|
27
|
+
}
|
|
28
|
+
catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
29
|
+
}
|
|
30
|
+
async function put(id, value) {
|
|
31
|
+
await init();
|
|
32
|
+
await assertRecord(id);
|
|
33
|
+
const temp = `${path(id)}.${randomUUID()}.tmp`;
|
|
34
|
+
const handle = await open(temp, 'wx', 0o600);
|
|
35
|
+
try { await handle.writeFile(JSON.stringify(value)); await handle.sync(); }
|
|
36
|
+
finally { await handle.close(); }
|
|
37
|
+
await rename(temp, path(id));
|
|
38
|
+
const parent = await open(directory, 'r');
|
|
39
|
+
try { await parent.sync(); } finally { await parent.close(); }
|
|
40
|
+
}
|
|
41
|
+
async function withLock(id, operation) {
|
|
42
|
+
await init();
|
|
43
|
+
const lock = join(directory, `${key(id)}.lock`);
|
|
44
|
+
let handle;
|
|
45
|
+
try { handle = await open(lock, 'wx', 0o600); }
|
|
46
|
+
catch (error) { if (error.code === 'EEXIST') throw new Error('action journal is locked; reconcile its prior writer before retry'); throw error; }
|
|
47
|
+
try {
|
|
48
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, created_at: new Date().toISOString() }));
|
|
49
|
+
await handle.sync();
|
|
50
|
+
return await operation();
|
|
51
|
+
} finally { await handle.close(); await rm(lock); }
|
|
52
|
+
}
|
|
53
|
+
async function inspect() {
|
|
54
|
+
// Read-only inventory. Never create a journal or remove a writer lock.
|
|
55
|
+
let entries;
|
|
56
|
+
try {
|
|
57
|
+
const stat = await lstat(directory);
|
|
58
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('unsafe journal directory');
|
|
59
|
+
entries = await readdir(directory);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.code === 'ENOENT') return { complete: false, records: [], locks: [] };
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (entries.length > 10000) throw new Error('journal inventory exceeds inspection bound');
|
|
65
|
+
const records = [], locks = [];
|
|
66
|
+
let complete = true;
|
|
67
|
+
for (const name of entries.sort()) {
|
|
68
|
+
if (!/^[a-f0-9]{64}\.(json|lock)$/.test(name)) { complete = false; continue; }
|
|
69
|
+
try {
|
|
70
|
+
const full = join(directory, name), stat = await lstat(full);
|
|
71
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4 * 1024 * 1024) throw new Error('unsafe journal entry');
|
|
72
|
+
const handle = await open(full, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
73
|
+
let value;
|
|
74
|
+
try { value = JSON.parse(await handle.readFile('utf8')); } finally { await handle.close(); }
|
|
75
|
+
if (name.endsWith('.lock')) { locks.push({ name }); continue; }
|
|
76
|
+
const requestId = value.request?.requestId;
|
|
77
|
+
if (typeof requestId !== 'string' || name !== `${key(requestId)}.json` || typeof value.phase !== 'string') throw new Error('invalid journal identity');
|
|
78
|
+
records.push({ requestId, itemId: value.item_id ?? value.request?.itemId ?? null, phase: value.phase, kind: value.kind ?? 'work' });
|
|
79
|
+
} catch { complete = false; if (name.endsWith('.lock')) locks.push({ name }); }
|
|
80
|
+
}
|
|
81
|
+
return { complete, records, locks };
|
|
82
|
+
}
|
|
83
|
+
return Object.freeze({ get, put, withLock, inspect });
|
|
84
|
+
}
|