Skip to content

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 the init case). Delivers G3 completely.
  • Against: Cannot say "continue cost-tracking at 85%". A user with in-flight work still has to run status to 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 over aidlc status is 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 whenever publish is false and rejected when it is true, enforced in both the JSON Schema and validateManifest.
  • A deferral re-checks itself. generate-blog-post.mjs --check-deferred re-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.sh runs it before the version bump.
  • An entry's instance list is checked against git. It must cover or explicitly omits every instance instancesForRange reports for its range. v1.9.1 had no instances field at all, which is how it silently dropped release-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) and the-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.