@assuredloop/cli
v5.0.4
Published
AssuredLoop keeps an AI focused on one change: the al command gathers and checks a change's requirement, spec, decisions, code and tests.
Readme
AssuredLoop
AssuredLoop keeps an AI focused on one change. It holds together:
- the owner's words, as snapshots;
- the organized requirement that the owner signs;
- the consolidated spec in
specs/: the system now, the promises and the lasting design, each paragraph with a hidden ID; - each change's own design, its change spec, beside its request;
- the decisions and the ADRs;
- the code, the tests and the documents that came with the change.
Its command, al, keeps these records, checks them, and says what to do
next. It refuses only in its own al conclude. al check gives the AI
review hints, and the review decides. Only a few exact rules are not ok
(see "Hints, and the rules that block"). A project that wants them to block
turns on al check --strict in its CI.
AssuredLoop is not a lifecycle tool. It has no phases, no roles, no task states and no approvals beyond the owner's sign-off. How and when you design, test and review is your project's way of working.
Install
Version 0.2.0 is v4. It is a breaking release: it does not read the records of 0.1.0. To move a project from 0.1.0, see "From 0.1.0" in "Adopting AssuredLoop".
It needs Node 24 or newer and git 2.31 or newer. Install the npm package
@assuredloop/cli, which puts al on your PATH:
npm install --global @assuredloop/cliSearch with a model needs the embedding model's library beside it (see
"Search"). al names the library as an optional dependency, so npm installs
it only when you ask:
npm install --global @assuredloop/cli @huggingface/[email protected]To pin a version, name it: npm install --global @assuredloop/cli@<version>.
The package carries the skill, at
$(npm root -g)/@assuredloop/cli/skills/assuredloop/SKILL.md. Add one line to
the project's AGENTS.md:
AssuredLoop keeps this project's requests and spec: at the start of each task, read <dir>/skills/assuredloop/SKILL.md and run `al check`.In a shared repo whose contributors install al in different places, copy
the skill at the pinned version into the project, for example as
.claude/skills/assuredloop/SKILL.md. Name that path in the AGENTS.md line,
and say in the commit which version it came from.
Search with a model needs the library @huggingface/transformers 4.3.1 (see
"Search"). Install it in the project, or beside al.
The model in one page
Paragraphs and markers. Every paragraph of a spec doc has an ID in a hidden HTML comment on its own line before it, with a blank line before and after it. The marker holds the ID, the kind and the links:
<!-- INV-41 rule serves:invoice-exports/R2 builds-on:INV-12 --> The export link MUST expire 30 minutes after the email is sent.GitHub does not show the marker. The ID is one flat number per file, and it is never used again.
alcomputes a display number, for exampleINV-41 (2.3:4, in Export links), which can change when text moves. So a heading needs no number of its own: write## The proxy, not## 7. The proxy.Kinds. Promise kinds (purpose, scope, rule, limit, definition): a change needs a signed requirement that covers it. Design kinds (component, interface, data, flow, choice) and
approach(how one change is built): a change needs review. Informative kinds (rationale, example, open),note(headings and connecting text), and the change-only kinds (plan, step, migration).A request is the folder
requests/<name>/:origin/(the owner's words and the sign-offs, as snapshots with SHA-256; a project may keep them outside the repo, see "Original files outside the repo"),request.md(the dialog, the organized requirement R1…, the decisions D1…) and, from path 2,spec.md, the change spec, with IDs SP-1, SP-2… The order of its paragraphs is free. The recommended order is one section per topic. Each section has the promise paragraph first, then the design that keeps it, with its rationale and examples, then its open points. Cross-cutting design and decisions follow. While the request is open, a paragraph of its change spec may be deleted: its ID is retired and never used again, andal checkgives a hint. Inspecs/, a removal needsremoves:. Plan, step and migration paragraphs may live in the tracker issue that a task'srefnames, instead of in the change spec.A new version of an archived requirement is a requirement of a new request, whose marker names the old one:
<!-- R5 from:<snapshot> replaces:assuredloop-v4/R5 -->. Once the new one is signed,alshows the old one as replaced.Records in
.assuredloop/records/, committed to git: one per request (its sources, requirement versions, sign-offs, decisions, tasks, the bindings of its links and its close lines) and one per doc (fields thatal indexcan always rebuild).alwrites every hash and every derived field.Bindings. When a link is first indexed,
al indexrecords the hash of both ends. Ordinary indexing never moves a binding. When the target changes, the source gets the hintstale-baseuntil someone checks it and records a judgement (see "Hints, and the rules that block"). When the source paragraph's own text changes, it gets the hintholder-changeduntil it is judged in the same way. The judgement moves the binding to the texts of now. This hint covers the links that a paragraph of a spec doc, an ADR or an open change spec still declares, to a target that still resolves, in the records of the open requests. A requirement is out, because a changed requirement needs a new sign-off. An output file is out. When no current marker declares a link any more,al indexremoves its binding, in the open and the archived records, and names it in its output. Git history keeps it.Consolidation is by judgement. A PR brings each piece of a change into
specs/in its own words: it may rewrite, split or join paragraphs, so that the spec reads as the current system. No command writes the spec, andaldoes not compare the two texts. The review judges each piece in a consolidation judgement, andal checkshows it as current or stale.
Paths
The user picks the path. The skill has the steps of a change, the review steps among them, and one short section on what differs by path.
| Path | When | Records | Sign-off | Closes when |
|---|---|---|---|---|
| 0. Fix | no promise changes: it restores what the spec says, or claims a typo in any paragraph | none | no | the PR merges |
| 1. Small promise amend | one or a few promise paragraphs change | a short request; the PR edits specs/ directly | yes | the PR merges |
| 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 |
| 2. Big change | a real design, one or a few PRs | a request and a change spec | if a promise kind changes, or the request has an R | the close rule holds |
| 3. Epic | big, in parts over time | as 2, with optional task references | as 2 | the close rule holds |
| 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 |
Every PR states its path and its claim in a commit message, for example
Tier: 0 — typo in INV-3 or Tier: 2 — invoice-payments, part 1. al check
reads the newest Tier: line in the branch's commits, and checks the claim
against the kinds and the changed paragraphs. Where they disagree, it gives
a hint, for example "path 0, but a rule paragraph is changed with no typo
claim". It checks the labels, not their meaning.
The close rule. al conclude <name> refuses while a promise change has
no sign-off, a spike's current question is not signed, or an original file
of the request is not checked or does not match its record. On paths 2 and
3 it also refuses while:
- a signed requirement of the request's current version has no declared
paragraph link: no paragraph of
specs/saysserves:<name>/R<n>, and no accepted ADR saysdecides:<name>/R<n>; - a change paragraph says
changes:<ID>, and that spec paragraph is unchanged since the link (or the link has no binding yet: runal index); - a change paragraph says
removes:<ID>, and that paragraph is still inspecs/.
A close line clears the last three for what it names: one sentence that you
record with al record <name> close --on <ID> --reason <sentence>. On a
change paragraph, it says why the paragraph is not delivered. On a
requirement, it says where the requirement is met when no lasting paragraph
links to it, for example an existing design reused or a one-time migration.
al keeps the version of what the line names and of each spec paragraph
that its reason names. When one of them changes, al conclude shows the
line as stale, and it clears nothing until you record it again. A close line
never replaces a sign-off. A review judgement never clears a refusal, and a
missing or stale consolidation judgement is shown and never refuses.
The commands
al new <name> --from <file|-> [--tier <1|1d|2|3|S>] [--title <text>]: start a request from the owner's words (-reads standard input).al record <name> origin --url <source> --from <file|->: a snapshot of later words of the owner, or of a source you rely on.al record <name> signoff --source <where> --words <their words>: show what the owner signs; with--yes, record the sign-off on the owner's own yes.al record <name> decision --source <who> --text <decision> [--clarifies R<n>]: add D1, D2…al record <name> judgement --hint <code> --on <ID> --reason <why>: record a one-line judgement that clears a hint; see "Hints, and the rules that block". Forissue-changed, add--issues <folder>; see "Hints from the linked work items".al record <name> close --on <ID> --reason <sentence>: a close line, on a change paragraph that is not delivered or on a requirement that is met where no lasting paragraph links to it; see "The close rule".al spec: every doc's IDs, display numbers and kinds.al spec --add-ids <file> [--prefix <P>]: give each unmarked paragraph the next free ID.al index: write the derived facts and the new bindings, remove the bindings of links that no marker declares, and list the AI hints to refresh.--alignis removed: a judgement moves a binding (see "Hints, and the rules that block").al index --issues <folder>also keeps the text of the tasks' work items (see "Hints from the linked work items").al check [--strict] [--issues <folder>] [--full] [--all] [--at <commit>]: it runsal indexfirst, and names in one line the record files that it wrote, to commit with the change. Then the checks of this branch against its base, what is in force for the change (see "What is in force for a change") and the map. With--issues <folder>, it also checks the work items that the caller saved in the folder (see "Hints from the linked work items").--fullshows the text of every item in force, and of each changed paragraph with its links.--allis the audit: every item in force on its own line.--at <commit>answers as of that commit, read from git, and writes nothing. The facts thataltakes from the branch history need that history: in a shallow clone they are "not known", so check out the whole history (on GitHub Actions,fetch-depth: 0).al context [<name> | <ID>]: where the project, a request, a paragraph, an ADR (ADR-2) or a requirement (<name>/R1) stands. For an ID, it gives oneCitedline for each file that names the ID, with the count and every line number;--fulladds the text of each of those lines. The review view is nowal check --full.--auditgives a request's whole trace;--at <commit>any v4 state.al conclude <name>: the close rule; with--yesit writes the Outcome and moves the request torequests/archive/<name>/.al export [--at <commit>] [--out <file>] [--yes]: one JSONL row per paragraph version, with its role, version, commit and links. It is not committed. With no--out, it writes the rows to standard output. With--out, it shows the path, the row count and whether it replaces an existing file; with--yes, it writes the file.al search <words>: see "Search".
Every command that changes files, except new and index, shows what it
would write, and writes only with --yes. Beside each ID that an answer
shows, it gives the file and the line of the item's text (never its marker
line). A finding of al check ends with · places: and the place of each
other ID that it names. A fact that only al's records hold, such as a
sign-off, is named as such. Each output ends with three lines:
Read (what it read), Next (what to do next) and Not known (what the
tool cannot know).
What is in force for a change
On a branch, al check shows the requirements and decisions in force that
may govern the change, also when no code or test names an ID. The change is
the branch against its base, with the untracked files and without
.assuredloop/. In force are the promise and design paragraphs of the spec
(not approach), and each accepted ADR that no accepted ADR supersedes.
in force <ID> <basis> · <file>:<line> · <kind>: <text>: an item that the change edits (changed by this change), names in a line that it adds or removes (cited (<file>:<line>)), or reaches through the item's file scope (scope changed in this change,by scope,all files). An accepted ADR whose text changed is markedtext changed, review open; an item that another open request changes is markedopen proposal: <name>/SP-n (<file>:<line>) changes it.proposed ...: the change's own signed requirements, its change spec paragraphs and the documents of its design lines. They are not in force. The change's own request is an open request whose folder or record the change touches, or whose name a commit message of the branch names.<N> further items in force: ...: every other item, by its ID, with exact counts.al check --fullshows their text.list basis:andlist limits:say what the list rests on and what it cannot show. An item that is not shown in full may still apply.
A file scope is a token in the item's marker, for example
<!-- INV-41 rule serves:R2 files:src/export/**,test/export/** -->. **
matches any number of folders, * matches within one folder name, and ?
one character; write ? for a space or a comma in a path. files:** is
all files. The token is not part of the text: changing it changes no hash
and gives no marker hint. A scope that matches no file gets the hint
scope-no-file.
A design line names a document as the change's design: a line
Design: <path> in the request's request.md, or the field design (a
path or a list) of a saved JSON work item that a task of the request names.
<repo>:<path> names a file of an output repo at its selected commit. A
document with markers lists its design paragraphs; one without is
supporting evidence. A design line whose document is missing or cannot be
read gets the hint design-doc.
The review's judgements
The review writes its judgements in requests/<name>/review.md of the
change's own request. One judgement is one top-level list item:
- INV-41 complies: the link expires 30 minutes after the email.
- Item: "MUST expire 30 minutes after the email is sent"
- Code (src/export.js): "export const ttl = 30;"
- Code (invoicer-web:src/guard.js, base): "if (!signed) refuse();"
- Test: no test foundThe verdict is complies, violates, unclear or not applicable. A
path is read at the head; , base reads it at the base, and
<repo>:<path> reads an output repo at its selected commit. Test takes a
quote, no test found or tests not examined. A quote matches word for
word, with each run of white space taken as one space. An ID in the note
is no citation in the list above.
al index keeps the identity of each new judgement in the request's record:
the base and a hash of the change in each repo, without the review notes
and .assuredloop/, and the hash of the item's text. Do not write or read
it. After the list, al check shows one line for each judgement:
review note requests/csv-export/review.md: 1 judgements, 1 current, 0 stale, 0 not known when judged, 0 identity not kept, 0 not read
judgement INV-41 version 1a2b3c4d5e6f complies · requests/csv-export/review.md:3 · item quote found; item in force · code quote found (src/export.js) · no test found · currentThe item's state is in force, proposed by this change, draft,
replaced or not in force. A quote that al found is "quote found",
never "complies". A judgement is stale: judged at <commit> when another
file of the change, in any repo, or the item's text or version changed
since. When a repo cannot be read, it is not known: <repo> cannot be read
(<reason>), never current. To renew
it, rewrite the judgement (for example its reason) and run al index.
These are hints only: a judgement changes no exit code, no not ok,
--strict or al conclude.
A consolidation judgement says how a piece of the change went into
specs/. It has a Spec line for each spec paragraph that holds the piece
now. It may name several change paragraphs, so a split or a join needs one
judgement:
- SP-3 and SP-4 complies: one shared link that lives 24 hours, now in INV-41 and INV-42.
- Item (SP-3): "A shared export link lives for 24 hours"
- Item (SP-4): "the email carries only the link"
- Spec (INV-41): "The export link MUST expire 24 hours after it is created."
- Spec (INV-42): "The email MUST hold the link and no file."
- Spec (INV-9, base): "The link expires 30 minutes after the email is sent."al index keeps the version of each paragraph that it names. The
judgement goes stale when any of them changes, by this change or by other
work.
The map of a change
On a branch with an open change of path 2 or 3, al check shows the map
after the review section, from facts only. al compares no text with text:
map: the change against its base 1a2b3c4 (own changes: csv-export), from facts only
map spec INV-41 · specs/invoices.md:14 · changed; named by csv-export/SP-3 changes, judgement requests/csv-export/review.md:3
map spec INV-43 · specs/invoices.md:30 · added; named by no change paragraph
map link csv-export/SP-3 changes INV-41 · requests/csv-export/spec.md:9 · changed since the link
map consolidation · 2 change paragraphs · current: its named paragraphs are unchanged (judgement requests/csv-export/review.md:3, complies): csv-export/SP-3 (requests/csv-export/spec.md:9), csv-export/SP-4 (requests/csv-export/spec.md:13)A link's target is unchanged since the link, changed since the link or
removed. "Unchanged" means that the paragraph still has the text that it
had when the change first linked it. Whether the change's effect is in it
is the review's judgement. Lines with the same fact are one line, which
names each ID. Two hints come with the map: map-unnamed, a spec paragraph
that the branch changed and that no changes or removes link and no
consolidation judgement names; and map-consolidation, the change
paragraphs that go into specs/ with no current consolidation judgement.
al conclude shows them again, and never refuses on them.
Settings
.assuredloop/config.yaml, at the repo's root. Every key is optional. A
spec root that is outside the repository, reached through a symlink, or not a
folder is a not ok in al check. Only the default root, specs, may be
absent.
root: specs # the spec folder; every *.md in it except adr/ is a spec doc
docs: # each file and its ID prefix; other files may be listed too
- {file: specs/invoices.md, prefix: INV}
results: .assuredloop/results # where result files are
outputs: # in the central repo: its output repos
- {name: invoicer-web, path: ../invoicer-web, commit: <sha>}
central: {path: ../invoicer} # in an output repo: the central repo
phrases: # text for the review to look at; each match is a hint
- the requester # a phrase: ignoring case, across a line break too
- {pattern: "Avery (asked|wants)"} # a JavaScript regular expression
items-out-of-reach: # work items that the CI token cannot read
- acme/private-plans # see "Items out of the CI token's reach"al check gives the hint phrase for each match of phrases: in the spec
docs, the ADRs and the Markdown files of the open requests, but their
origin/ folders; with --issues <folder>, in the work items too. The list is empty by
default. A bad entry or an invalid pattern stops al check with exit 2.
items-out-of-reach: names the work items that the CI token cannot read
(see "Hints from the linked work items"). A value that is not a list of
texts that are not empty stops al check with exit 2.
.assuredloop/schema.yaml holds the kinds and the link words, with a version.
al spec --add-ids writes both files when they are absent.
ADRs are specs/adr/<n>-<slug>.md, with Status: proposed, accepted or
superseded by ADR-<n> on the first line. The name starts with one or more
digits and a dash, and the digits are the ADR's number: 001-x.md is ADR-1.
Two files with the same number give the hint adr-number; al reads each
of them, and uses the first in name order where the number is named. When
specs/adr/ holds Markdown files and none of them is an ADR, al check
gives the hint adr-none. A status line may name several
ADRs that supersede it: superseded by ADR-11, ADR-12 or
ADR-11 and ADR-12. The heading's marker holds ADR-<n> and its
decides:, source: and supersedes: links.
- Any text may be rewritten in other words when its meaning stays the same, an accepted or superseded ADR's text too. A new ADR that supersedes it is needed only when the decision changes, in whole or in part.
- The review judges whether a rewrite keeps the decision;
aldoes not. So a changed ADR text, a changed or removed link in its marker, or any other change of its marker line, gives a hint: check that its decision is unchanged. Links added to the marker of an accepted ADR, with nothing else changed, give no finding. al checkgives a hint for a marker line of an ADR that holds anything other than its ID, its kind and links: an unknown token, a second<!--or-->, or an empty item in a list of links. A line that starts like a marker, but thataldoes not read as one, gets the same hint.al checkgives a hint for an ADR whose first line is notStatus: proposed,Status: acceptedorStatus: superseded by ADR-<n>, with one ADR or a list of them.- The hint
adr-changedclears by a fix, or by a judgement, with its reason, that the decision is unchanged:al record <name> judgement --hint adr-changed --on ADR-3 --reason <why>. It holds until the ADR's text or its marker line changes again. - After a rewrite, a judgement on
holder-changedmoves the bindings of paragraph<ID>to the ADR's new text, for an accepted or a superseded ADR. al checkgives a hint for a proposed or accepted ADR with nosource:or nodecides:.- A current place that names a superseded ADR also names the ADR that
supersedes it, in the same marker or the same paragraph. Current places
are the spec docs, the open requests and the proposed and accepted ADRs.
The text names an ADR by
ADR-40, byADR 0040, or in a list or a range such asADRs 0003, 0005 and 0040orADRs 0003 to 0005. The chain itself and dated records are exempt: an ADR that supersedes it, a superseded ADR, an ADR's## History, a request's## Decisions, theorigin/files and the archived requests. Elseal checkgivesnot ok superseded-cited. - A superseded ADR's status line names the ADR that supersedes it, and that
ADR's marker has
supersedes:for it. Elseal checkgivesnot ok superseded-status.
Original files outside the repo
A project may keep the original files of its requests outside the repo: the owner's words, the sign-off snapshots and the other source snapshots. The repo then keeps only the name and the SHA-256 of each file, in the records. The committed config names the place by a name, never by a path:
originals:
outside: assuredloop # the name that each worker maps to a folderWith no originals key, the files are in requests/<name>/origin/, as
before, and al reads no other setting.
Each worker maps the name to a folder in assuredloop/places.yaml in the
user's config folder ($XDG_CONFIG_HOME, else ~/.config), one line for
each name:
assuredloop: /path/to/folderIn the folder, the files are <request>/<file>, with the same names as in
origin/. Every clone on the machine uses the same folder.
al new,al record <name> originandal record <name> signoffwrite each new original file to the folder, under a name that is free in the folder and inorigin/. With no place set, or when the write fails, they refuse and write nothing. A--url,--sourceor--presentedthat is a path inside the folder is recorded asoriginals:<path>.alreadsrequests/<name>/origin/<file>first. Only a file that is not there is read from the folder. A file inorigin/that cannot be read isnot ok record-evidence, also when the folder holds a good copy.- A file in the folder that is missing, cannot be read or has changed is
not ok record-evidence, named asoriginals:<request>/<file>. - With no place set, as on CI,
al checkgivesinfo originals-not-checkedfor each request, with its files, and theNot knownline names them. A sign-off whose file is not checked is not counted as signed, and is nonot okfor that reason alone. Its other conditions are checked. Soal check --strictpasses on CI. al concludechecks each original file of the request again, and refuses while one is not checked or does not match. So a request closes only on a machine where the place is set.al exportgives no row for a file kept outside. It names each one with its SHA-256 on standard error, or in the summary with--out.- A places file that cannot be read or is not valid YAML, or a line that is not the absolute path of a folder, stops the command. No places file, or no line for the name, is "no place set".
Hints from the linked work items
A project may keep its work items in any tracker, or in none. al never
contacts a tracker. The caller saves the items in a folder, and
al check --issues <folder> checks the items that the tasks: of the open
requests name. Without --issues, al check reads no item. A task's ref
names an item as #n, owner/repo#n, <name>#n for an output repo that
config names, an issue URL on any host, or the item's own ref, for
example PROJ-12. Each finding is a hint, except that a closed item gives
issue-superseded as info:
issue-missing: a task names an item that is not in the folder;issue-superseded: the item's text names a superseded ADR without the ADR that supersedes it, by the rule ofsuperseded-cited. A closed item gives it asinfo, not as a hint, because a closed item records what held when it closed;issue-bare-ref: a bare#nin an item of another repository, which GitHub reads as that repository's issue. The request's own repository comes fromgit remote get-url origin;issue-not-sub: the item is not a child of the request's own item, the item thatassuredloop-requests.jsonin the folder names (below);phrase: a phrase ofphrases:matches;issue-unread: a file in the folder is not valid JSON or YAML, is a Markdown file that is not an item, or holds an entry that is not an item.issue-changed: the item says something different from what it said when the change relied on it, oralcannot tell (below).
A work item that changed. al index --issues <folder> keeps the hash
of each task item's text in the request's record, by itself; no person and
no AI writes or reads it. The text is the body and each comment that is not
hidden; the state is not text. It keeps the text at the moment of reliance:
when the request names the item, and again at the first sign-off after
that. So run it in the commit that names an item, and after
al record <name> signoff --yes, before the commit that records the
sign-off. It never keeps a text for a moment that is past: for an item
named in an earlier commit, it keeps nothing. al cannot tell when the
caller saved the items, and the Not known line of each
al check --issues says so: save them again before a check.
al check --issues <folder> then gives issue-changed:
- "changed since …" when the item's text now differs from the kept text;
- "not known whether it changed …" when no text was kept at that moment, or
the item is not in the folder or cannot be read.
alnever reports such an item as unchanged.
A closed item gets the same hint. To clear it, read the item, then record that you read it, with its reason:
al record <name> judgement --hint issue-changed --on <item> \
--reason "<why>" --issues <folder> --yes<item> takes the forms of a task's ref. The judgement records the item's
text now, and the moment of reliance that it covers. The hint comes back
when the text changes again, or when a later sign-off becomes the moment of
reliance.
The folder holds *.json, *.yaml, *.yml and *.md files. Every file
of these kinds is read as items, a README.md too. A JSON or YAML file holds
one item, or a list of items; a Markdown file holds one item. An item is
the JSON that the GitHub CLI prints with
--json number,url,body,comments,parent,state, so GitHub issues need no
conversion. A YAML item has the same fields:
| Field | What it is | Needed |
|---|---|---|
| body | the item's text | yes |
| url | its address, https://<host>/owner/repo/issues/n for GitHub | one of url, number with repository, or ref |
| number | its number in its repository | |
| comments | a list, each with body, and isMinimized true for a hidden comment | no |
| parent | the parent item, with url and number, or ref; null for none | no: with no parent, the child check is not made |
| ref | the reference that a task names, for another tracker, for example PROJ-12 | for another tracker |
| repository | owner/repo, or a project key | no |
| state | OPEN or CLOSED, as gh gives it; another tracker maps its states to these two. Any other value is ignored | no: with no state, the item counts as open |
To save GitHub issues, GitHub Enterprise included, save each issue with
gh issue view. gh issue list gives at most 100 comments of an issue.
mkdir -p /tmp/items
gh issue view 31 --repo acme/invoicer \
--json number,url,body,comments,parent,state > /tmp/items/invoicer-31.json
GH_HOST=github.example.com gh issue view 5 --repo acme/web \
--json number,url,body,comments,parent,state > /tmp/items/web-5.json
al check --issues /tmp/itemsAn item of another tracker:
{"ref": "PROJ-12", "repository": "PROJ", "body": "Build the export (ADR-3).",
"comments": [{"body": "Done.", "isMinimized": false}], "parent": {"ref": "PROJ-1"}}An item in Markdown is plain text that a person or an AI writes with no tool:
URL: https://github.com/acme/invoicer/issues/31
State: OPEN
Parent: https://github.com/acme/invoicer/issues/30
Build the CSV export of invoices (ADR-3).
Design: docs/rfc/0018-export.md
## Comment 1
The first PR is up.
## Comment 2 (minimized)
An off-topic remark.- The header is the first lines, up to the first blank line, each
<Key>: <value>. The keys areURL,Number,Repository,Ref,StateandParent, case ignored, and each gives the field of the same name.Stateisopenorclosed.Parentis a URL,#n,nonefor no parent, or another text, which is the parent'sref. With noParentline, the child check is not made. - The body goes from the header's blank line to the first comment. A
Design:line in it names the item's design. - Each comment starts at a line that is exactly
## Comment <k>or## Comment <k> (minimized), outside fenced code. k counts from 1, in order. - Blank lines at the start and the end of the body and of each comment are dropped. So an item whose body or comment starts or ends with a blank line has another kept text in Markdown than in JSON or YAML.
- A Markdown file whose first line is not a header line, or that has no
URL, noNumberwith aRepository, and noRef, is not an item: it getsissue-unreadwith the reason.
Two files may hold the same item, in any formats. The files are read in one
name order. Where a task names the item by #n, owner/repo#n,
<name>#n or a URL, the first copy is checked, and the other counts as an
item that no task names. Where a task names it by its ref, each copy is
checked.
Items out of the CI token's reach. The token of a CI job often cannot read every item, for example the items of a private repository. Name those items in the committed config, and save the others:
items-out-of-reach: # work items that the CI token cannot read
- acme/private-plans # every item of this repository
- acme/web#5 # one item, in a form of a task's ref
- PROJ-12For a task's item that the list names and that is not in the folder,
al check --issues gives no issue-missing and no issue-changed. In
their place, it gives one line for all of them, with every task that names
each one:
info issues-out-of-reach - - 2 items out of the CI token's reach (items-out-of-reach in .assuredloop/config.yaml) are not in <folder>, so they are not checked, and al cannot tell whether they changed since the change relied on them: acme/private-plans#4 (pay-run/T2), PROJ-12 (pay-run/T1)An item that the list names and that is in the folder is read as usual.
The line names each item once, ignoring case. A task's ref that names no
saved item, and that holds more than other trackers' keys of the list,
keeps its issue-missing.
The folder also says which item is each request's own item, in the file
assuredloop-requests.json. al reads the own item from this file only,
never from an origin source. Without the file, or for a request that it
does not list, issue-not-sub is not checked, and the Not known line
says so once for each request. The file is not read as an item. A file that
is not valid gets issue-unread and is ignored; al check goes on.
format: the exact textassuredloop-items/1;requests: each key is a request's name, and its value is that request's own item, in a form that a task'srefuses: a URL,owner/repo#n, or another tracker's key, for examplePROJ-1.
{
"format": "assuredloop-items/1",
"requests": {
"invoice-exports": "https://github.com/acme/invoicer/issues/30",
"pay-run": "PROJ-1"
}
}To write it for a GitHub issue:
printf '{"format": "assuredloop-items/1", "requests": {"invoice-exports": "%s"}}\n' \
"$(gh issue view 30 --repo acme/invoicer --json url --jq .url)" \
> /tmp/items/assuredloop-requests.jsonThe JSON Schema (draft 2020-12) of each file, for a person or an AI that writes them. An item file:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "an item file: one item, or a list of items",
"oneOf": [
{"$ref": "#/$defs/item"},
{"type": "array", "items": {"$ref": "#/$defs/item"}}
],
"$defs": {
"item": {
"type": "object",
"required": ["body"],
"anyOf": [
{"required": ["url"], "properties": {"url": {"type": "string", "pattern": "^https?://[^/\\s]+/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/issues/[0-9]+([^A-Za-z0-9_][\\s\\S]*)?$"}}},
{"required": ["number", "repository"]},
{"required": ["ref"]}
],
"properties": {
"body": {"type": "string"},
"url": {"type": "string"},
"number": {"type": "integer"},
"comments": {
"type": "array",
"items": {
"type": "object",
"required": ["body"],
"properties": {"body": {"type": "string"}, "isMinimized": {"type": "boolean"}}
}
},
"parent": {
"oneOf": [
{"type": "null"},
{
"type": "object",
"properties": {"url": {"type": "string"}, "number": {"type": "integer"}, "ref": {"type": "string", "minLength": 1}}
}
]
},
"ref": {"type": "string", "minLength": 1},
"repository": {"type": "string", "minLength": 1},
"state": {"enum": ["OPEN", "CLOSED"]}
}
}
}
}assuredloop-requests.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "assuredloop-requests.json",
"type": "object",
"required": ["format", "requests"],
"additionalProperties": false,
"properties": {
"format": {"const": "assuredloop-items/1"},
"requests": {
"type": "object",
"additionalProperties": {"type": "string", "minLength": 1}
}
}
}Search
Search follows one committed setting, search: in
.assuredloop/config.yaml. No choice is a default. Until you set one,
al search searches nothing: it shows the choices, what each costs on this
computer, the measured figures with their limits, and its recommendation.
The checks never use search.
| Setting | What al search does |
|---|---|
| search: {own: <name>} | Your team's knowledge base, an MCP server or a skill: al search names it and says to ask it. al export gives it al's rows. |
| search: local | Word search (level 1): a local full-text index (SQLite FTS5, BM25), with the ID in its own exact column. Needs Node 24 or newer (node:sqlite). |
| search: {local: <row>} | Word search and one row of the measured table (level 2): for an embedding row, word and vector results are fused; for a reranker row, the reranker reorders the word-search top 30. A tjs-* row needs the library @huggingface/transformers 4.3.1, in the project or beside al. A llama-* row needs llama.cpp's llama-server on PATH (measured: release b11624), on macOS arm64. |
| search: none | No search: al search says so. Read the files (grep). |
- The measured table holds each model, precision and runtime that was
measured for
al search(issue #28), for examplebge-small-en-v1.5 q8 tjs-cpu. With no setting,al search <words>names every row: the rows that fit this computer, each with its download, memory, first index time, time for each search, recall@10 and gain over word search with their intervals; then the rows that do not fit and the excluded rows, each with its reason. Allama-*row does not fit off macOS arm64, where it was not measured.--fullshows every row in full, and--jsongives them all. - The recommendation comes from code with no model: of the rows that this
alruns and that fit, those with a gain whose interval is above zero and a measured time of at most 10 s for each search, then the smallest download within 0.05 of the best recall. When no row is left, it recommendslocal. When the 10 s bound removes the row that it would recommend, it names that row, its gain and its time. The figures come from one Mac and 50 questions from one repo; the Limits line says so. - A wrong setting, a row that is not in the table and an excluded row are
refused (exit 2). A row that does not fit this computer still runs, and
a
Not knownline says what it needs. - The default query is the current system: the spec text at the selected
commit.
--change <name>adds that request's open change spec, its sources and its evidence.--historyadds removed and replaced text, marked as history. --id <ID>finds one paragraph by its exact ID, never through search.--level <0|1|2>asks for a lower level (2 is the row of the setting);--at <commit>another commit.- Word search also finds each file of the central repo and the output repos
by the words of its path. Such a hit shows
file <path> <kind>, where the kind (code, test, doc, config or other) comes from the path alone. - Each hit shows its role (
baseline,proposal,spike,source,evidenceorhistory), its version and its commit. - In the central repo, search covers the output repos that its committed
config lists, each at its resolved commit. The
Reposline names each repo and its commit, for exampleRepos invoicer@a6ac3a7785dd invoicer-web@5aa04042117b. An output repo givesevidencerows: its result files, and the files that cite a central ID or that a record declares. Such a hit shows<repo>:<path>, and each line that names a central ID, with its line number. A repo that is absent, or a commit that cannot be found, gives no rows and aNot knownline with the reason. - The index is one SQLite file inside
.git, never committed.al searchbrings it up to date before it answers;--rebuildbuilds it again. - Level 2 runs the model of the row on this computer, pinned by its commit.
Before the first download,
alwrites the row and its size to standard error. The model download is the only data that leaves the machine.alitself makes no network call. Without the row's runtime,al searchanswers at level 1, and aNot knownline says how to add it. Onlyal searchloads the library or startsllama-server. - For a
llama-*row,alstarts onellama-serveron 127.0.0.1 for eachal search, with the measured arguments, and stops it after. The model file is fetched byllama-serverinto its own place (LLAMA_CACHE, else~/Library/Caches/llama.cpp).alchecks the file's size on every run and its SHA-256 once; a file that differs gives word search, and theFallbackline says what was expected and found. Another build of llama.cpp still runs, and aNot knownline says that its figures may differ.
One central repo, and output repos
The central repo holds specs/, the ADRs, the requests and the records. Its
config lists the output repos. An output repo holds code, tests and user
documents only; its config names the central repo.
- Code and tests in an output repo cite central IDs with a qualifier:
central:INV-41,central:invoice-exports/R3. A fix there saysTier: 0 — restores central:INV-41. alreads another repo only through git, at the commit that config pins or at the clone's HEAD, and never fetches. A repo or a commit that is not there is "unknown", never a guess.- In an output repo,
al checklooks up the central IDs that the branch's changed files cite.
Adopting AssuredLoop
- Put the spec under the root (
specs/by default), or setroot:. - Mark every paragraph:
al spec --add-ids specs/<file>.md --prefix <P> --yesfor each spec file. Headings get the kindnote. Write the kind of each other paragraph into its marker. An adopted paragraph needs no link. - Add the AGENTS.md line from "Install". Claim
Tier: 0 — adds paragraph IDs; no promise changes. al spec --add-ids --yesalso records where the adopted text came from. For each paragraph it marks whose text is a block of the file at HEAD, in the same place, it writes an entry with HEAD's commit and the text's hash to.assuredloop/records/requests/adoption.yaml. It writesrequests/archive/adoption/request.mdwhen that file is absent. So commit the spec unmarked before you mark it. A paragraph whose text is not at HEAD is not adopted: the command names it, and it follows its path as a change.source: adoptionsays where the text came from. It is not an owner approval, and it does not excuse a later change.- Run
al check, and commit the markers and the records together. On that branch,al check --strictreads the adopted paragraphs as unchanged, so they need no sign-off and no path claim.
A sign-off bound to the whole text. Markers change a file's bytes. If a
signed requirement, or any other record, binds the whole text of a spec file
by its SHA-256, that hash no longer matches after the markers are added. Ask
the owner for a new approval after the conversion. The paragraph text itself
does not change; al hashes each paragraph without its marker.
From 0.1.0. v4 (0.2.0) does not read 0.1.0 records. Keep 0.1.0 pinned until you move. Older records stay readable as files; run the pinned 0.1.0 at those commits.
In a project where .assuredloop is a file, as 0.1.0 writes it, each al
command refuses with exit 2 and one line: al: .assuredloop is a file, so the
project has 0.1.0 records, which v4 does not read; keep 0.1.0 pinned, or move
as "From 0.1.0" in the README says. It reads and writes nothing else, so
al check never passes there. al --version and the usage message still
work. To move:
- Finish each open 0.1.0 request on its pinned copy, or start it again under v4.
- Install 0.2.0 (see "Install").
- Remove the 0.1.0 file:
git rm .assuredloop. v4 keeps its settings in the folder.assuredloop/. If the file had aroot:line, write the same line in.assuredloop/config.yaml(see "Settings"). - Run
al spec --add-ids, as in steps 1-5 above. It marks every paragraph and writes the adoption record.
Hints, and the rules that block
al check gives the AI review hints, and the review decides. A finding is
not ok only for a rule that meets two tests: breaking it lets an
unapproved or wrong decision be built, and an exact check of the records,
hashes and links decides it, with no judgement. These rules are:
- An unsigned requirement (
not ok unsigned-requirement). In each request whose record hasrequirements:, open or archived, the latest version of each requirement has a valid sign-off. A sign-off is valid only when acoversentry matches the requirement's id, version andsha256exactly; its file is in the request'sorigin/folder (or at the place, see "Original files outside the repo"), and the hash of its signed text equals the sign-off'ssha256; and it records when it was signed and the signer's words. A requirement inrequest.mdthat is not its latest recorded version is not signed. An entry thatal checkcannot read isnot oktoo. A promise paragraph that changes with no signed requirement that covers it staysnot ok signoff-coverage. - Changed record evidence (
not ok record-evidence). Each origin file undersources:, and each sign-off file, still has the hash that the record lists.al checkhashes the snapshot's text after its---line, asal recorddid. - A citation of a superseded ADR (
not ok superseded-citedandsuperseded-status; see "Settings").
A request record that cannot be read (record-unreadable) and a spec root
that cannot be read (spec-root) are not ok too. Every other finding is a
hint in every mode, --strict included: the marker and ID lints, the path
claim, a link that does not resolve, an ADR's changed text or missing links,
the map's hints, and the rest. --strict changes only the exit code:
it is 1 when a not ok prints.
A hint clears by a fix, or by a one-line judgement with its reason:
al record <name> judgement --hint <code> --on <ID> --reason "<why>" --yes.
<name> is an open request, the change's own; --on is the ID that the
hint line shows, or its path when that ID is -. The judgement keeps the
versions of both sides of the hint. When one of them changes, the hint
comes back and names the judgement as stale. al check shows the current
judgements in one line, info judged. On stale-base, holder-changed and
binding-unknown, the judgement moves or binds the binding of that link to
the texts of now. A judgement never clears a not ok or a refusal of
al conclude: it is shown on the not ok line, and nothing else changes.
al check gives the hint "promise kind?" (promise-kind) for a paragraph
that is not a promise kind and holds a requirement word: MUST, SHALL, SHOULD,
MAY, never or always. When the paragraph is a promise, change its kind. When
it applies a promise, for example a flow step that keeps a rule, give it
builds-on: the promise paragraph in its marker. The hint then goes. A
builds-on: to another kind, or to an ID that does not exist, keeps it.
al check gives the hint no-paragraph-link for a signed requirement of an
open request that no paragraph of specs/, no accepted ADR and no promise or
design paragraph of an open change spec links to. At the close, al conclude
refuses on the same fact for paths 2 and 3. The hint says that the link is
missing, not that nothing was built. It names the known direct links, each
with the limit of its evidence: an approach or another paragraph of a
change spec that serves the requirement, an ADR that decides it and is not
accepted, a declared verifies output, and each file that cites
<name>/R<n>. The message holds no line number: the citing lines follow it
after · cited lines:, and the places of its IDs after · places:. It
clears by a link, by a close line (al record <name> close --on R<n>
--reason <where it is met>), or by a judgement on <name>/R<n>.
What is enforced, and by whom
| The tool checks | The skill asks | The owner decides |
|---|---|---|
| al conclude refuses on its 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 |
| not ok: sign-off coverage of promise changes, unsigned requirement versions, changed record evidence, citations of superseded ADRs and their status lines, and records or a spec root that cannot be read | choose the path, and state it in a commit message and the PR | whether a changed requirement is signed again |
| hints for the review: IDs missing, used twice, lost or used again; kinds and links; links that do not resolve; path and typo claims; a spec paragraph changed with no change paragraph that names it, and a change paragraph with no current consolidation judgement; stale bindings; overlapping changes; ADR text changes; a proposed or accepted ADR with no source: or no decides:; result applicability; a design paragraph that links to none of its requirement's promise paragraphs in its change spec; a for: that names no task; matches of the configured phrases; with --issues <folder>, the hints from the linked work items | record each later decision, with its source | decisions whose source is the owner, such as dropping or keeping work |
| | review from the changed behaviour: one search question, a judgement for each item that applies, the consolidation, the tests and the user documents; say what is unknown | |
| check --strict exits 1 on a not ok, when a project opts in | | |
al checks that declarations are well formed and that references resolve.
It does not check that a kind is right, that a requirement is fully
covered, that a test checks the rule it names, or that a change is
authorized in substance; its views say "not checked" for these. Nothing is
installed in git hooks.
Beyond code: results
A result file in .assuredloop/results/ (or the results: folder) records a
check, a test run or a review:
check: test/export-link.test.js
outcome: pass # pass, fail or not run
commit: <sha> # or unknown
inputs:
- {file: src/export-link.js, sha256: <sha256 of its bytes>}
by: ci # optional, with source and noteal shows "declared inputs unchanged since " when the inputs still
have those hashes, "does not apply to the current text" when one changed,
and "applicability unknown" when it cannot tell. That the declared inputs
are complete is a claim. The same works for a repo of documents: the report
is the deliverable, and a review result names it as its input.
This repository
requests/: the requests, andrequests/archive/for the concluded ones;docs/adr/: the decision records;specs/: the current spec.
The previous version is on the legacy branch. Its build is tagged
assuredloop-base-0.1.0-canonical-build, and its issues and pull requests stay
in this repository.
Releasing
- Bump
versioninpackage.jsonin a PR, and merge it. - Publish a GitHub Release tagged
v<version>on that commit. Mark it a pre-release to publish under npm'snexttag.
.github/workflows/publish.yml does the rest: it runs the tests and
al check --strict, checks that the tag matches package.json, and
publishes @assuredloop/cli to npm. It skips a version that is already on npm,
so a failed run can run again. It stores no npm token: npm trusts the
workflow by name, and adds a provenance statement saying which commit built
the package.
