@assuredloop/cli 0.0.1 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wenqing Gu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,313 @@
1
- # @assuredloop/cli
1
+ # AssuredLoop
2
2
 
3
- Placeholder. This name is reserved for AssuredLoop's command-line tool, `al`.
3
+ AssuredLoop keeps an AI focused on one change. It holds together:
4
+ - the owner's words, as snapshots;
5
+ - the organized requirement that the owner signs;
6
+ - the consolidated spec in `specs/`: the system now, the promises and the
7
+ lasting design, each paragraph with a hidden ID;
8
+ - each change's own design, its change spec, beside its request;
9
+ - the decisions and the ADRs;
10
+ - the code, the tests and the documents that came with the change.
4
11
 
5
- The first real release will be published from this project's GitHub Actions
6
- release workflow. Until then, this package contains no code.
12
+ Its command, `al`, keeps these records, checks them, and says what to do
13
+ next. It refuses only in its own `al conclude`. Every other check is a hint;
14
+ a project that wants blocks turns on `al check --strict` in its CI.
15
+
16
+ AssuredLoop is not a lifecycle tool. It has no phases, no roles, no task
17
+ states and no approvals beyond the owner's sign-off. How and when you design,
18
+ test and review is your project's way of working.
19
+
20
+ ## Install
21
+
22
+ Version 0.2.0 is v4. It is a breaking release: it does not read the records
23
+ of 0.1.0. To move a project from 0.1.0, see "From 0.1.0" in "Adopting
24
+ AssuredLoop".
25
+
26
+ It needs Node 24 or newer and git 2.31 or newer. Install the npm package
27
+ `@assuredloop/cli`, which puts `al` on your PATH:
28
+
29
+ npm install --global @assuredloop/cli
30
+
31
+ The recommended install adds the optional package `@assuredloop/search`
32
+ beside it, for search at level 2 (see "Search"):
33
+
34
+ npm install --global @assuredloop/cli @assuredloop/search
35
+
36
+ To pin a version, name it: `npm install --global @assuredloop/cli@<version>`.
37
+ Give both packages the same version.
38
+
39
+ The package carries the skill, at
40
+ `$(npm root -g)/@assuredloop/cli/skills/assuredloop/SKILL.md`. Add one line to
41
+ the project's AGENTS.md:
42
+
43
+ ```
44
+ AssuredLoop keeps this project's requests and spec: before work that changes the spec, read <dir>/skills/assuredloop/SKILL.md and run `al context`.
45
+ ```
46
+
47
+ In a shared repo whose contributors install `al` in different places, copy
48
+ the skill at the pinned version into the project, for example as
49
+ `.claude/skills/assuredloop/SKILL.md`. Name that path in the AGENTS.md line,
50
+ and say in the commit which version it came from.
51
+
52
+ Search at level 2 needs the optional package `@assuredloop/search` (see
53
+ "Search"). Install it in the project, or beside `al`.
54
+
55
+ ## The model in one page
56
+
57
+ - **Paragraphs and markers.** Every paragraph of a spec doc has an ID in a
58
+ hidden HTML comment on its own line before it, with a blank line before and
59
+ after it. The marker holds the ID, the kind and the links:
60
+
61
+ ```markdown
62
+ <!-- INV-41 rule serves:invoice-exports/R2 builds-on:INV-12 -->
63
+
64
+ The export link MUST expire 30 minutes after the email is sent.
65
+ ```
66
+
67
+ GitHub does not show the marker. The ID is one flat number per file, and
68
+ it is never used again. `al` computes a display number, for example
69
+ `INV-41 (2.3:4, in Export links)`, which can change when text moves.
70
+ - **Kinds.** Promise kinds (purpose, scope, rule, limit, definition): a
71
+ change needs a signed requirement that covers it. Design kinds (component,
72
+ interface, data, flow, choice) and `approach` (how one change is built):
73
+ a change needs review. Informative kinds (rationale, example, open), `note`
74
+ (headings and connecting text), and the change-only kinds (plan, step,
75
+ migration).
76
+ - **A request** is the folder `requests/<name>/`: `origin/` (the owner's words
77
+ and the sign-offs, as snapshots with SHA-256), `request.md` (the dialog, the
78
+ organized requirement R1…, the decisions D1…) and, from path 2, `spec.md`,
79
+ the change spec, with IDs SP-1, SP-2…
80
+ - **Records** in `.assuredloop/records/`, committed to git: one per request
81
+ (its sources, requirement versions, sign-offs, decisions, tasks, the
82
+ bindings of its links and the dispositions of its paragraphs) and one per
83
+ doc (fields that `al index` can always rebuild). `al` writes every hash and
84
+ every derived field.
85
+ - **Bindings.** When a link is first indexed, `al index` records the hash of
86
+ both ends. Ordinary indexing never moves a binding. When the target
87
+ changes, the source gets a hint until someone checks it and runs
88
+ `al index --align <ID>`.
89
+ - **Consolidation is by hand.** A PR copies a change paragraph into `specs/`
90
+ word for word, and records its disposition: `incorporated`, `removed`,
91
+ `superseded` or `abandoned`. No command writes the spec.
92
+
93
+ ## Paths
94
+
95
+ The user picks the path. The skill has the steps of each path.
96
+
97
+ | Path | When | Records | Sign-off | Closes when |
98
+ |---|---|---|---|---|
99
+ | 0. Fix | no promise changes: it restores what the spec says, or claims a typo in any paragraph | none | no | the PR merges |
100
+ | 1. Small promise amend | one or a few promise paragraphs change | a short request; the PR edits `specs/` directly | yes | the PR merges |
101
+ | 1d. Small design correction | names or wording of design paragraphs only | a short request with a one-line source note; the PR edits `specs/` directly | no | the PR merges |
102
+ | 2. Big change | a real design, one or a few PRs | a request and a change spec | if a promise kind changes | the close rule holds |
103
+ | 3. Epic | big, in parts over time | as 2, with optional task references | as 2 | the close rule holds |
104
+ | S. Spike | not yet clear what to build | a request with an organized question; the findings open with the answer | yes, on the question | the answer names the question version |
105
+
106
+ Every PR states its path and its claim in a commit message, for example
107
+ `Tier: 0 — typo in INV-3` or `Tier: 2 — invoice-payments, part 1`. `al check`
108
+ reads the newest `Tier:` line in the branch's commits, and checks the claim
109
+ against the kinds and the changed paragraphs: "path 0, but a rule paragraph
110
+ is changed with no typo claim" is `not ok`. It checks the labels, not their
111
+ meaning.
112
+
113
+ **The close rule.** `al conclude <name>` refuses while a promise change has
114
+ no sign-off, a spike's current question is not signed, or a change paragraph
115
+ with a baseline effect has no valid disposition for its current version.
116
+
117
+ ## The commands
118
+
119
+ - `al new <name> --from <file|-> [--tier <1|1d|2|3|S>] [--title <text>]`:
120
+ start a request from the owner's words (`-` reads standard input).
121
+ - `al record <name> origin --url <source> --from <file|->`: a snapshot of
122
+ later words of the owner, or of a source you rely on.
123
+ - `al record <name> signoff --source <where> --words <their words>`: show what
124
+ the owner signs; with `--yes`, record the sign-off on the owner's own yes.
125
+ - `al record <name> decision --source <who> --text <decision>
126
+ [--clarifies R<n>]`: add D1, D2…
127
+ - `al spec`: every doc's IDs, display numbers and kinds.
128
+ `al spec --add-ids <file> [--prefix <P>]`: give each unmarked paragraph the
129
+ next free ID.
130
+ - `al index`: write the derived facts and the new bindings, and list the AI
131
+ hints to refresh. `al index --align <ID>` moves the bindings of one
132
+ paragraph to its targets' current text, on purpose.
133
+ - `al check [--strict]`: the checks of this branch against its base.
134
+ - `al context [<name> | <ID>]`: where the project, a request or a paragraph
135
+ stands. `--diff <range> --for review` is the review view. `--audit` gives a
136
+ request's whole trace; `--at <commit>` any v4 state.
137
+ - `al conclude <name>`: the close rule; with `--yes` it writes the Outcome
138
+ and moves the request to `requests/archive/<name>/`.
139
+ - `al export [--at <commit>] [--out <file>]`: one JSONL row per paragraph
140
+ version, with its role, version, commit and links. It is not committed.
141
+ - `al search <words>`: see "Search".
142
+
143
+ Every command that changes files, except `new` and `index`, shows what it
144
+ would write, and writes only with `--yes`. Each output ends with three lines:
145
+ `Read` (what it read), `Next` (what to do next) and `Not known` (what the
146
+ tool cannot know).
147
+
148
+ ## Settings
149
+
150
+ `.assuredloop/config.yaml`, at the repo's root. Every key is optional.
151
+
152
+ ```yaml
153
+ root: specs # the spec folder; every *.md in it except adr/ is a spec doc
154
+ docs: # each file and its ID prefix; other files may be listed too
155
+ - {file: specs/invoices.md, prefix: INV}
156
+ results: .assuredloop/results # where result files are
157
+ outputs: # in the central repo: its output repos
158
+ - {name: invoicer-web, path: ../invoicer-web, commit: <sha>}
159
+ central: {path: ../invoicer} # in an output repo: the central repo
160
+ ```
161
+
162
+ `.assuredloop/schema.yaml` holds the kinds and the link words, with a version.
163
+ `al spec --add-ids` writes both files when they are absent.
164
+
165
+ ADRs are `specs/adr/NNNN-<slug>.md`, with `Status: proposed`, `accepted` or
166
+ `superseded` on the first line. The heading's marker holds `ADR-<n>` and its
167
+ `decides:`, `source:` and `supersedes:` links. The text of an accepted ADR
168
+ does not change; a new ADR supersedes it.
169
+
170
+ ## Search
171
+
172
+ `al search` answers at the strongest level that is installed, falls back by
173
+ itself (2, then 1, then 0), and says which level answered and why.
174
+
175
+ | Level | What it is | Needs |
176
+ |---|---|---|
177
+ | 0 | a scan of the export | nothing |
178
+ | 1 | a local full-text index (SQLite FTS5, BM25), with the ID in its own exact column | Node 24 or newer (`node:sqlite`) |
179
+ | 2 | level 1, plus a small local embedding model; word and vector results are fused | the package `@assuredloop/search` |
180
+
181
+ - The default query is the current system: the spec text at the selected
182
+ commit. `--change <name>` adds that request's open change spec, its
183
+ sources and its evidence. `--history` adds removed, abandoned and replaced
184
+ text, marked as history.
185
+ - `--id <ID>` finds one paragraph by its exact ID, never through search.
186
+ `--level <0|1|2>` asks for a lower level; `--at <commit>` another commit.
187
+ - Each hit shows its role (`baseline`, `proposal`, `spike`, `source`,
188
+ `evidence` or `history`), its version and its commit.
189
+ - In the central repo, search covers the output repos that its committed
190
+ config lists, each at its resolved commit. The `Repos` line names each repo
191
+ and its commit, for example
192
+ `Repos invoicer@a6ac3a7785dd invoicer-web@5aa04042117b`. An output repo
193
+ gives `evidence` rows: its result files, and the files that cite a central
194
+ ID or that a record declares. Such a hit shows `<repo>:<path>`, and each
195
+ line that names a central ID, with its line number. A repo that is absent,
196
+ or a commit that cannot be found, gives no rows and a `Not known` line with
197
+ the reason.
198
+ - The index is one SQLite file inside `.git`, never committed. `al search`
199
+ brings it up to date before it answers; `--rebuild` builds it again.
200
+ - Level 2 runs a small local model (bge-small-en-v1.5, pinned by its
201
+ revision). The model download is the only data that leaves the machine.
202
+ `al` itself makes no network call.
203
+
204
+ ## One central repo, and output repos
205
+
206
+ The central repo holds `specs/`, the ADRs, the requests and the records. Its
207
+ config lists the output repos. An output repo holds code, tests and user
208
+ documents only; its config names the central repo.
209
+
210
+ - Code and tests in an output repo cite central IDs with a qualifier:
211
+ `central:INV-41`, `central:invoice-exports/R3`. A fix there says
212
+ `Tier: 0 — restores central:INV-41`.
213
+ - `al` reads another repo only through git, at the commit that config pins
214
+ or at the clone's HEAD, and never fetches. A repo or a commit that is not
215
+ there is "unknown", never a guess.
216
+ - In an output repo, `al check` looks up the central IDs that the branch's
217
+ changed files cite.
218
+
219
+ ## Adopting AssuredLoop
220
+
221
+ 1. Put the spec under the root (`specs/` by default), or set `root:`.
222
+ 2. Mark every paragraph: `al spec --add-ids specs/<file>.md --prefix <P> --yes`
223
+ for each spec file. Headings get the kind `note`. Write the kind of each
224
+ other paragraph into its marker. An adopted paragraph needs no link.
225
+ 3. Add the AGENTS.md line from "Install". Claim
226
+ `Tier: 0 — adds paragraph IDs; no promise changes`.
227
+ 4. `al spec --add-ids --yes` also records where the adopted text came from.
228
+ For each paragraph it marks whose text is a block of the file at HEAD, in
229
+ the same place, it writes an entry with HEAD's commit and the text's hash
230
+ to `.assuredloop/records/requests/adoption.yaml`. It writes
231
+ `requests/archive/adoption/request.md` when that file is absent. So commit
232
+ the spec unmarked before you mark it. A paragraph whose text is not at
233
+ HEAD is not adopted: the command names it, and it follows its path as a
234
+ change. `source: adoption` says where the text came from. It is not an
235
+ owner approval, and it does not excuse a later change.
236
+ 5. Run `al index`, and commit the markers and the records together. On that
237
+ branch, `al check --strict` reads the adopted paragraphs as unchanged, so
238
+ they need no sign-off and no path claim.
239
+
240
+ **A sign-off bound to the whole text.** Markers change a file's bytes. If a
241
+ signed requirement, or any other record, binds the whole text of a spec file
242
+ by its SHA-256, that hash no longer matches after the markers are added. Ask
243
+ the owner for a new approval after the conversion. The paragraph text itself
244
+ does not change; `al` hashes each paragraph without its marker.
245
+
246
+ **From 0.1.0.** v4 (0.2.0) does not read 0.1.0 records. Keep 0.1.0 pinned
247
+ until you move. Older records stay readable as files; run the pinned 0.1.0 at
248
+ those commits. To move:
249
+
250
+ 1. Finish each open 0.1.0 request on its pinned copy, or start it again
251
+ under v4.
252
+ 2. Install 0.2.0 (see "Install").
253
+ 3. Run `al spec --add-ids`, as in steps 1-5 above. It marks every paragraph
254
+ and writes the adoption record.
255
+
256
+ ## What is enforced, and by whom
257
+
258
+ | The tool checks | The skill asks | The owner decides |
259
+ |---|---|---|
260
+ | `al conclude` refuses on its three conditions (see "The close rule") | write the owner's words and the organized requirement, and ask for the sign-off | what is wanted, by signing the organized requirement |
261
+ | IDs: missing, used twice, lost, used again; kinds and links; links that do not resolve; sign-off coverage of promise changes; path and typo claims; dispositions and exact equality; stale bindings; overlapping changes; ADR text changes; result applicability, as hints | choose the path, and state it in a commit message and the PR | whether a changed requirement is signed again |
262
+ | `check --strict` exits 1 on a `not ok`, when a project opts in | record each later decision, with its source | decisions whose source is the owner, such as dropping or keeping work |
263
+
264
+ `al` checks that declarations are well formed and that references resolve.
265
+ It does not check that a kind is right, that a requirement is fully
266
+ covered, that a test checks the rule it names, or that a change is
267
+ authorized in substance; its views say "not checked" for these. Nothing is
268
+ installed in git hooks.
269
+
270
+ ## Beyond code: results
271
+
272
+ A result file in `.assuredloop/results/` (or the `results:` folder) records a
273
+ check, a test run or a review:
274
+
275
+ ```yaml
276
+ check: test/export-link.test.js
277
+ outcome: pass # pass, fail or not run
278
+ commit: <sha> # or unknown
279
+ inputs:
280
+ - {file: src/export-link.js, sha256: <sha256 of its bytes>}
281
+ by: ci # optional, with source and note
282
+ ```
283
+
284
+ `al` shows "declared inputs unchanged since <commit>" when the inputs still
285
+ have those hashes, "does not apply to the current text" when one changed,
286
+ and "applicability unknown" when it cannot tell. That the declared inputs
287
+ are complete is a claim. The same works for a repo of documents: the report
288
+ is the deliverable, and a review result names it as its input.
289
+
290
+ ## This repository
291
+
292
+ - `requests/`: the requests, and `requests/archive/` for the concluded ones;
293
+ - `docs/adr/`: the decision records;
294
+ - `specs/`: the current spec.
295
+
296
+ The previous version is on the `legacy` branch. Its build is tagged
297
+ `assuredloop-base-0.1.0-canonical-build`, and its issues and pull requests stay
298
+ in this repository.
299
+
300
+ ## Releasing
301
+
302
+ 1. Bump `version` in `package.json` and in `packages/search/package.json` to
303
+ the same version in a PR, and merge it.
304
+ 2. Publish a GitHub Release tagged `v<version>` on that commit. Mark it a
305
+ pre-release to publish under npm's `next` tag.
306
+
307
+ `.github/workflows/publish.yml` does the rest: it runs the tests and
308
+ `al check --strict`, checks that the tag matches both `package.json` files,
309
+ and publishes @assuredloop/cli, then @assuredloop/search, to npm. It skips a
310
+ package whose version is already on npm, so a failed run can run again. It
311
+ stores no npm token: npm trusts the workflow by name, and adds a provenance
312
+ statement saying which commit built the package. npm must trust the workflow
313
+ for each package before its first publish.
package/bin/al.js ADDED
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ // al, the v4 command (T17 made it the one in package.json's bin).
3
+ import { parseArgs } from 'node:util';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { readFileSync, realpathSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { check } from '../src/v4/check.js';
8
+ import { Fail, spec } from '../src/v4/spec.js';
9
+ import { newRequest, record } from '../src/v4/commands.js';
10
+ import { index } from '../src/v4/indexer.js';
11
+ import { exportCommand } from '../src/v4/export.js';
12
+ import { search } from '../src/v4/search.js';
13
+ import { context } from '../src/v4/views.js';
14
+ import { conclude } from '../src/v4/conclude.js';
15
+
16
+ // positionals: how many positional arguments a command uses, or a function of
17
+ // its options that says so; any more are refused. None given: it uses them all.
18
+ const COMMANDS = {
19
+ new: { run: newRequest, positionals: 1, options: { from: { type: 'string' }, title: { type: 'string' }, tier: { type: 'string' } } },
20
+ record: {
21
+ run: record,
22
+ positionals: 2,
23
+ options: {
24
+ url: { type: 'string' }, from: { type: 'string' }, fetched: { type: 'string' }, yes: { type: 'boolean' },
25
+ source: { type: 'string' }, words: { type: 'string' }, presented: { type: 'string' }, 'transcribed-by': { type: 'string' },
26
+ text: { type: 'string' }, clarifies: { type: 'string' },
27
+ },
28
+ },
29
+ index: { run: index, positionals: 0, options: { align: { type: 'string' } } },
30
+ spec: { run: spec, positionals: (o) => (o['add-ids'] === undefined ? Infinity : 0), options: { 'add-ids': { type: 'string' }, prefix: { type: 'string' }, yes: { type: 'boolean' } } },
31
+ check: { run: check, positionals: 0, options: { strict: { type: 'boolean' } } },
32
+ export: { run: exportCommand, positionals: 0, options: { at: { type: 'string' }, out: { type: 'string' } } },
33
+ search: {
34
+ run: search,
35
+ options: {
36
+ id: { type: 'string' }, change: { type: 'string' }, history: { type: 'boolean' }, level: { type: 'string' }, at: { type: 'string' },
37
+ limit: { type: 'string' }, section: { type: 'boolean' }, rebuild: { type: 'boolean' }, json: { type: 'boolean' },
38
+ },
39
+ },
40
+ context: { run: context, options: { diff: { type: 'string' }, for: { type: 'string' }, audit: { type: 'boolean' }, at: { type: 'string' } } },
41
+ conclude: { run: conclude, options: { yes: { type: 'boolean' } } },
42
+ };
43
+ const USAGE = `al <${Object.keys(COMMANDS).join('|')}|--version> [options]`;
44
+
45
+ const line = (label, text) => `${label.padEnd(10)}${text}`;
46
+ // Every output line is one line: control characters but a tab show as \xNN.
47
+ const shown = (l) => String(l).replace(/[\x00-\x08\x0a-\x0c\x0e-\x1f\x7f-\x9f]|\r(?!$)/g, (c) => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
48
+ const print = (body, read, next, notKnown) => process.stdout.write(
49
+ [...body, line('Read', read), line('Next', next), line('Not known', notKnown.join('; ') || 'nothing beyond what is shown')].map(shown).join('\n') + '\n');
50
+
51
+ function topLevel(cwd) {
52
+ try {
53
+ return execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
54
+ } catch {
55
+ throw new Fail('not in a git repository', 'run it inside a git checkout');
56
+ }
57
+ }
58
+
59
+ // `al --version`: the package version, and the commit of the install folder
60
+ // when that folder is the top of a git checkout, so a run says what it runs.
61
+ function version() {
62
+ const dir = realpathSync(fileURLToPath(new URL('..', import.meta.url)));
63
+ const pkg = JSON.parse(readFileSync(`${dir}/package.json`, 'utf8'));
64
+ // The caller's GIT_DIR and the like point at the caller's repo, not this folder.
65
+ const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => !k.startsWith('GIT_')));
66
+ const git = (args) => {
67
+ try {
68
+ return execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', env, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
69
+ } catch {
70
+ return null;
71
+ }
72
+ };
73
+ const top = git(['rev-parse', '--show-toplevel']);
74
+ const sha = top && realpathSync(top) === dir ? git(['rev-parse', 'HEAD']) : null;
75
+ const where = sha ? `${sha.slice(0, 7)} · ${dir}` : `not a git checkout: ${dir}`;
76
+ print([`al ${pkg.version ?? 'unreleased'} · ${where}`], 'the install folder · no project read', 'al context',
77
+ [sha ? 'whether the install folder has uncommitted changes' : 'which commit it was installed from']);
78
+ return 0;
79
+ }
80
+
81
+ async function main(argv) {
82
+ const cwd = process.cwd();
83
+ let read = 'nothing';
84
+ if (argv.length === 1 && ['--version', '-v'].includes(argv[0])) return version();
85
+ try {
86
+ const top = topLevel(cwd);
87
+ read = 'working tree';
88
+ const [name, ...rest] = argv;
89
+ const command = COMMANDS[name];
90
+ if (!command) throw new Fail(name ? `unknown command: ${name}` : 'no command given', USAGE);
91
+ let parsed;
92
+ try {
93
+ parsed = parseArgs({ args: rest, options: command.options, allowPositionals: true, strict: true });
94
+ } catch (e) {
95
+ throw new Fail(e.message, USAGE);
96
+ }
97
+ const { positionals = Infinity } = command;
98
+ const n = typeof positionals === 'function' ? positionals(parsed.values) : positionals;
99
+ const extra = parsed.positionals[n];
100
+ if (extra !== undefined) {
101
+ const used = name === 'spec' ? 'spec --add-ids' : name;
102
+ throw new Fail(`extra argument ${JSON.stringify(extra)}: ${used} takes ${n === 0 ? 'no' : n} positional argument${n === 1 ? '' : 's'}`,
103
+ 'quote an option value that holds spaces, and leave out the extra argument');
104
+ }
105
+ let out;
106
+ try {
107
+ out = await command.run({ top, cwd, args: parsed.positionals, opts: parsed.values });
108
+ } catch (e) {
109
+ // A config or schema file that does not parse is the user's to fix.
110
+ if (e instanceof Fail || !/^\.assuredloop\//.test(e.message)) throw e;
111
+ throw new Fail(e.message, 'fix the file and run it again');
112
+ }
113
+ // A machine stream (the export's JSONL, search --json) is written as it is.
114
+ if (out.raw !== undefined) process.stdout.write(out.raw);
115
+ else print(out.body, out.read ?? read, out.next, out.notKnown ?? []);
116
+ return out.exit ?? 0;
117
+ } catch (e) {
118
+ if (!(e instanceof Fail)) throw e;
119
+ print([`al: ${e.message}`], read, e.next ?? USAGE, ['nothing was written']);
120
+ return 2;
121
+ }
122
+ }
123
+
124
+ try {
125
+ process.exitCode = await main(process.argv.slice(2));
126
+ } catch (e) {
127
+ process.stderr.write(`al: internal error: ${e.stack || e}\n`);
128
+ process.exitCode = 2;
129
+ }
package/package.json CHANGED
@@ -1,10 +1,37 @@
1
1
  {
2
2
  "name": "@assuredloop/cli",
3
- "version": "0.0.1",
4
- "description": "Placeholder for AssuredLoop's CLI (al). The first real release will be published from GitHub Actions.",
5
- "license": "UNLICENSED",
6
- "files": ["README.md"],
3
+ "version": "0.2.0",
4
+ "description": "AssuredLoop keeps an AI focused on one change: the al command gathers and checks a change's requirement, spec, decisions, code and tests.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/guwenqing/assuredloop.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/guwenqing/assuredloop/issues"
12
+ },
13
+ "homepage": "https://github.com/guwenqing/assuredloop#readme",
14
+ "type": "module",
15
+ "engines": {
16
+ "node": ">=24"
17
+ },
18
+ "bin": {
19
+ "al": "bin/al.js"
20
+ },
21
+ "files": [
22
+ "bin/",
23
+ "src/",
24
+ "skills/",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "scripts": {
29
+ "test": "node --test"
30
+ },
7
31
  "publishConfig": {
8
32
  "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "yaml": "2.9.1"
9
36
  }
10
37
  }