v1.10.1 ·
A front door for the CLI: bare `aidlc` opens a menu
The problem
Typing plain aidlc today is a dead end, and slightly worse than the source idea assumed. Commander's
no-argument behaviour prints the help screen to stderr and exits 1 — verified:
$ node packages/cli/dist/cli.js >/tmp/o.txt 2>/tmp/e.txt; echo $?
1
stdout bytes: 0 # nothing on stdout
stderr bytes: 1988 # the help screen
So a user's first contact with the tool is an error exit with no stdout. Worse, that help screen is
the only map of a 19-command surface (add, claim, continue, cost, discover, docs,
doctor, gate, init, knowledge, metrics, release, review, start, status,
transition, update), several of which nest two or three levels deep (cost hooks install, cost rates update). Nothing in that listing conveys lifecycle order — which command a person runs first, or
which one they run next.
The framework's own answer to "what should I do next?" lives in the skills, not the CLI: the
aidlc entry-point skill reads .aidlc/state/ and composes a state-aware menu. That is by design —
but it means a terminal user who has not invoked an agent has no equivalent, and a discovery path
exists only inside an AI session.
How it could be solved
Two axes were explored: how much the menu knows and how much it collects.
Axis 1 — Menu intelligence
Option A — Dumb launcher. A static list of commands in lifecycle order, each with a one-line description and its flag form. Reads no lifecycle state at all.
- For: Zero divergence risk against the skill menu — it does not answer "what should I do next?",
so it cannot answer it differently. Fully deterministic, trivially unit-testable, works in a
project with no
.aidlc/at all (which is exactly theinitcase). Delivers G3 completely. - Against: Cannot say "continue cost-tracking at 85%". A user with in-flight work still has to run
statusto orient.
Option B — State-aware menu. Reads .aidlc/state/, leads with live entries (continue X at N%,
N raw ideas, pending retrospectives), mirroring what the entry-point skill composes.
- For: Best experience; the menu answers the actual question a returning user has.
- Against: Given C4, this is a second independent "what next?" engine. The skill ranks by most recently updated and gates on claims and retro markers; a CLI implementation would re-derive all of that from the same files, and the two would drift the moment either changed. This repo has already paid for that exact failure shape three times — the skill/template artifact-name mismatches recorded in lessons (app-operational-skills, 2026-08-22) are all "two definitions of one thing, one of them wrong". A contract test could pin the ranking rules, but nothing can pin judgment the skill applies and the CLI cannot.
Option C — Launcher plus a thin status header. Static command list, preceded by one line of orientation derived from existing state counts ("14 instances · 1 incomplete · 13 raw ideas").
- For: Orientation without a ranking engine; the header states facts, not recommendations.
- Against: Still reads state, so still needs the no-
.aidlc/path; the header's value overaidlc statusis modest.
Axis 2 — Argument collection depth
Option D — Launch only. Selecting an entry that needs arguments prints the command and exits.
Smallest surface, but leaves aidlc start — the most argument-heavy and most beginner-facing
command — undiscoverable, which undercuts the whole point.
Option E — Zero-arg run plus required-argument prompts. Commands needing nothing (doctor,
status, metrics, cost, continue) run immediately on selection. Commands with required
arguments get a minimal sub-prompt for those arguments only (knowledge query → term;
review → instance and artifact). Optional flags are never prompted, and start is handed to its
own existing template picker rather than wrapped in a second one.
Option F — Full wizard over every option. Every command walks its whole option set. Most discoverable, but duplicates commander's option metadata in a second place — the largest drift surface, and C3 means it cannot be generated from the existing reference.
How AIDLC solves it
interactive-cli-menu
Typing plain aidlc used to be a dead end — commander printed its help screen to stderr and exited
1, so a newcomer's first contact with the tool was a failing exit, and that screen was the only map
of a 19-command surface nesting three levels deep. Nothing in it conveyed lifecycle order.
Bare aidlc now opens an interactive launcher: twelve commands grouped by lifecycle stage — set up, run
health checks, begin new work, resume, inspect, maintain — navigated with arrow keys. Every row displays
the command it runs (Begin new work · aidlc start), so the menu teaches the scriptable CLI instead of
replacing it. aidlc menu is the same launcher, addressable by name.
Commands needing no arguments run the moment you pick them. The two that require arguments —
aidlc review <instance> <artifact> and aidlc knowledge query <term> — prompt for exactly those and
nothing else; optional flags are never asked about. aidlc start is deliberately handed its own
existing template picker rather than wrapped in a second one.
The launcher deliberately knows nothing about your lifecycle state. It answers "what commands exist",
never "what should I do next?" — that question stays owned by the aidlc skill, which cannot depend on
the CLI at runtime, so a second state-aware implementation in the CLI would drift from it permanently.
Everything scripted keeps working byte for byte. aidlc --help, aidlc cost, aidlc gate x --json,
aidlc bogus — identical output, identical streams, identical exit codes. Only the zero-argument
invocation changed, and only when a real terminal is attached: in CI, a pipe, or an agent's subprocess
it still prints help to stderr and exits 1, exactly as before.
blog-trail-deferrals
Two releases had been tagged, published, and deployed with no blog post, and
nothing reported it. publish: false in the blog manifest meant two different
things — v1.8.1's "never" (a patch release, settled) and v1.9.0's "not yet"
("revisit once the deployment record lands") — and validation only checked that a
note existed. Nothing ever re-read one.
- Declines are now typed.
decline: "never" | "deferred"is required wheneverpublishis false and rejected when it is true, enforced in both the JSON Schema andvalidateManifest. - A deferral re-checks itself.
generate-blog-post.mjs --check-deferredre-probes every deferred entry by running generation into a temp directory, because a note is prose about a blocker and only generation knows whether the blocker is still there. Non-zero exit when one would now succeed; zero while genuinely blocked, printing the blocker either way.release.shruns it before the version bump. - An entry's instance list is checked against git. It must cover or explicitly
omitsevery instanceinstancesForRangereports for its range. v1.9.1 had noinstancesfield at all, which is how it silently droppedrelease-blog-trail— the release that shipped the blog generator got no post about itself. - Both missing posts are on the site.
skills-the-app-owns(v1.9.0) andthe-lifecycle-tells-its-own-story(v1.9.1, covering both its instances).
Changes since v1.9.1
8 commits, 27 files, +1611/−28. No changes under packages/cli/ or
packages/content/.
User-visible effect
Two new posts at /blog/skills-the-app-owns/ and
/blog/the-lifecycle-tells-its-own-story/, both listed on /blog/, in the sitemap,
in /feed.xml, and in site search. The trail is continuous from v0.7.0 to v1.9.1.
For maintainers: a release now stops if a deferred post has become generatable, and a manifest entry cannot silently under-report which instances a release covered.
release-blog-tag-deadlock
./scripts/release.sh could not release a new version. Two requirements contradicted each other: blog
generation needs the tag to exist, because it resolves its act range with git log <prev>..<tag>; but the
manifest stored a date that had to equal the tag's creatordate, which cannot be known before tagging.
Adding the manifest entry first failed four tests; omitting it failed generation. Both were reproduced.
The fix deletes the duplicated fact instead of sequencing around it. date is gone from the manifest
schema, from all 19 entries, and from the validator — a stored copy is now rejected, not ignored, so it
cannot creep back and silently disagree with the tag it duplicates. render.mjs takes the date the
generator derives through the tagDates() helper that already existed. release-blog-trail/AC-47's own
wording — "derived from git for-each-ref --format='%(creatordate:iso-strict)'" — is now literally true,
where the code had stored a copy and asserted equality between the two.
release.sh now bumps, commits, and tags locally before generating, then folds the post into the release
commit by amending and re-points the tag. Nothing is pushed until pnpm test passes, and a failure trap
deletes the local tag and resets the tree, so a failed release leaves the repository exactly as it was.