@arjunkhera/atlas 0.3.15 → 0.3.17
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/agents/artifact-format/example.html +23 -7
- package/agents/artifact-format/system.css +71 -18
- package/agents/artifact-renderer.md +42 -11
- package/door/cli.mjs +12 -3
- package/door/lib/design.mjs +50 -1
- package/package.json +1 -1
- package/skills/lead/SKILL.md +1 -1
- package/skills/sdlc-task/SKILL.md +4 -3
- package/skills/sdlc-task/design/README.md +11 -1
- package/skills/sdlc-task/templates/design-hub.html +1 -1
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
<!-- The structural reference for a rendered
|
|
1
|
+
<!-- The structural reference for a rendered page. Neutral content: a
|
|
2
2
|
made-up repo, "orders-api". Copy the structure and the classes, never the
|
|
3
|
-
words. In a real render, paste system.css where the comment says.
|
|
3
|
+
words. In a real render, paste system.css where the comment says. Every
|
|
4
|
+
colour below is a token from system.css, so the page works in the light
|
|
5
|
+
and the dark theme. Never write a colour value here. -->
|
|
4
6
|
<title>Refund rounding</title>
|
|
5
7
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Caprasimo&family=Figtree:wght@400;600;700&display=swap">
|
|
6
8
|
<style>
|
|
7
9
|
/* paste system.css here in a real render */
|
|
8
|
-
body { background:
|
|
10
|
+
body { background: var(--color-bg); color: var(--color-text); font: 16.5px/1.7 Figtree, system-ui, sans-serif; margin: 0; }
|
|
9
11
|
[data-shell] { display: grid; grid-template-columns: 288px minmax(0, 860px); gap: 40px; padding-inline: 24px; }
|
|
10
12
|
[data-rail] { position: sticky; top: 0; align-self: start; padding-block: 28px; display: flex; flex-direction: column; gap: 14px; }
|
|
11
13
|
[data-main] { padding-block: 28px 80px; }
|
|
12
14
|
h1, h2 { font-family: Caprasimo, Georgia, serif; font-weight: 400; text-wrap: balance; }
|
|
13
15
|
h2 { font-size: 29px; margin: 0; }
|
|
14
|
-
.kicker { font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color:
|
|
15
|
-
.intent { font-style: italic; color:
|
|
16
|
-
.prompt { border: 1px dashed
|
|
16
|
+
.kicker { font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--color-accent); font-weight: 700; }
|
|
17
|
+
.intent { font-style: italic; color: var(--color-muted); margin: 4px 0 12px; }
|
|
18
|
+
.prompt { border: 1px dashed var(--color-line-strong); border-radius: 8px; padding: 10px 14px; color: var(--color-muted); font-size: 14px; }
|
|
17
19
|
.table-wrap { overflow-x: auto; }
|
|
18
|
-
section { padding-block: 28px; border-bottom: 1px solid
|
|
20
|
+
section { padding-block: 28px; border-bottom: 1px solid var(--color-line); }
|
|
21
|
+
.theme { font: inherit; font-size: 13px; color: var(--color-text); background: var(--color-surface); border: 1px solid var(--color-line-strong); border-radius: 999px; padding: 4px 12px; cursor: pointer; align-self: start; }
|
|
19
22
|
@media (max-width: 760px) { [data-shell] { grid-template-columns: 1fr; padding-inline: 16px; } [data-rail] { position: static; } }
|
|
20
23
|
</style>
|
|
21
24
|
<div data-shell>
|
|
@@ -23,6 +26,7 @@
|
|
|
23
26
|
<div class="kicker">orders-api · Design doc</div>
|
|
24
27
|
<div style="font-family: Caprasimo, Georgia, serif; font-size: 22px; line-height: 1.1">Refund rounding</div>
|
|
25
28
|
<div><span class="tag tag-accent">DRAFT</span> <span class="tag tag-neutral">service</span></div>
|
|
29
|
+
<button type="button" class="theme" id="theme" hidden>Dark</button>
|
|
26
30
|
<nav style="display: flex; flex-direction: column; gap: 4px; font-size: 14px">
|
|
27
31
|
<strong>1 · Frame</strong>
|
|
28
32
|
<a href="#summary">Summary</a>
|
|
@@ -69,3 +73,15 @@
|
|
|
69
73
|
</section>
|
|
70
74
|
</main>
|
|
71
75
|
</div>
|
|
76
|
+
<script>
|
|
77
|
+
// Optional. With no script the page follows the system setting. The switch
|
|
78
|
+
// sets data-theme on <html>, and system.css lets that choice win.
|
|
79
|
+
(function () {
|
|
80
|
+
var root = document.documentElement, btn = document.getElementById('theme');
|
|
81
|
+
var dark = false;
|
|
82
|
+
try { dark = window.matchMedia('(prefers-color-scheme: dark)').matches; } catch (e) {}
|
|
83
|
+
function show() { btn.textContent = dark ? 'Light' : 'Dark'; }
|
|
84
|
+
btn.hidden = false; show();
|
|
85
|
+
btn.addEventListener('click', function () { dark = !dark; root.setAttribute('data-theme', dark ? 'dark' : 'light'); show(); });
|
|
86
|
+
})();
|
|
87
|
+
</script>
|
|
@@ -1,22 +1,5 @@
|
|
|
1
1
|
/* Organic — design-system tokens and component classes. This file is the source of truth for the system's look; retune it here and see readme.md. */
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
3
|
:root {
|
|
21
4
|
--color-bg: #f5ead8;
|
|
22
5
|
--color-surface: #ebddc5;
|
|
@@ -77,6 +60,76 @@
|
|
|
77
60
|
--shadow-sm: 0 1px 2px color-mix(in srgb, #2e2b25 14%, transparent);
|
|
78
61
|
--shadow-md: 0 3px 10px color-mix(in srgb, #2e2b25 16%, transparent);
|
|
79
62
|
--shadow-lg: 0 12px 32px color-mix(in srgb, #2e2b25 22%, transparent);
|
|
63
|
+
--color-scrim: color-mix(in srgb, #2e2b25 50%, transparent);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* The quiet reading palette, light and dark (owner, 9 October 2026: "We
|
|
67
|
+
should support a dark mode as well"). The light set is the ratified quiet
|
|
68
|
+
override of the Organic ground. The dark set holds the same names, so a
|
|
69
|
+
page that uses only tokens works in both themes. The page follows the
|
|
70
|
+
system setting. A switch can set data-theme="light" or "dark" on <html>,
|
|
71
|
+
and that choice wins. Never write a colour on a page outside these blocks. */
|
|
72
|
+
:root {
|
|
73
|
+
--color-bg: #fcfbf9;
|
|
74
|
+
--color-surface: #f4f0ea;
|
|
75
|
+
--color-text: #211d19;
|
|
76
|
+
--color-muted: #5c554e;
|
|
77
|
+
--color-line: #ece7e1;
|
|
78
|
+
--color-line-strong: #d9d2c9;
|
|
79
|
+
--color-accent: #a97f5f;
|
|
80
|
+
color-scheme: light;
|
|
81
|
+
}
|
|
82
|
+
:root[data-theme="dark"] {
|
|
83
|
+
--color-bg: #1d1b19;
|
|
84
|
+
--color-surface: #282522;
|
|
85
|
+
--color-text: #ece6dd;
|
|
86
|
+
--color-muted: #b5ada2;
|
|
87
|
+
--color-line: #36322e;
|
|
88
|
+
--color-line-strong: #4d4842;
|
|
89
|
+
--color-accent: #d3a27c;
|
|
90
|
+
--color-accent-2: #a9ba88;
|
|
91
|
+
--color-divider: color-mix(in srgb, #ece6dd 16%, transparent);
|
|
92
|
+
--color-neutral-100: #2e2b25; --color-neutral-200: #3a362f; --color-neutral-300: #474238;
|
|
93
|
+
--color-neutral-400: #645c50; --color-neutral-500: #82796a; --color-neutral-600: #a19786;
|
|
94
|
+
--color-neutral-700: #c0b6a5; --color-neutral-800: #dcd3c4; --color-neutral-900: #f2ece2;
|
|
95
|
+
--color-accent-100: #402310; --color-accent-200: #643312; --color-accent-300: #8c491a;
|
|
96
|
+
--color-accent-400: #b2622d; --color-accent-500: #d67f48; --color-accent-600: #f6a06b;
|
|
97
|
+
--color-accent-700: #ffc6a5; --color-accent-800: #ffe1d0; --color-accent-900: #fff2eb;
|
|
98
|
+
--color-accent-2-100: #272e1b; --color-accent-2-200: #3d472b; --color-accent-2-300: #56633f;
|
|
99
|
+
--color-accent-2-400: #728157; --color-accent-2-500: #8fa073; --color-accent-2-600: #aebf92;
|
|
100
|
+
--color-accent-2-700: #ccdbb2; --color-accent-2-800: #e1eecc; --color-accent-2-900: #f0fae1;
|
|
101
|
+
--shadow-sm: 0 0 0 1px color-mix(in srgb, #ece6dd 8%, transparent);
|
|
102
|
+
--shadow-md: 0 0 0 1px color-mix(in srgb, #ece6dd 10%, transparent), 0 4px 12px color-mix(in srgb, #000000 40%, transparent);
|
|
103
|
+
--shadow-lg: 0 0 0 1px color-mix(in srgb, #ece6dd 12%, transparent), 0 12px 32px color-mix(in srgb, #000000 55%, transparent);
|
|
104
|
+
--color-scrim: color-mix(in srgb, #000000 60%, transparent);
|
|
105
|
+
color-scheme: dark;
|
|
106
|
+
}
|
|
107
|
+
@media (prefers-color-scheme: dark) {
|
|
108
|
+
:root:not([data-theme="light"]) {
|
|
109
|
+
--color-bg: #1d1b19;
|
|
110
|
+
--color-surface: #282522;
|
|
111
|
+
--color-text: #ece6dd;
|
|
112
|
+
--color-muted: #b5ada2;
|
|
113
|
+
--color-line: #36322e;
|
|
114
|
+
--color-line-strong: #4d4842;
|
|
115
|
+
--color-accent: #d3a27c;
|
|
116
|
+
--color-accent-2: #a9ba88;
|
|
117
|
+
--color-divider: color-mix(in srgb, #ece6dd 16%, transparent);
|
|
118
|
+
--color-neutral-100: #2e2b25; --color-neutral-200: #3a362f; --color-neutral-300: #474238;
|
|
119
|
+
--color-neutral-400: #645c50; --color-neutral-500: #82796a; --color-neutral-600: #a19786;
|
|
120
|
+
--color-neutral-700: #c0b6a5; --color-neutral-800: #dcd3c4; --color-neutral-900: #f2ece2;
|
|
121
|
+
--color-accent-100: #402310; --color-accent-200: #643312; --color-accent-300: #8c491a;
|
|
122
|
+
--color-accent-400: #b2622d; --color-accent-500: #d67f48; --color-accent-600: #f6a06b;
|
|
123
|
+
--color-accent-700: #ffc6a5; --color-accent-800: #ffe1d0; --color-accent-900: #fff2eb;
|
|
124
|
+
--color-accent-2-100: #272e1b; --color-accent-2-200: #3d472b; --color-accent-2-300: #56633f;
|
|
125
|
+
--color-accent-2-400: #728157; --color-accent-2-500: #8fa073; --color-accent-2-600: #aebf92;
|
|
126
|
+
--color-accent-2-700: #ccdbb2; --color-accent-2-800: #e1eecc; --color-accent-2-900: #f0fae1;
|
|
127
|
+
--shadow-sm: 0 0 0 1px color-mix(in srgb, #ece6dd 8%, transparent);
|
|
128
|
+
--shadow-md: 0 0 0 1px color-mix(in srgb, #ece6dd 10%, transparent), 0 4px 12px color-mix(in srgb, #000000 40%, transparent);
|
|
129
|
+
--shadow-lg: 0 0 0 1px color-mix(in srgb, #ece6dd 12%, transparent), 0 12px 32px color-mix(in srgb, #000000 55%, transparent);
|
|
130
|
+
--color-scrim: color-mix(in srgb, #000000 60%, transparent);
|
|
131
|
+
color-scheme: dark;
|
|
132
|
+
}
|
|
80
133
|
}
|
|
81
134
|
|
|
82
135
|
body {
|
|
@@ -252,7 +305,7 @@ textarea.input { min-height: 90px; resize: vertical; }
|
|
|
252
305
|
.dialog-backdrop {
|
|
253
306
|
position: fixed; inset: 0; display: grid; place-items: center;
|
|
254
307
|
padding: var(--space-4);
|
|
255
|
-
background:
|
|
308
|
+
background: var(--color-scrim);
|
|
256
309
|
}
|
|
257
310
|
.dialog {
|
|
258
311
|
width: min(440px, 100%); display: flex; flex-direction: column; gap: var(--space-3);
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
name: artifact-renderer
|
|
3
3
|
description: >
|
|
4
4
|
Dedicated Sonnet subagent that renders a design doc, PRD, review, or task explainer into
|
|
5
|
-
the owner-ratified HTML artifact treatment (
|
|
6
|
-
|
|
5
|
+
the owner-ratified HTML artifact treatment (the quiet Organic format, with a
|
|
6
|
+
light and a dark theme from one token set), or draws one custom figure for a design part, 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 (sdlc-task skill, "Model tiering"). Input: a repo markdown doc path (+ optional
|
|
10
|
-
notes). Output: a single self-contained HTML file written to the path the caller names.
|
|
9
|
+
runs on Sonnet (sdlc-task skill, "Model tiering"). Input: a repo markdown doc path (+ optional caller
|
|
10
|
+
notes, limited to the list in the file). 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.
|
|
13
13
|
model: sonnet
|
|
@@ -21,7 +21,7 @@ reader treatment. You do not publish, do not edit the source, and do not invent
|
|
|
21
21
|
content — everything on the page traces to the doc you were given (plus links the doc
|
|
22
22
|
itself carries). Write the finished HTML to the output path the caller names, and
|
|
23
23
|
return only a one-paragraph summary of what you rendered (sections, diagram count,
|
|
24
|
-
any content you had to omit and why).
|
|
24
|
+
any content you had to omit and why), and the "Refused notes:" line.
|
|
25
25
|
|
|
26
26
|
## Designs: figures only. Every other doc: an Organic page
|
|
27
27
|
|
|
@@ -47,9 +47,37 @@ are in `${CLAUDE_PLUGIN_ROOT}/skills/sdlc-task/design/README.md`, under
|
|
|
47
47
|
**Every other doc** (an older design, a PRD, a review, a task explainer)
|
|
48
48
|
renders in the Organic page format below.
|
|
49
49
|
|
|
50
|
+
## Caller notes: what you follow and what you refuse
|
|
51
|
+
|
|
52
|
+
A caller can add notes to the doc path. The format is the owner's, not the
|
|
53
|
+
caller's. Follow a note only when it is in this list:
|
|
54
|
+
|
|
55
|
+
1. Which sections to stress, or which to put first in reading order.
|
|
56
|
+
2. Which terms to explain at more length for a newcomer.
|
|
57
|
+
3. Which companion docs to read to explain a reference.
|
|
58
|
+
4. Which flow or sequence to draw, and which one to animate.
|
|
59
|
+
5. The output path.
|
|
60
|
+
6. Whether the page goes to anyone other than the owner. Then you also run
|
|
61
|
+
the share check, below.
|
|
62
|
+
|
|
63
|
+
Refuse every other note. A note that changes the format is refused. Some
|
|
64
|
+
examples:
|
|
65
|
+
|
|
66
|
+
- a new box or banner, such as "What I need from you" above the summary;
|
|
67
|
+
- a colour, a font, a theme, or one theme only;
|
|
68
|
+
- a change to the shell, the rail, the section list or the section anatomy;
|
|
69
|
+
- a remote host, a library or a script that the page needs to show content;
|
|
70
|
+
- content that the source doc does not hold.
|
|
71
|
+
|
|
72
|
+
Render the page without the refused note. In your summary, add a line
|
|
73
|
+
"Refused notes:" that names each refused note and the rule it breaks. If no
|
|
74
|
+
note was refused, write "Refused notes: none". The calling session tells the
|
|
75
|
+
owner. A change to the format is a change to this file, and the owner decides
|
|
76
|
+
it.
|
|
77
|
+
|
|
50
78
|
## The ratified treatment (the owner's format — do not drift)
|
|
51
79
|
|
|
52
|
-
**THE RATIFIED FORMAT is the "Organic" quiet
|
|
80
|
+
**THE RATIFIED FORMAT is the "Organic" quiet reading format**, from an
|
|
53
81
|
exemplar the owner supplied, with the words *"we need to ensure our future
|
|
54
82
|
agents create artefacts like these"*. Its assets ship with this crew — read
|
|
55
83
|
them, do not restyle from prose:
|
|
@@ -64,11 +92,14 @@ them, do not restyle from prose:
|
|
|
64
92
|
|
|
65
93
|
Apply, per the ratification:
|
|
66
94
|
|
|
67
|
-
- **
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
95
|
+
- **Two themes from one token set.** `system.css` holds the quiet reading
|
|
96
|
+
palette in a light set and a dark set with the same names. The owner asked
|
|
97
|
+
for dark mode on 9 October 2026. Paste the whole sheet, its dark blocks too.
|
|
98
|
+
Paint every colour with a token, such as `var(--color-bg)`,
|
|
99
|
+
`var(--color-text)`, `var(--color-muted)` or `var(--color-accent)`.
|
|
100
|
+
Never write a colour value outside the sheet's token blocks.
|
|
101
|
+
- **The page follows the system setting.** A light or dark switch, as in
|
|
102
|
+
`example.html`, is optional. The page must work with its script removed.
|
|
72
103
|
- **Shell**: 288px sticky left rail — kicker "<repo name> · Design doc", title,
|
|
73
104
|
status tags, per-section completeness meter, TOC grouped 1·Frame /
|
|
74
105
|
2·Requirements / 3·Current state / 4·Design / 5·Decisions / 6·Delivery /
|
package/door/cli.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { scan, toText, verdict, CHECK_VERSION, SPECS } from '../shape/check.mjs'
|
|
|
23
23
|
import { packagePackageVersion } from './lib/releases.mjs';
|
|
24
24
|
import { install, upgrade, doctor } from './lib/install.mjs';
|
|
25
25
|
import { checkPaths, LIMITS } from './lib/ste.mjs';
|
|
26
|
-
import { checkDesignFolder } from './lib/design.mjs';
|
|
26
|
+
import { checkDesignFolder, designFolderOf, repoRootFrom } from './lib/design.mjs';
|
|
27
27
|
import { writeDesignPage, readTracker } from './lib/design-build.mjs';
|
|
28
28
|
import { loadPrivateTerms } from './lib/privacy.mjs';
|
|
29
29
|
import { checkCommand as testsCheck, writeCommand as testsWrite } from './lib/tests.mjs';
|
|
@@ -52,6 +52,8 @@ const HELP = `atlas — the door into a repo's Atlas files
|
|
|
52
52
|
--share also run the privacy filter, quotes included
|
|
53
53
|
--terms <file> private terms; default ~/.config/atlas/private-terms.txt
|
|
54
54
|
atlas design check <folder> … check a design folder: each needed part, no state line, every code in the Key
|
|
55
|
+
atlas design folder print where this repo keeps its designs: design.folder in atlas.yaml, or docs/design-docs
|
|
56
|
+
--root <repo> the repo; default the nearest folder up from here that holds atlas.yaml
|
|
55
57
|
atlas design build <folder> build the design's page from its folder
|
|
56
58
|
--tracker <file> the tracker data, as JSON, from the lead
|
|
57
59
|
--out <file> the page; default docs/artifacts/<folder name>.html
|
|
@@ -117,7 +119,7 @@ export const FLAGS = Object.freeze({
|
|
|
117
119
|
'kind-drift': { root: 'optional', 'record-kind': 'value', registry: 'value', repo: 'value' },
|
|
118
120
|
check: { root: 'optional', repo: 'value', 'fail-on': 'value' },
|
|
119
121
|
ste: { share: 'switch', terms: 'value' },
|
|
120
|
-
design: { tracker: 'value', out: 'value', draft: 'switch' },
|
|
122
|
+
design: { tracker: 'value', out: 'value', draft: 'switch', root: 'optional' },
|
|
121
123
|
tests: { root: 'optional', halves: 'value', evidence: 'value', 'guards-from': 'value', tests: 'value', 'dry-run': 'switch', run: 'value', own: 'value', base: 'value', covers: 'switch', findings: 'value', repo: 'value', max: 'value', bot: 'value', since: 'value', out: 'value', text: 'switch', json: 'switch' },
|
|
122
124
|
install: { local: 'switch', from: 'value', 'skip-global': 'switch', yes: 'switch' },
|
|
123
125
|
upgrade: { version: 'optional', yes: 'switch' },
|
|
@@ -258,7 +260,14 @@ function steCommand(chosen) {
|
|
|
258
260
|
function designCommand(chosen) {
|
|
259
261
|
const what = chosen._[1];
|
|
260
262
|
const folders = chosen._.slice(2);
|
|
261
|
-
if (what
|
|
263
|
+
if (what === 'folder') {
|
|
264
|
+
if (folders.length || chosen.tracker !== undefined || chosen.out !== undefined || chosen.draft) throw new Error('atlas design folder takes only --root.');
|
|
265
|
+
const where = designFolderOf(chosen.root === undefined || chosen.root === true ? repoRootFrom(process.cwd()) : resolve(chosen.root));
|
|
266
|
+
line(where.folder);
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
if (chosen.root !== undefined) throw new Error('--root works only with atlas design folder.');
|
|
270
|
+
if (what !== 'check' && what !== 'build') throw new Error('use atlas design check <folder>, atlas design build <folder> or atlas design folder');
|
|
262
271
|
if (!folders.length) throw new Error(`give a design folder: atlas design ${what} <folder>`);
|
|
263
272
|
if (what === 'check' && (chosen.tracker !== undefined || chosen.out !== undefined || chosen.draft)) throw new Error('--tracker, --out and --draft work only with atlas design build.');
|
|
264
273
|
if (what === 'build') {
|
package/door/lib/design.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
|
|
11
11
|
import { join, dirname, resolve, basename } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import { parseYaml } from '../../shape/check.mjs';
|
|
13
|
+
import { parseYaml, insideRepo } from '../../shape/check.mjs';
|
|
14
14
|
|
|
15
15
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
16
16
|
export const PARTS_FILE = join(PACKAGE_ROOT, 'skills', 'sdlc-task', 'design', 'parts.yaml');
|
|
@@ -227,3 +227,52 @@ export function checkDesign(design, registry = loadRegistry()) {
|
|
|
227
227
|
export function checkDesignFolder(folder, registry) {
|
|
228
228
|
return checkDesign(readDesign(folder), registry);
|
|
229
229
|
}
|
|
230
|
+
|
|
231
|
+
// Where a repo keeps its designs. A repo sets the folder in atlas.yaml, as
|
|
232
|
+
// `design: { folder: docs/design }`. Without the key, the folder is
|
|
233
|
+
// docs/design-docs, so a repo that never sets it does not change. The shape
|
|
234
|
+
// check does not read the key: a check change makes every repo's copy behind.
|
|
235
|
+
// The path is relative to the repo root, plain (no `.`, `..` or `~`), and
|
|
236
|
+
// never in a folder that a tool owns or a crew may not read.
|
|
237
|
+
export const DEFAULT_DESIGN_FOLDER = 'docs/design-docs';
|
|
238
|
+
export const CLOSED_FOLDERS = Object.freeze(['.git', '.github', '.claude', '.atlas', 'node_modules']);
|
|
239
|
+
|
|
240
|
+
export function designFolderOf(root) {
|
|
241
|
+
const fallback = { folder: DEFAULT_DESIGN_FOLDER, path: join(root, DEFAULT_DESIGN_FOLDER), from: 'default' };
|
|
242
|
+
const facts = join(root, 'atlas.yaml');
|
|
243
|
+
const data = existsSync(facts) ? parseYaml(readFileSync(facts, 'utf8')) : null;
|
|
244
|
+
if (!data || typeof data !== 'object' || !Object.hasOwn(data, 'design')) return fallback;
|
|
245
|
+
const design = data.design;
|
|
246
|
+
if (!design || typeof design !== 'object' || Array.isArray(design)) throw new Error('atlas.yaml: design must be a map with one key, such as "design: { folder: docs/design }". Remove design to keep docs/design-docs.');
|
|
247
|
+
const unknown = Object.keys(design).filter((key) => key !== 'folder');
|
|
248
|
+
if (unknown.length) throw new Error(`atlas.yaml: design has the unknown key(s) ${unknown.join(', ')}. The one key is folder.`);
|
|
249
|
+
const raw = design.folder;
|
|
250
|
+
if (typeof raw !== 'string' || !raw.trim()) throw new Error('atlas.yaml: design.folder must be a path, such as docs/design.');
|
|
251
|
+
const steps = raw.trim().replace(/\/+$/, '').split('/');
|
|
252
|
+
if (raw.trim().startsWith('/') || /^[A-Za-z]:/.test(raw.trim()) || steps.some((step) => step === '' || step === '.' || step === '..' || step.includes('\\')) || steps[0].startsWith('~')) {
|
|
253
|
+
throw new Error(`atlas.yaml: design.folder "${raw}" must be a plain path inside the repo, relative to its root, such as docs/design.`);
|
|
254
|
+
}
|
|
255
|
+
if (CLOSED_FOLDERS.includes(steps[0])) throw new Error(`atlas.yaml: design.folder "${raw}" is in ${steps[0]}/, which a tool owns or a crew may not read. Pick a folder such as docs/design.`);
|
|
256
|
+
const folder = steps.join('/');
|
|
257
|
+
return { folder, path: join(root, folder), from: 'atlas.yaml' };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// The repo root for a command run with no --root: the nearest folder, from
|
|
261
|
+
// here up, that holds atlas.yaml. The search stops at the git root. With no
|
|
262
|
+
// atlas.yaml on the way, it is the folder the command ran in.
|
|
263
|
+
export function repoRootFrom(start) {
|
|
264
|
+
for (let dir = resolve(start); ; dir = dirname(dir)) {
|
|
265
|
+
if (existsSync(join(dir, 'atlas.yaml'))) return dir;
|
|
266
|
+
if (existsSync(join(dir, '.git')) || dirname(dir) === dir) return resolve(start);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Each design in the repo: a folder under the design folder that holds
|
|
271
|
+
// design.yaml. A missing design folder holds no designs.
|
|
272
|
+
export function designFoldersIn(root) {
|
|
273
|
+
const { path } = designFolderOf(root);
|
|
274
|
+
if (!existsSync(path) || !statSync(path).isDirectory()) return [];
|
|
275
|
+
return readdirSync(path, { withFileTypes: true })
|
|
276
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(path, entry.name, DESIGN_FILE)))
|
|
277
|
+
.map((entry) => join(path, entry.name));
|
|
278
|
+
}
|
package/package.json
CHANGED
package/skills/lead/SKILL.md
CHANGED
|
@@ -101,7 +101,7 @@ set. A test keeps the two lists equal.
|
|
|
101
101
|
| `atlas code-read` | To resolve the citations of a code digest |
|
|
102
102
|
| `atlas ste` | Before each publish; `--share` before a page goes to anyone else |
|
|
103
103
|
| `atlas tests` | `write` puts the test kit in an area. `check` runs before a merge. `proof` prints the proof table of one run |
|
|
104
|
-
| `atlas design` | `check` before you show a design; `build` to make its page |
|
|
104
|
+
| `atlas design` | `folder` to learn where designs live; `check` before you show a design; `build` to make its page |
|
|
105
105
|
|
|
106
106
|
A person merges every file that `atlas tooling` writes.
|
|
107
107
|
A person merges every file that `atlas tests write` writes.
|
|
@@ -68,8 +68,9 @@ with hashes retire.)
|
|
|
68
68
|
commit to it. Local sessions use `feature/`, `fix/` or `chore/`; cloud
|
|
69
69
|
sessions use `claude/`. The pull request targets that branch.
|
|
70
70
|
4. **Design** (standard and above): follow [`design/README.md`](design/README.md).
|
|
71
|
-
Pick the kinds, and say why. Make the folder
|
|
72
|
-
with `design.yaml
|
|
71
|
+
Pick the kinds, and say why. Make the folder `<design folder>/<slug>/`
|
|
72
|
+
with `design.yaml` (`atlas design folder` prints the design folder),
|
|
73
|
+
then write the Summary, Your words and Goals first.
|
|
73
74
|
Read the template for each part in `design/parts/`. List the folder in
|
|
74
75
|
`docs/index.md` in the commit that first lands it. Hotfix tier skips the design: the definition of done lives on
|
|
75
76
|
the work item, and a resume anchor covers pauses.
|
|
@@ -123,7 +124,7 @@ A lock is the owner's approval of a short design (target design, flow 4).
|
|
|
123
124
|
not.
|
|
124
125
|
4. Call `item_lock` with the definition of done as `scope.text`. Set
|
|
125
126
|
`scope.design_doc` to a link to the design at the approved commit, such
|
|
126
|
-
as `https://github.com/<owner>/<repo>/tree/<sha
|
|
127
|
+
as `https://github.com/<owner>/<repo>/tree/<sha>/<design folder>/<slug>`.
|
|
127
128
|
The verb keeps a fingerprint of that text for
|
|
128
129
|
the tools. No person reads or writes a hash, and no frozen text is copied
|
|
129
130
|
into the doc.
|
|
@@ -126,8 +126,18 @@ change as work moves went to the tracker.
|
|
|
126
126
|
|
|
127
127
|
## The folder
|
|
128
128
|
|
|
129
|
+
Each design is one folder in the repo's design folder. `atlas design folder`
|
|
130
|
+
prints that folder. A repo sets it in `atlas.yaml`:
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
design:
|
|
134
|
+
folder: docs/design
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Without the key, the design folder is `docs/design-docs`.
|
|
138
|
+
|
|
129
139
|
```text
|
|
130
|
-
|
|
140
|
+
<design folder>/<slug>/
|
|
131
141
|
design.yaml title, item, product, kinds, shared, look
|
|
132
142
|
summary.md ---
|
|
133
143
|
part: summary
|
|
@@ -37,7 +37,7 @@ a { color: var(--accent); }
|
|
|
37
37
|
<div class="wrap"><table>
|
|
38
38
|
<thead><tr><th>Changed</th><th>Product</th><th>Design</th><th>State</th><th>Source</th></tr></thead>
|
|
39
39
|
<tbody>
|
|
40
|
-
<tr><td class="when">2026-10-07</td><td class="product">atlas</td><td><a href="https://claude.ai/artifact/EXAMPLE">Design docs as walkthroughs, in one place</a></td><td><span class="state build">Building</span></td><td><a href="https://github.com/ACCOUNT/REPO/
|
|
40
|
+
<tr><td class="when">2026-10-07</td><td class="product">atlas</td><td><a href="https://claude.ai/artifact/EXAMPLE">Design docs as walkthroughs, in one place</a></td><td><span class="state build">Building</span></td><td><a href="https://github.com/ACCOUNT/REPO/tree/master/DESIGN-FOLDER/EXAMPLE">design doc</a></td></tr>
|
|
41
41
|
</tbody>
|
|
42
42
|
</table></div>
|
|
43
43
|
</main></body></html>
|