@arjunkhera/atlas 0.3.45 → 0.3.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/door/cli.mjs +4 -2
- package/door/lib/install.mjs +47 -5
- package/package.json +1 -1
- package/skills/lead/SKILL.md +23 -1
- package/work/lib/child-env.mjs +6 -0
- package/work/mcp.mjs +16 -8
package/door/cli.mjs
CHANGED
|
@@ -140,8 +140,10 @@ const HELP = `atlas — the door into a repo's Atlas files
|
|
|
140
140
|
compare the judge's verdict with the sealed tasks and the runs; exit 0 only when all pass
|
|
141
141
|
|
|
142
142
|
atlas install install Atlas for every folder on this Mac
|
|
143
|
+
--cloud in a cloud setup script: skip the checks that need a started session
|
|
143
144
|
atlas upgrade [--version <v>] check, show and install a newer release
|
|
144
145
|
atlas doctor is Atlas installed and working here
|
|
146
|
+
--cloud before the first cloud session: skip the checks that need a started session
|
|
145
147
|
|
|
146
148
|
Atlas ${ATLAS_VERSION}. Check version ${CHECK_VERSION}; it reads spec ${Object.keys(SPECS).join(', ')}.
|
|
147
149
|
A person merges every file the tooling writer writes.
|
|
@@ -169,9 +171,9 @@ export const FLAGS = Object.freeze({
|
|
|
169
171
|
upkeep: { root: 'optional', event: 'value', json: 'switch' },
|
|
170
172
|
gate: { repo: 'value', root: 'optional', 'dry-run': 'switch', off: 'switch', on: 'switch' },
|
|
171
173
|
judge: { tasks: 'value', verdict: 'value', seal: 'value', runs: 'value', out: 'value', repo: 'value', budget: 'value', model: 'value' },
|
|
172
|
-
install: { local: 'switch', from: 'value', 'skip-global': 'switch', yes: 'switch' },
|
|
174
|
+
install: { local: 'switch', from: 'value', 'skip-global': 'switch', yes: 'switch', cloud: 'switch' },
|
|
173
175
|
upgrade: { version: 'optional', yes: 'switch' },
|
|
174
|
-
doctor: {},
|
|
176
|
+
doctor: { cloud: 'switch' },
|
|
175
177
|
});
|
|
176
178
|
|
|
177
179
|
// Every command takes --help.
|
package/door/lib/install.mjs
CHANGED
|
@@ -140,9 +140,12 @@ export async function install({ packageRoot, options = {}, line = console.log, e
|
|
|
140
140
|
const installArgs = ['plugin', 'install', PLUGIN_ID, '--scope', 'user'];
|
|
141
141
|
exec('claude', installArgs);
|
|
142
142
|
line('');
|
|
143
|
-
|
|
143
|
+
// A cloud setup script runs before any session: the key comes from the
|
|
144
|
+
// environment variable, and no tools server has started yet (item 92cfbba3).
|
|
145
|
+
if (options.cloud) line('Installed. In a cloud session the key comes from the environment, and the lead checks the version at its first reply.');
|
|
146
|
+
else for (const text of connectionSteps(env)) line(text);
|
|
144
147
|
line('');
|
|
145
|
-
return doctor({ packageRoot, options, line, env, exec });
|
|
148
|
+
return doctor({ packageRoot, options: { cloud: Boolean(options.cloud) }, line, env, exec });
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
// ---------------------------------------------------------------- upgrade
|
|
@@ -151,13 +154,15 @@ export async function install({ packageRoot, options = {}, line = console.log, e
|
|
|
151
154
|
export const blobSha = (bytes) => createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
|
|
152
155
|
export const sha512Integrity = (bytes) => `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
|
|
153
156
|
|
|
154
|
-
function filesUnder(root) {
|
|
157
|
+
function filesUnder(root, skip = () => false) {
|
|
155
158
|
const out = new Map();
|
|
156
159
|
const walk = (dir) => {
|
|
157
160
|
for (const entry of readdirSync(dir)) {
|
|
158
161
|
const full = join(dir, entry);
|
|
162
|
+
const path = relative(root, full).split('\\').join('/');
|
|
163
|
+
if (skip(path)) continue;
|
|
159
164
|
if (statSync(full).isDirectory()) walk(full);
|
|
160
|
-
else out.set(
|
|
165
|
+
else out.set(path, readFileSync(full));
|
|
161
166
|
}
|
|
162
167
|
};
|
|
163
168
|
walk(root);
|
|
@@ -201,6 +206,36 @@ export function matchTree({ tree, files }) {
|
|
|
201
206
|
return problems;
|
|
202
207
|
}
|
|
203
208
|
|
|
209
|
+
// Claude Code writes `.in_use/` into a plugin copy to mark it in use. It is
|
|
210
|
+
// not part of the package, so the proof of a loaded copy skips it.
|
|
211
|
+
const COPY_MARKER = /^\.in_use(\/|$)/;
|
|
212
|
+
const firstLines = (error, count = 3) => String(error?.stderr || error?.message || error).trim().split('\n').slice(0, count).join(' ');
|
|
213
|
+
|
|
214
|
+
// Is the copy at packageRoot the copy that a master release run built? The
|
|
215
|
+
// same proof as the upgrade, run on files already loaded. It reads only.
|
|
216
|
+
// The proof runs from the copy it checks, so it catches stale, broken or
|
|
217
|
+
// drifted files, not a package made to lie (security review of atlas#187).
|
|
218
|
+
// "unproven" is the upgrade's refusal: no master run built this version.
|
|
219
|
+
// "not checked" is never a pass: the caller says the reason.
|
|
220
|
+
export function proveCopy({ packageRoot, exec = defaultExec }) {
|
|
221
|
+
try {
|
|
222
|
+
const { version } = packageJson(packageRoot);
|
|
223
|
+
const repo = repoOf(packageRoot);
|
|
224
|
+
const gh = (args) => JSON.parse(exec('gh', args));
|
|
225
|
+
let run;
|
|
226
|
+
try { run = findReleaseRun({ gh, repo, version }); } catch (error) { return { state: 'not checked', reason: `GitHub did not answer: ${firstLines(error)}` }; }
|
|
227
|
+
if (!run) return { state: 'unproven', reason: `no successful release run on a commit of master built ${version}. It may have been published from a branch.` };
|
|
228
|
+
let tree;
|
|
229
|
+
try { tree = gh(['api', '--method', 'GET', `repos/${repo}/git/trees/${run.head_sha}?recursive=1`]).tree; } catch (error) { return { state: 'not checked', reason: `GitHub did not give the file tree: ${firstLines(error)}` }; }
|
|
230
|
+
const files = filesUnder(packageRoot, (path) => COPY_MARKER.test(path));
|
|
231
|
+
const problems = matchTree({ tree, files });
|
|
232
|
+
const found = { run: run.id, commit: run.head_sha };
|
|
233
|
+
return problems.length ? { state: 'differs', ...found, problems: problems.slice(0, 20) } : { state: 'match', ...found, files: files.size };
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return { state: 'not checked', reason: `the proof stopped: ${firstLines(error)}` };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
204
239
|
export async function upgrade({ packageRoot, options = {}, line = console.log, env = process.env, exec = defaultExec, ask = defaultAsk }) {
|
|
205
240
|
const { name, version: installed } = packageJson(packageRoot);
|
|
206
241
|
const repo = repoOf(packageRoot);
|
|
@@ -276,11 +311,18 @@ export function doctor({ packageRoot, options = {}, line = console.log, env = pr
|
|
|
276
311
|
}
|
|
277
312
|
|
|
278
313
|
// Off the cloud, the key lives only in the connection file. The cloud passes it as a variable.
|
|
279
|
-
if (env.CLAUDE_CODE_REMOTE !== 'true') {
|
|
314
|
+
if (env.CLAUDE_CODE_REMOTE !== 'true' && !options.cloud) {
|
|
280
315
|
const connection = readConnection({ HOME: env.HOME });
|
|
281
316
|
check('connection', connection.state === 'ok', connection.state === 'ok' ? `the Engram connection file is in place; the key expires ${connection.expires}` : connection.message);
|
|
282
317
|
}
|
|
283
318
|
|
|
319
|
+
// A cloud setup runs before the first session, so the three checks of a
|
|
320
|
+
// started tools server cannot pass yet. The lead checks at its first reply.
|
|
321
|
+
if (options.cloud) {
|
|
322
|
+
for (const row of rows) line(`${row.pass ? 'PASS' : 'FAIL'} ${row.name.padEnd(12)} ${row.detail}`);
|
|
323
|
+
line('SKIP tools server, engram and gh: no session has started yet');
|
|
324
|
+
return rows.every((row) => row.pass) ? 0 : 1;
|
|
325
|
+
}
|
|
284
326
|
let health = null;
|
|
285
327
|
try { health = JSON.parse(readFileSync(join(env.HOME, '.atlas', 'health.json'), 'utf8')); } catch { /* never started */ }
|
|
286
328
|
check('tools server', Boolean(health), health ? `last started ${health.started_at}, Atlas ${health.package_version}` : 'the Atlas tools server has not started yet. Open a Claude Code session, then run "atlas doctor" again.');
|
package/package.json
CHANGED
package/skills/lead/SKILL.md
CHANGED
|
@@ -93,7 +93,7 @@ set. A test keeps the two lists equal.
|
|
|
93
93
|
| Command | When to call it |
|
|
94
94
|
|---|---|
|
|
95
95
|
| `atlas install` | Once on a new Mac, when the owner asks for Atlas there |
|
|
96
|
-
| `atlas upgrade` | When the owner asks to upgrade Atlas. Call the `atlas_upgrade` tool instead: the Bash sandbox blocks this command. Run the command by hand only where that tool is missing |
|
|
96
|
+
| `atlas upgrade` | When the owner asks to upgrade Atlas. Call the `atlas_upgrade` tool instead: the Bash sandbox blocks this command. Run the command by hand only where that tool is missing. In a cloud session, call the tool with `check: true` at the first reply, as section 7 says |
|
|
97
97
|
| `atlas doctor` | When Atlas seems missing or broken in a session |
|
|
98
98
|
| `atlas check` | Before you ask for a merge in a repo that has the shape check |
|
|
99
99
|
| `atlas status` | To learn whether a repo's check is current or behind |
|
|
@@ -110,3 +110,25 @@ set. A test keeps the two lists equal.
|
|
|
110
110
|
|
|
111
111
|
A person merges every file that `atlas tooling` writes.
|
|
112
112
|
A person merges every file that `atlas tests write` writes.
|
|
113
|
+
|
|
114
|
+
## 7. The first reply of a cloud session
|
|
115
|
+
|
|
116
|
+
A cloud session can start from an old setup and run an old Atlas. So in a
|
|
117
|
+
Claude cloud session, do these steps before the first reply.
|
|
118
|
+
|
|
119
|
+
1. Call the `atlas_upgrade` tool with `check: true`. It writes no Atlas
|
|
120
|
+
state.
|
|
121
|
+
2. When `behind` is true, put one line in the reply: "This session runs
|
|
122
|
+
Atlas <loaded>; npm has <newest>. A new session from the cloud
|
|
123
|
+
environment loads the newest." Use the two versions from the answer.
|
|
124
|
+
3. When `provenance.state` is `differs` or `unproven`, the loaded Atlas
|
|
125
|
+
is not proven to be its release. Say so, with the first reason. Then
|
|
126
|
+
stop all Atlas work until the owner answers.
|
|
127
|
+
4. When `provenance.state` is `not checked`, say so in one line, with the
|
|
128
|
+
reason. Then go on.
|
|
129
|
+
5. When the tool is missing, or its answer has no `provenance`, this Atlas
|
|
130
|
+
is too old for the check. Say so in one line, and go on.
|
|
131
|
+
6. When the tool refuses the check, give its error in one line, and go on.
|
|
132
|
+
|
|
133
|
+
The check runs from the copy it checks. It finds stale or drifted files. It
|
|
134
|
+
cannot find a package made to lie, so never call it a security proof.
|
package/work/lib/child-env.mjs
CHANGED
|
@@ -8,6 +8,11 @@ const CHILD_VARIABLES = ['PATH', 'HOME', 'USER', 'LOGNAME', 'LANG', 'LC_ALL', 'T
|
|
|
8
8
|
// proxy is a loopback address (owner, 7 October 2026: "yes fix it and test").
|
|
9
9
|
const CLOUD_PROXY_VARIABLES = ['HTTPS_PROXY', 'https_proxy'];
|
|
10
10
|
const CLOUD_NO_PROXY_VARIABLES = ['NO_PROXY', 'no_proxy'];
|
|
11
|
+
// The two fixed values of Claude's cloud proxy (owner, 7 October 2026, "Narrow
|
|
12
|
+
// exception"). The tools server accepts exactly these values. A child gets the
|
|
13
|
+
// CA file only when it is this exact value: without it, npm behind the proxy
|
|
14
|
+
// waits about 70 seconds before it answers (item 92cfbba3, 24 October 2026).
|
|
15
|
+
export const CLOUD_NODE_VARIABLES = Object.freeze({ NODE_EXTRA_CA_CERTS: '/root/.ccr/ca-bundle.crt', NODE_PATH: '/usr/local/lib/node_modules_global' });
|
|
11
16
|
const LOOPBACK_PROXY = /^http:\/\/(127\.0\.0\.1|localhost|\[::1\]):\d{1,5}\/?$/;
|
|
12
17
|
|
|
13
18
|
export function childEnvironment(env = process.env) {
|
|
@@ -19,6 +24,7 @@ export function childEnvironment(env = process.env) {
|
|
|
19
24
|
const proxies = CLOUD_PROXY_VARIABLES.filter((name) => env[name] !== undefined);
|
|
20
25
|
if (proxies.length && proxies.every((name) => LOOPBACK_PROXY.test(env[name]))) {
|
|
21
26
|
for (const name of [...proxies, ...CLOUD_NO_PROXY_VARIABLES]) if (env[name] !== undefined) out[name] = env[name];
|
|
27
|
+
if (env.NODE_EXTRA_CA_CERTS === CLOUD_NODE_VARIABLES.NODE_EXTRA_CA_CERTS) out.NODE_EXTRA_CA_CERTS = env.NODE_EXTRA_CA_CERTS;
|
|
22
28
|
}
|
|
23
29
|
}
|
|
24
30
|
return out;
|
package/work/mcp.mjs
CHANGED
|
@@ -24,8 +24,8 @@ import { createVerbs, STAGES, TIERS, DRIVERS, LINK_KINDS, REPO_KINDS, WAITS_ON,
|
|
|
24
24
|
import { COST_MEANING, DECISION_KINDS } from './lib/questions.mjs';
|
|
25
25
|
import { FATAL_CONCLUSIONS } from './lib/delivery.mjs';
|
|
26
26
|
import { createMcpServer } from './lib/mcp-stdio.mjs';
|
|
27
|
-
import { childEnvironment } from './lib/child-env.mjs';
|
|
28
|
-
import { upgrade } from '../door/lib/install.mjs';
|
|
27
|
+
import { childEnvironment, CLOUD_NODE_VARIABLES } from './lib/child-env.mjs';
|
|
28
|
+
import { upgrade, proveCopy } from '../door/lib/install.mjs';
|
|
29
29
|
export { childEnvironment };
|
|
30
30
|
|
|
31
31
|
const ATLAS_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
@@ -74,7 +74,7 @@ export const TOOLS = [
|
|
|
74
74
|
{ name: 'where_are_we', description: 'Answer "where are we with X" in six parts: live support and known limits from the code read at question time, then in progress, delivered and proposed from the items, then the current priority decision. Every row carries a citation: a path and a line, an entity id, a pull request, or a decision date. The code half comes from the sub-agent digests, which are refused when they cite a never-read path or quote a never-quote path, and every string is scrubbed by pattern. Computed at read time and never stored. Scope: read, no owner flag needed.', inputSchema: { type: 'object', required: ['product', 'capability', 'code'], properties: { product: S('Product registry id.'), capability: S('The capability the question is about, the X.'), code: { type: 'array', description: 'One digest per repo, from the sub-agents briefed by code_read_brief. At least one is required: an answer with no code read says nothing about what the code does today.', items: CODE_DIGEST }, items: { type: 'array', description: 'Item ids the lead judges to answer this question.', items: { type: 'string' } }, match: { type: 'array', description: 'Terms to match against item titles, summaries and tags. Give items, match, or both.', items: { type: 'string' } }, movement_repos: { type: 'array', description: "Narrow the movement read to these registered repo ids. Default: every repo of the product. The digests' own repos are always read.", items: { type: 'string' } }, since: S('ISO date. Adds what changed since then.') }, additionalProperties: false } },
|
|
75
75
|
{ name: 'built_pending_possible', description: 'Answer "what are we building, what is built, what is pending and what can be done": delivered items by date, active items, paused items with their reason, proposed items with their age, and the decisions that set the order. Item records, delivery marks and decisions only; no code read and no GitHub read. Computed at read time and never stored. Scope: read, no owner flag needed.', inputSchema: { type: 'object', properties: { product: S('Limit the view to one product id.'), since: S('ISO date. Limits built and the decisions to what came after it.') }, additionalProperties: false } },
|
|
76
76
|
{ name: 'design_index', description: 'List every design: each work item with a design-doc link, in any lifecycle, newest change first, with its product, its state for the hub ("Waiting on the owner", "Building", "Paused", "Shipped", "Dropped"), its design docs and its pages. Also returns design_hub_url, the one hub page. When: before the lead rebuilds the "Design docs" hub, after each design publish, lock and done. Item records only; computed at read time and never stored. Scope: read, no owner flag needed.', inputSchema: { type: 'object', properties: { product: S('Limit the list to one product id.') }, additionalProperties: false } },
|
|
77
|
-
{ name: 'atlas_upgrade', description: 'Move Atlas on this Mac to the newest release: the same steps as the "atlas upgrade --yes" command. It checks that a master release run built the release, installs it and updates the plugin. When: the owner asks to upgrade Atlas. Use this tool and not the command: the Bash sandbox blocks the writes to ~/.npm, ~/.atlas and ~/.claude/plugins, and this server runs outside it. With check true it
|
|
77
|
+
{ name: 'atlas_upgrade', description: 'Move Atlas on this Mac to the newest release: the same steps as the "atlas upgrade --yes" command. It checks that a master release run built the release, installs it and updates the plugin. When: the owner asks to upgrade Atlas. Use this tool and not the command: the Bash sandbox blocks the writes to ~/.npm, ~/.atlas and ~/.claude/plugins, and this server runs outside it. With check true it writes no Atlas state: it reports the loaded and the newest version, and checks that the loaded files are the files of their master release commit (provenance: match, differs, unproven or not checked). The check runs from the copy it checks, so it finds stale or drifted files, not a package made to lie. The check runs anywhere, also in a Claude cloud session and with the owner flag off; the lead calls it at the first reply of a cloud session. Scope: this Mac, no tracker write. The upgrade itself is owner present only, and never in a Claude cloud session: a new cloud session installs the newest release.', inputSchema: { type: 'object', properties: { check: { type: 'boolean', description: 'True: report the versions and the provenance of the loaded copy, and install nothing.' } }, additionalProperties: false } },
|
|
78
78
|
];
|
|
79
79
|
|
|
80
80
|
// The Engram connection: the address and the key, in one setting, separated by
|
|
@@ -156,7 +156,7 @@ export const HOSTILE_NODE_VARIABLES = Object.freeze(['NODE_TLS_REJECT_UNAUTHORIZ
|
|
|
156
156
|
// that proxy. The owner accepted that proxy on 7 October 2026 ("Narrow
|
|
157
157
|
// exception"): the server accepts exactly these values, and only when the
|
|
158
158
|
// cloud's own marker is set. Any other value stays hostile, in the cloud too.
|
|
159
|
-
export
|
|
159
|
+
export { CLOUD_NODE_VARIABLES };
|
|
160
160
|
export const inClaudeCloud = (env) => env.CLAUDE_CODE_REMOTE === 'true';
|
|
161
161
|
export function hostileVariables(env) {
|
|
162
162
|
const cloud = inClaudeCloud(env);
|
|
@@ -246,16 +246,24 @@ const ATLAS_VERSION_OF = (root) => JSON.parse(readFileSync(join(root, 'package.j
|
|
|
246
246
|
function childExec(env) {
|
|
247
247
|
return (command, args) => execFileSync(command, args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 600_000, stdio: ['pipe', 'pipe', 'pipe'], env: childEnvironment(env) });
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
const PROOFS = new WeakMap();
|
|
250
|
+
export async function runUpgrade({ args = {}, env = process.env, ownerPresent, packageRoot = PACKAGE_ROOT, exec = childExec(env), upgradeFn = upgrade, proveFn = proveCopy } = {}) {
|
|
250
251
|
const refuse = (message) => { const error = new Error(message); error.name = 'VerbError'; throw error; };
|
|
251
|
-
if (!ownerPresent) refuse('atlas_upgrade needs the owner-present flag on the server host (ATLAS_WORK_OWNER_PRESENT=1)');
|
|
252
|
-
if (inClaudeCloud(env)) refuse('atlas_upgrade does not run in a Claude cloud session. A new cloud session installs the newest release.');
|
|
253
252
|
const loaded = ATLAS_VERSION_OF(packageRoot);
|
|
253
|
+
// The check reads only, so it runs in a cloud session and with the owner
|
|
254
|
+
// flag off (item 92cfbba3): a cloud session states its lag at its first reply.
|
|
254
255
|
if (args.check) {
|
|
255
256
|
let newest;
|
|
256
257
|
try { newest = exec('npm', ['view', '@arjunkhera/atlas', 'version']).trim(); } catch (error) { refuse(`npm could not read the newest version: ${String(error.stderr || error.message).trim().split('\n').slice(0, 3).join(' ')}`); }
|
|
257
|
-
|
|
258
|
+
// The loaded copy cannot change within a server's life, so one proof per
|
|
259
|
+
// copy: a repeated check makes no new GitHub reads.
|
|
260
|
+
if (!PROOFS.has(proveFn)) PROOFS.set(proveFn, new Map());
|
|
261
|
+
const proofs = PROOFS.get(proveFn);
|
|
262
|
+
if (!proofs.has(packageRoot)) proofs.set(packageRoot, proveFn({ packageRoot, exec }));
|
|
263
|
+
return { loaded, newest, behind: newest !== loaded, provenance: proofs.get(packageRoot) };
|
|
258
264
|
}
|
|
265
|
+
if (!ownerPresent) refuse('atlas_upgrade needs the owner-present flag on the server host (ATLAS_WORK_OWNER_PRESENT=1)');
|
|
266
|
+
if (inClaudeCloud(env)) refuse('atlas_upgrade does not run in a Claude cloud session. A new cloud session installs the newest release.');
|
|
259
267
|
const lines = [];
|
|
260
268
|
let code;
|
|
261
269
|
try { code = await upgradeFn({ packageRoot, options: { yes: true }, line: (text) => lines.push(text), env, exec }); } catch (error) {
|