@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,115 @@
|
|
|
1
|
+
// One helper change set becomes one pull request, with the fixed label and
|
|
2
|
+
// per-change provenance.
|
|
3
|
+
//
|
|
4
|
+
// Milestone M2, slice L2 stage one. The frozen sentence: "Helper pull requests
|
|
5
|
+
// carry the fixed label and per-change provenance." The rule behind it is R13
|
|
6
|
+
// of the M2 design study's review panel.
|
|
7
|
+
//
|
|
8
|
+
// The body is built by a pure function, so a test reads it without GitHub. The
|
|
9
|
+
// git and GitHub calls sit below it, in one place.
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { childEnvironment } from '../../work/lib/child-env.mjs';
|
|
12
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { checkChanges, HELPER_LABEL, PROVENANCE_SOURCES } from './helpers.mjs';
|
|
15
|
+
|
|
16
|
+
export { HELPER_LABEL, PROVENANCE_SOURCES };
|
|
17
|
+
|
|
18
|
+
const escapeCell = (text) => String(text ?? '').replaceAll('|', '\\|').replaceAll('\n', ' ').trim();
|
|
19
|
+
|
|
20
|
+
// One request may reach two helpers. "We now deploy prod on Fly" reaches Facts
|
|
21
|
+
// and Procedures. It is still ONE pull request: design page D2's rule, carried
|
|
22
|
+
// here (architecture section 5.1, rule 1).
|
|
23
|
+
export function checkAll(groups) {
|
|
24
|
+
if (!Array.isArray(groups) || !groups.length) throw new Error('a pull request needs at least one helper group');
|
|
25
|
+
const seen = new Map();
|
|
26
|
+
for (const group of groups) {
|
|
27
|
+
const helper = checkChanges(group.helperId, group.changes);
|
|
28
|
+
if (seen.has(helper.id)) throw new Error(`the ${helper.id} helper appears twice; put its changes in one group`);
|
|
29
|
+
seen.set(helper.id, { helper, changes: group.changes });
|
|
30
|
+
}
|
|
31
|
+
return [...seen.values()].sort((a, b) => a.helper.file - b.helper.file);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The pull request body. Every change is one row, and every row names where the
|
|
35
|
+
// change came from. A reader can tell an owner decision from a guess without
|
|
36
|
+
// opening the diff.
|
|
37
|
+
export function provenanceBody({ helper, helpers, why, changes, groups, repo = null }) {
|
|
38
|
+
const parts = groups ?? [{ helper, changes }];
|
|
39
|
+
const named = (helpers ?? parts.map((group) => group.helper));
|
|
40
|
+
const lines = [
|
|
41
|
+
named.length === 1
|
|
42
|
+
? `The \`${named[0].title}\` helper wrote this. It owns ${named[0].ownsText}.`
|
|
43
|
+
: `${named.length} helpers wrote this, and it is one pull request because it is one change:\n${named.map((one) => `- \`${one.title}\`, which owns ${one.ownsText}`).join('\n')}`,
|
|
44
|
+
'',
|
|
45
|
+
`**Why.** ${String(why ?? '').trim() || 'Not given.'}`,
|
|
46
|
+
'',
|
|
47
|
+
'## What changed, and where each change came from',
|
|
48
|
+
'',
|
|
49
|
+
'| File | Helper | Source | What the source says |',
|
|
50
|
+
'|---|---|---|---|',
|
|
51
|
+
];
|
|
52
|
+
for (const group of parts) {
|
|
53
|
+
for (const change of group.changes) {
|
|
54
|
+
lines.push(`| \`${escapeCell(change.path)}\` | ${escapeCell(group.helper.title)} | ${escapeCell(change.provenance.source)} | ${escapeCell(change.provenance.detail)} |`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
lines.push(
|
|
58
|
+
'',
|
|
59
|
+
'## How to read this',
|
|
60
|
+
'',
|
|
61
|
+
`1. Every file above is instruction class. It steers agents, so a person merges it${repo ? ` in ${repo}` : ''}.`,
|
|
62
|
+
'2. An agent made every change. No person edited these files by hand.',
|
|
63
|
+
`3. "Source" is one of: ${PROVENANCE_SOURCES.join(', ')}.`,
|
|
64
|
+
'',
|
|
65
|
+
`Label: \`${HELPER_LABEL}\`. Authority: Arjunkhera/atlas, \Atlas amendment A1.6\`, section 5, slice L2 stage one.`,
|
|
66
|
+
'',
|
|
67
|
+
);
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Write a checked change set into a working tree. It writes files and nothing
|
|
72
|
+
// else: no branch, no commit, no network. `open` below does the rest.
|
|
73
|
+
export function applyChanges(root, helperId, changes) {
|
|
74
|
+
const helper = checkChanges(helperId, changes);
|
|
75
|
+
const written = [];
|
|
76
|
+
for (const change of changes) {
|
|
77
|
+
const path = join(root, change.path);
|
|
78
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
79
|
+
writeFileSync(path, change.contents);
|
|
80
|
+
written.push(change.path);
|
|
81
|
+
}
|
|
82
|
+
return { helper, written };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const run = (command, args, cwd) => execFileSync(command, args, { cwd, encoding: 'utf8', timeout: 120_000, maxBuffer: 8 * 1024 * 1024, env: childEnvironment() }).trim();
|
|
86
|
+
|
|
87
|
+
// The whole pull request, live. Needs git and the GitHub command line.
|
|
88
|
+
//
|
|
89
|
+
// It never merges. A person merges every file a helper writes, because every
|
|
90
|
+
// one of them steers agents.
|
|
91
|
+
export function open({ root, helperId, changes, groups, why, branch, title, base = null, label = HELPER_LABEL, dryRun = false }) {
|
|
92
|
+
const wanted = groups ?? [{ helperId, changes }];
|
|
93
|
+
const checked = checkAll(wanted);
|
|
94
|
+
const all = checked.flatMap((group) => group.changes);
|
|
95
|
+
// A dry run writes nothing. The first version applied the changes and then
|
|
96
|
+
// printed "Nothing was pushed", which was true and misleading (acceptance
|
|
97
|
+
// review of batch 16, finding S3-5).
|
|
98
|
+
if (!dryRun) for (const group of checked) applyChanges(root, group.helper.id, group.changes);
|
|
99
|
+
const repo = (() => { try { return run('gh', ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner'], root); } catch { return null; } })();
|
|
100
|
+
const body = provenanceBody({ groups: checked, why, repo });
|
|
101
|
+
const onto = base ?? run('git', ['symbolic-ref', '--short', 'HEAD'], root);
|
|
102
|
+
const helper = checked[0].helper;
|
|
103
|
+
if (dryRun) return { helper, helpers: checked.map((group) => group.helper), repo, branch, title, body, base: onto, written: all.map((change) => change.path), opened: false };
|
|
104
|
+
|
|
105
|
+
run('git', ['checkout', '-b', branch], root);
|
|
106
|
+
run('git', ['add', '--', ...all.map((change) => change.path)], root);
|
|
107
|
+
run('git', ['commit', '-m', title, '-m', body], root);
|
|
108
|
+
run('git', ['push', '-u', 'origin', branch], root);
|
|
109
|
+
// A label that does not exist yet makes `gh pr create` fail, so it is made
|
|
110
|
+
// first. A label that already exists makes that call fail instead, and that
|
|
111
|
+
// failure is the expected one.
|
|
112
|
+
try { run('gh', ['label', 'create', label, '--description', 'Written by an Atlas helper. A person merges it.', '--color', 'C5A882'], root); } catch { /* it already exists */ }
|
|
113
|
+
const url = run('gh', ['pr', 'create', '--title', title, '--body', body, '--label', label, '--base', onto], root);
|
|
114
|
+
return { helper, helpers: checked.map((group) => group.helper), repo, branch, title, body, base: onto, written: all.map((change) => change.path), opened: true, url };
|
|
115
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The release list of the check: every copy of shape/check.mjs that a release
|
|
2
|
+
// of Atlas has shipped, by fingerprint (row F11, amendment A1.9).
|
|
3
|
+
//
|
|
4
|
+
// An installed package has no git history, so "behind" cannot be decided by
|
|
5
|
+
// git ancestry. This list decides it. The last entry is the check this package
|
|
6
|
+
// ships. An earlier entry is a check an earlier release shipped.
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { sha256 } from '../../shape/check.mjs';
|
|
10
|
+
|
|
11
|
+
export const RELEASES_PATH = 'shape/releases.json';
|
|
12
|
+
export const PACKAGE_CHECK_SOURCE = 'shape/check.mjs';
|
|
13
|
+
|
|
14
|
+
export function readReleases(packageRoot) {
|
|
15
|
+
const list = JSON.parse(readFileSync(join(packageRoot, RELEASES_PATH), 'utf8'));
|
|
16
|
+
if (!Array.isArray(list.releases) || !list.releases.length) throw new Error(`${RELEASES_PATH} lists no release`);
|
|
17
|
+
for (const row of list.releases) {
|
|
18
|
+
if (!Number.isInteger(row.check_version) || !/^[0-9a-f]{64}$/.test(row.sha256 ?? '') || typeof row.first_package !== 'string') {
|
|
19
|
+
throw new Error(`${RELEASES_PATH} holds a malformed row: ${JSON.stringify(row)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return list.releases;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function packagePackageVersion(packageRoot) {
|
|
26
|
+
return JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).version;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Where one fingerprint sits in the list: the row, and whether it is the last.
|
|
30
|
+
export function placeOf(releases, fingerprint) {
|
|
31
|
+
const index = releases.findIndex((row) => row.sha256 === fingerprint);
|
|
32
|
+
if (index < 0) return { row: null, latest: false };
|
|
33
|
+
return { row: releases[index], latest: index === releases.length - 1 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The check this package would put in a repo, and where it sits in the list.
|
|
37
|
+
export function packagedCheck(packageRoot) {
|
|
38
|
+
const text = readFileSync(join(packageRoot, PACKAGE_CHECK_SOURCE), 'utf8');
|
|
39
|
+
const releases = readReleases(packageRoot);
|
|
40
|
+
const fingerprint = sha256(text);
|
|
41
|
+
return { text, sha256: fingerprint, releases, ...placeOf(releases, fingerprint) };
|
|
42
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The session verb: is this repository's copy of the check current, behind,
|
|
2
|
+
// unknown or missing?
|
|
3
|
+
//
|
|
4
|
+
// Milestone M2, slice L2 stage one, done line 2: "The session verb names a
|
|
5
|
+
// stale copy by hash." Row F11 (amendment A1.9) changes how: the answer comes
|
|
6
|
+
// from the release list the package carries, shape/releases.json, and never
|
|
7
|
+
// from git history or the package version.
|
|
8
|
+
//
|
|
9
|
+
// current the copy's fingerprint is the last row of the list
|
|
10
|
+
// behind the copy's fingerprint is an earlier row
|
|
11
|
+
// unknown the fingerprint is in no row: edited by hand, or from elsewhere
|
|
12
|
+
// missing the repo has no copy
|
|
13
|
+
//
|
|
14
|
+
// CI never makes this judgement. A check with no network cannot learn the
|
|
15
|
+
// package's list. This runs in a session, with the package on disk.
|
|
16
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { CHECK_PATH, checkStamps, sha256 } from '../../shape/check.mjs';
|
|
19
|
+
import { packagedCheck, placeOf } from './releases.mjs';
|
|
20
|
+
|
|
21
|
+
export const STATES = Object.freeze(['current', 'behind', 'unknown', 'missing']);
|
|
22
|
+
|
|
23
|
+
export function status({ root, packageRoot }) {
|
|
24
|
+
const pkg = packagedCheck(packageRoot);
|
|
25
|
+
const latest = pkg.releases[pkg.releases.length - 1];
|
|
26
|
+
const packaged = { sha256: pkg.sha256, check_version: checkStamps(pkg.text).check_version, released: Boolean(pkg.row), latest: pkg.latest };
|
|
27
|
+
const full = join(root, CHECK_PATH);
|
|
28
|
+
if (!existsSync(full)) {
|
|
29
|
+
return { root, path: CHECK_PATH, state: 'missing', packaged, copy: null, why: `${CHECK_PATH} does not exist in this repo, so it runs no check of its own. "atlas tooling pr" adds it and its GitHub workflow.` };
|
|
30
|
+
}
|
|
31
|
+
const text = readFileSync(full, 'utf8');
|
|
32
|
+
const fingerprint = sha256(text);
|
|
33
|
+
const { row, latest: isLatest } = placeOf(pkg.releases, fingerprint);
|
|
34
|
+
const copy = { sha256: fingerprint, check_version: row?.check_version ?? checkStamps(text).check_version, first_package: row?.first_package ?? null };
|
|
35
|
+
if (row && isLatest) {
|
|
36
|
+
return { root, path: CHECK_PATH, state: 'current', packaged, copy, why: `the copy is check version ${row.check_version}, the newest one Atlas has released (first in Atlas ${row.first_package}).` };
|
|
37
|
+
}
|
|
38
|
+
if (row) {
|
|
39
|
+
return { root, path: CHECK_PATH, state: 'behind', packaged, copy, why: `the copy is check version ${row.check_version}, first released in Atlas ${row.first_package}. The newest is check version ${latest.check_version}, from Atlas ${latest.first_package}. "atlas tooling pr" refreshes it.` };
|
|
40
|
+
}
|
|
41
|
+
return { root, path: CHECK_PATH, state: 'unknown', packaged, copy, why: `the copy's fingerprint, sha256 ${fingerprint.slice(0, 12)}…, is in no release of the check. Someone edited it, or it came from an unreleased package. "atlas tooling pr --replace-unknown" shows the difference and replaces it.` };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The same question across several repositories.
|
|
45
|
+
export function statusMany(roots, packageRoot) {
|
|
46
|
+
return roots.map((root) => { try { return status({ root, packageRoot }); } catch (error) { return { root, state: 'unknown', why: error.message, packaged: null, copy: null }; } });
|
|
47
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The tooling helper. It writes and refreshes the two files Atlas puts in a
|
|
2
|
+
// repository, and it needs no model and no network to do it.
|
|
3
|
+
//
|
|
4
|
+
// Milestone M2, slice L2 stage one, done line 1: "The tooling helper writes
|
|
5
|
+
// both files into a repository with no hand step."
|
|
6
|
+
//
|
|
7
|
+
// Both files come from the package: the check is copied byte for byte from
|
|
8
|
+
// shape/check.mjs, and the workflow is written from the WORKFLOW_TEMPLATE
|
|
9
|
+
// constant inside that same file. One place writes both, so they can never
|
|
10
|
+
// disagree about the path.
|
|
11
|
+
//
|
|
12
|
+
// Row F11 (amendment A1.9): a refresh never moves a repo backwards and never
|
|
13
|
+
// plants a check no release has shipped. It writes only when the repo's copy
|
|
14
|
+
// is missing or behind, or unknown with --replace-unknown, and only a check
|
|
15
|
+
// that is the newest row of the release list. The Atlas repo itself may take
|
|
16
|
+
// its own unreleased check, because that is how a new check is made.
|
|
17
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { CHECK_PATH, WORKFLOW_PATH, WORKFLOW_TEMPLATE, CHECK_VERSION, sha256, parseYaml } from '../../shape/check.mjs';
|
|
21
|
+
import { packagedCheck, packagePackageVersion, PACKAGE_CHECK_SOURCE } from './releases.mjs';
|
|
22
|
+
import { status } from './status.mjs';
|
|
23
|
+
|
|
24
|
+
export { CHECK_PATH, WORKFLOW_PATH, PACKAGE_CHECK_SOURCE };
|
|
25
|
+
|
|
26
|
+
export class ToolingRefusal extends Error {
|
|
27
|
+
constructor(message, { current = false } = {}) { super(message); this.current = current; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// What the package would put in a repository right now.
|
|
31
|
+
export function packagedFiles(packageRoot) {
|
|
32
|
+
return {
|
|
33
|
+
[CHECK_PATH]: readFileSync(join(packageRoot, PACKAGE_CHECK_SOURCE), 'utf8'),
|
|
34
|
+
[WORKFLOW_PATH]: WORKFLOW_TEMPLATE,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Is this repository the one the package is built from? Two things must
|
|
39
|
+
// agree: the repo named in atlas.yaml, and the git remote of the checkout.
|
|
40
|
+
// Either alone is one line anyone can write; the harm of a false claim stays
|
|
41
|
+
// in that one repository.
|
|
42
|
+
const githubName = (url) => /github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?\/?$/.exec(String(url ?? '').trim())?.[1] ?? null;
|
|
43
|
+
export function isPackageRepo(root, packageRoot) {
|
|
44
|
+
const own = githubName(JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).repository?.url);
|
|
45
|
+
const file = join(root, 'atlas.yaml');
|
|
46
|
+
if (!own || !existsSync(file)) return false;
|
|
47
|
+
let named = null;
|
|
48
|
+
try { named = parseYaml(readFileSync(file, 'utf8'))?.repo ?? null; } catch { return false; }
|
|
49
|
+
if (named !== own) return false;
|
|
50
|
+
try {
|
|
51
|
+
const remote = execFileSync('git', ['-C', root, 'config', '--get', 'remote.origin.url'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
52
|
+
return githubName(remote) === own;
|
|
53
|
+
} catch { return false; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// May the helper write into this repository? Returns nothing, or throws a
|
|
57
|
+
// refusal that says what to do instead.
|
|
58
|
+
const readWorkflow = (root) => { const full = join(root, WORKFLOW_PATH); return existsSync(full) ? readFileSync(full, 'utf8') : null; };
|
|
59
|
+
|
|
60
|
+
export function mayWrite(root, packageRoot, { replaceUnknown = false, workflowOnly = false } = {}) {
|
|
61
|
+
const pkg = packagedCheck(packageRoot);
|
|
62
|
+
const own = isPackageRepo(root, packageRoot);
|
|
63
|
+
if (!pkg.latest && !own) {
|
|
64
|
+
throw new ToolingRefusal(`this package's ${PACKAGE_CHECK_SOURCE} is ${pkg.row ? 'not the newest release' : 'in no release'} of the check, so it is not written into another repo. Use a released Atlas.`);
|
|
65
|
+
}
|
|
66
|
+
const now = status({ root, packageRoot });
|
|
67
|
+
if (now.state === 'current' && !workflowOnly) {
|
|
68
|
+
throw new ToolingRefusal(`this repo's check is already current. ${readWorkflow(root) === WORKFLOW_TEMPLATE ? 'Both files match the package, so there is nothing to write.' : 'Its workflow file differs from the package; pass --workflow to rewrite only that file.'}`, { current: true });
|
|
69
|
+
}
|
|
70
|
+
if (now.state === 'unknown' && !replaceUnknown && !own) {
|
|
71
|
+
throw new ToolingRefusal(`${now.why}`);
|
|
72
|
+
}
|
|
73
|
+
return now;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Write both files into a repository. Returns one row per file saying whether
|
|
77
|
+
// it was written new, refreshed, or already the same. Safe to run again.
|
|
78
|
+
export function write(root, packageRoot, { dryRun = false, replaceUnknown = false, workflowOnly = false } = {}) {
|
|
79
|
+
const before = dryRun ? null : mayWrite(root, packageRoot, { replaceUnknown, workflowOnly });
|
|
80
|
+
const all = packagedFiles(packageRoot);
|
|
81
|
+
const wanted = workflowOnly ? { [WORKFLOW_PATH]: all[WORKFLOW_PATH] } : all;
|
|
82
|
+
const rows = [];
|
|
83
|
+
for (const [path, contents] of Object.entries(wanted)) {
|
|
84
|
+
const full = join(root, path);
|
|
85
|
+
const was = existsSync(full) ? readFileSync(full, 'utf8') : null;
|
|
86
|
+
const state = was === null ? 'written' : was === contents ? 'unchanged' : 'refreshed';
|
|
87
|
+
if (state !== 'unchanged' && !dryRun) {
|
|
88
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
89
|
+
writeFileSync(full, contents);
|
|
90
|
+
}
|
|
91
|
+
rows.push({ path, state, sha256: sha256(contents), was: was === null ? null : sha256(was) });
|
|
92
|
+
}
|
|
93
|
+
if (!dryRun && rows.every((row) => row.state === 'unchanged')) {
|
|
94
|
+
throw new ToolingRefusal('this repo is already current: both files match the package, so there is nothing to write.', { current: true });
|
|
95
|
+
}
|
|
96
|
+
return { rows, version: packagePackageVersion(packageRoot), check_version: CHECK_VERSION, status: before?.state ?? null, changed: rows.some((row) => row.state !== 'unchanged') };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The change set the pull request machinery takes, for the two files above.
|
|
100
|
+
export function changes(root, packageRoot, { replaceUnknown = false, workflowOnly = false } = {}) {
|
|
101
|
+
mayWrite(root, packageRoot, { replaceUnknown, workflowOnly });
|
|
102
|
+
const { rows, version } = write(root, packageRoot, { dryRun: true, workflowOnly });
|
|
103
|
+
if (rows.every((row) => row.state === 'unchanged')) {
|
|
104
|
+
throw new ToolingRefusal('this repo is already current: both files match the package, so there is nothing to open.', { current: true });
|
|
105
|
+
}
|
|
106
|
+
const wanted = packagedFiles(packageRoot);
|
|
107
|
+
return rows
|
|
108
|
+
.filter((row) => row.state !== 'unchanged')
|
|
109
|
+
.map((row) => ({
|
|
110
|
+
path: row.path,
|
|
111
|
+
contents: wanted[row.path],
|
|
112
|
+
provenance: {
|
|
113
|
+
source: 'repo read',
|
|
114
|
+
detail: row.path === CHECK_PATH
|
|
115
|
+
? `copied byte for byte from ${PACKAGE_CHECK_SOURCE}, check version ${CHECK_VERSION}, in Atlas ${version}`
|
|
116
|
+
: `written from the WORKFLOW_TEMPLATE constant in ${PACKAGE_CHECK_SOURCE}, Atlas ${version}`,
|
|
117
|
+
},
|
|
118
|
+
}));
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arjunkhera/atlas",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, as a Claude Code plugin for any repository.",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"license": "UNLICENSED",
|
|
6
|
-
"repository": {
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/Arjunkhera/atlas.git",
|
|
10
|
+
"directory": "packages/atlas"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"atlas": "bin/atlas"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"door/",
|
|
21
|
+
"work/",
|
|
22
|
+
"shape/",
|
|
23
|
+
"skills/",
|
|
24
|
+
"agents/",
|
|
25
|
+
".claude-plugin/",
|
|
26
|
+
".mcp.json"
|
|
27
|
+
]
|
|
7
28
|
}
|