Files
msd-core/src/command-aliases.cts
Tom Boucher fe64704ace enhance(#3588): add an opt-in commit_docs pre-commit hook (#3609)
* feat(#3588): add an opt-in commit_docs pre-commit hook

Final phase of epic #2292, scope narrowed to opt-in by maintainer decision:
default-on installation and the bin/install.js wiring it would have required
are explicitly out of scope.

Enabling is an explicit verb call. The hook is written to the repo's real hooks
dir resolved via git rev-parse --git-path hooks, so a linked worktree or
submodule whose .git is a FILE works rather than getting a literal .git/hooks
path. It refuses rather than overwrite a foreign pre-commit, refuses to delete
one it did not write, and refuses outright when core.hooksPath is already set --
a written-but-ignored hook is worse than a refusal. Ownership is detected by
marker presence, not byte-equality, so a user who appends a line does not make
it unrecognizable.

Deliberately NOT included: teaching cmdCheckCommit the per-phase commit_docs
tier. #3587 was still unmerged when this landed, and implementing precedence
against helpers that did not yet exist would have meant a second copy of the
resolution chain -- the divergence class this epic has spent three phases
fighting. That follows as its own change now that #3587 is on next.

The ordering constraint is recorded in the design doc: this must not merge
before #3587, or the hook would block a commit cmdCommit itself allows.

* fix(#3588): teach the commit_docs guard the per-phase tier and -z paths

Part 1, deferred until #3587 merged.

cmdCheckCommit read only project-level commit_docs, so once #3587 landed, a
phase with phase_commit_docs true under project false was ALLOWED by
query commit and BLOCKED by this guard -- and the hook shipped in this same
branch shells out to it. It now derives the staged phase via the single-owner
detectPhaseNumberFromFiles and resolves through #3587's own
resolveCommitDocsPolicy rather than a second precedence copy.

Also fixes a proven false negative in the harm direction. git diff --cached
--name-only C-style-quotes any path with non-ASCII or special characters, so a
staged .planning/cafe.md was emitted as a quoted string, failed
startsWith('.planning/'), and slipped past the guard entirely under
commit_docs:false. Reading with -z and splitting on NUL removes the quoting at
the source. The f.startsWith('.planning\\') branch was dead code under that
read -- git emits /-separated paths on every platform -- and is removed rather
than left implying coverage it never provided.

The earlier C7 test pinned the buggy behavior as intended; it now asserts the
file is detected and the commit refused.

Self-caught: the commit-docs-guard verb was wired into the routers by this
branch's earlier pass but missing from the top-level help listing.

* test(#3588): replace try/finally with t.after, add negative-routing cases

Standards review findings.

CONTRIBUTING bans try/finally inside a test body outright -- it masks failures
-- and B8 used one for worktree cleanup. Now t.after(), assertions unchanged.

The new commit-docs-guard command family had zero negative-routing coverage,
which CONTRIBUTING requires for any change to command dispatch. B11-B15 cover
no subcommand, unknown, empty string, whitespace-only and a flag-shaped value,
each asserting non-zero exit, a structured error, no stack trace, and -- the
one that matters for a command that writes into a user's repo -- that NO hook
is written in any of them.

Those tests were verified to fail when routeCommitDocsGuard's else-branch is
neutered, so they exercise the routing guard rather than any convenient error
path.

Also made two error() calls' control flow explicit with a return; they were
safe only because error() is typed never two files away.

* chore(#3588): backfill changeset pr number to 3609

* test(#3588): skip Windows-unrepresentable fixtures on win32

CI's Windows shards caught two of my own tests: fixtures whose filenames
contain a quote and a backslash. Both are illegal on Windows -- backslash is
the path separator, quote is invalid on NTFS -- so fixture creation failed
before any assertion ran.

Test-portability defect, not a production one. Those inputs cannot exist on
that platform, so the guard has nothing to detect there.

Both now check process.platform FIRST, before any fs or git call, and use
t.skip() rather than a bare return -- a bare return registers as a PASS and
would hide the gap it is meant to record. Each carries a comment saying the
input is unrepresentable rather than unverified, so nobody later re-enables it.

No padding added: the cafe.md case already exercises git's C-quoting path on
every platform, since non-ASCII names are legal on NTFS.

This is exactly the coverage the Linux-only remote matrix cannot provide, which
the PR body already stated -- CI's Windows shards are what caught it.

---------

Co-authored-by: sim <sim@local>
2026-08-18 00:25:32 -04:00

972 lines
18 KiB
TypeScript

/**
* state.*, verify.*, init.*, phase.*, phases.*, validate.*, roadmap.*, and non-family alias/subcommand metadata for CJS routing.
*
* ADR-457 build-at-publish: the hand-written bin/lib/command-aliases.cjs collapsed
* to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
* from the prior hand-written .cjs; only types are added.
*/
interface CommandAlias {
canonical: string;
aliases: string[];
subcommand: string;
mutation: boolean;
}
interface NonFamilyCommandAlias {
canonical: string;
aliases: string[];
mutation: boolean;
}
export const STATE_COMMAND_ALIASES: CommandAlias[] = [
{
"canonical": "state.load",
"aliases": [],
"subcommand": "load",
"mutation": false
},
{
"canonical": "state.json",
"aliases": [
"state json"
],
"subcommand": "json",
"mutation": false
},
{
"canonical": "state.get",
"aliases": [
"state get"
],
"subcommand": "get",
"mutation": false
},
{
"canonical": "state.update",
"aliases": [
"state update"
],
"subcommand": "update",
"mutation": true
},
{
"canonical": "state.patch",
"aliases": [
"state patch"
],
"subcommand": "patch",
"mutation": true
},
{
"canonical": "state.begin-phase",
"aliases": [
"state begin-phase"
],
"subcommand": "begin-phase",
"mutation": true
},
{
"canonical": "state.advance-plan",
"aliases": [
"state advance-plan"
],
"subcommand": "advance-plan",
"mutation": true
},
{
"canonical": "state.record-metric",
"aliases": [
"state record-metric"
],
"subcommand": "record-metric",
"mutation": true
},
{
"canonical": "state.update-progress",
"aliases": [
"state update-progress"
],
"subcommand": "update-progress",
"mutation": true
},
{
"canonical": "state.add-decision",
"aliases": [
"state add-decision"
],
"subcommand": "add-decision",
"mutation": true
},
{
"canonical": "state.add-blocker",
"aliases": [
"state add-blocker"
],
"subcommand": "add-blocker",
"mutation": true
},
{
"canonical": "state.resolve-blocker",
"aliases": [
"state resolve-blocker"
],
"subcommand": "resolve-blocker",
"mutation": true
},
{
"canonical": "state.record-session",
"aliases": [
"state record-session"
],
"subcommand": "record-session",
"mutation": true
},
{
"canonical": "state.signal-waiting",
"aliases": [
"state signal-waiting"
],
"subcommand": "signal-waiting",
"mutation": true
},
{
"canonical": "state.signal-resume",
"aliases": [
"state signal-resume"
],
"subcommand": "signal-resume",
"mutation": true
},
{
"canonical": "state.planned-phase",
"aliases": [
"state planned-phase"
],
"subcommand": "planned-phase",
"mutation": true
},
{
"canonical": "state.validate",
"aliases": [
"state validate"
],
"subcommand": "validate",
"mutation": false
},
{
"canonical": "state.sync",
"aliases": [
"state sync"
],
"subcommand": "sync",
"mutation": true
},
{
"canonical": "state.prune",
"aliases": [
"state prune"
],
"subcommand": "prune",
"mutation": true
},
{
"canonical": "state.rebuild",
"aliases": [
"state rebuild"
],
"subcommand": "rebuild",
"mutation": true
},
{
"canonical": "state.milestone-switch",
"aliases": [
"state milestone-switch"
],
"subcommand": "milestone-switch",
"mutation": true
},
{
"canonical": "state.add-roadmap-evolution",
"aliases": [
"state add-roadmap-evolution"
],
"subcommand": "add-roadmap-evolution",
"mutation": true
}
];
export const VERIFY_COMMAND_ALIASES: CommandAlias[] = [
{
"canonical": "verify.plan-structure",
"aliases": [
"verify plan-structure"
],
"subcommand": "plan-structure",
"mutation": false
},
{
"canonical": "verify.phase-completeness",
"aliases": [
"verify phase-completeness"
],
"subcommand": "phase-completeness",
"mutation": false
},
{
"canonical": "verify.references",
"aliases": [
"verify references"
],
"subcommand": "references",
"mutation": false
},
{
"canonical": "verify.commits",
"aliases": [
"verify commits"
],
"subcommand": "commits",
"mutation": false
},
{
"canonical": "verify.artifacts",
"aliases": [
"verify artifacts"
],
"subcommand": "artifacts",
"mutation": false
},
{
"canonical": "verify.key-links",
"aliases": [
"verify key-links"
],
"subcommand": "key-links",
"mutation": false
},
{
"canonical": "verify.schema-drift",
"aliases": [
"verify schema-drift"
],
"subcommand": "schema-drift",
"mutation": false
},
{
"canonical": "verify.codebase-drift",
"aliases": [
"verify codebase-drift"
],
"subcommand": "codebase-drift",
"mutation": false
}
];
export const INIT_COMMAND_ALIASES: CommandAlias[] = [
{
"canonical": "init.execute-phase",
"aliases": [
"init execute-phase"
],
"subcommand": "execute-phase",
"mutation": false
},
{
"canonical": "init.plan-phase",
"aliases": [
"init plan-phase"
],
"subcommand": "plan-phase",
"mutation": false
},
{
"canonical": "init.new-project",
"aliases": [
"init new-project"
],
"subcommand": "new-project",
"mutation": false
},
{
"canonical": "init.new-milestone",
"aliases": [
"init new-milestone"
],
"subcommand": "new-milestone",
"mutation": false
},
{
"canonical": "init.onboard",
"aliases": [
"init onboard"
],
"subcommand": "onboard",
"mutation": false
},
{
"canonical": "init.quick",
"aliases": [
"init quick"
],
"subcommand": "quick",
"mutation": false
},
{
"canonical": "init.ingest-docs",
"aliases": [
"init ingest-docs"
],
"subcommand": "ingest-docs",
"mutation": false
},
{
"canonical": "init.resume",
"aliases": [
"init resume"
],
"subcommand": "resume",
"mutation": false
},
{
"canonical": "init.verify-work",
"aliases": [
"init verify-work"
],
"subcommand": "verify-work",
"mutation": false
},
{
"canonical": "init.phase-op",
"aliases": [
"init phase-op"
],
"subcommand": "phase-op",
"mutation": false
},
{
"canonical": "init.code-review",
"aliases": [
"init code-review"
],
"subcommand": "code-review",
"mutation": false
},
{
"canonical": "init.review",
"aliases": [
"init review"
],
"subcommand": "review",
"mutation": false
},
{
"canonical": "init.discuss-phase-assumptions",
"aliases": [
"init discuss-phase-assumptions"
],
"subcommand": "discuss-phase-assumptions",
"mutation": false
},
{
"canonical": "init.todos",
"aliases": [
"init todos"
],
"subcommand": "todos",
"mutation": false
},
{
"canonical": "init.milestone-op",
"aliases": [
"init milestone-op"
],
"subcommand": "milestone-op",
"mutation": false
},
{
"canonical": "init.map-codebase",
"aliases": [
"init map-codebase"
],
"subcommand": "map-codebase",
"mutation": false
},
{
"canonical": "init.progress",
"aliases": [
"init progress"
],
"subcommand": "progress",
"mutation": false
},
{
"canonical": "init.manager",
"aliases": [
"init manager"
],
"subcommand": "manager",
"mutation": false
},
{
"canonical": "init.complete-milestone",
"aliases": [
"init complete-milestone"
],
"subcommand": "complete-milestone",
"mutation": false
},
{
"canonical": "init.autonomous",
"aliases": [
"init autonomous"
],
"subcommand": "autonomous",
"mutation": false
},
{
"canonical": "init.docs-update",
"aliases": [
"init docs-update"
],
"subcommand": "docs-update",
"mutation": false
},
{
"canonical": "init.update",
"aliases": [
"init update"
],
"subcommand": "update",
"mutation": false
},
{
"canonical": "init.transition",
"aliases": [
"init transition"
],
"subcommand": "transition",
"mutation": false
},
{
"canonical": "init.debug",
"aliases": [
"init debug"
],
"subcommand": "debug",
"mutation": false
},
{
"canonical": "init.new-workspace",
"aliases": [
"init new-workspace"
],
"subcommand": "new-workspace",
"mutation": false
},
{
"canonical": "init.list-workspaces",
"aliases": [
"init list-workspaces"
],
"subcommand": "list-workspaces",
"mutation": false
},
{
"canonical": "init.remove-workspace",
"aliases": [
"init remove-workspace"
],
"subcommand": "remove-workspace",
"mutation": false
}
];
export const PHASE_COMMAND_ALIASES: CommandAlias[] = [
{
"canonical": "phase.uat-passed",
"aliases": [
"phase uat-passed"
],
"subcommand": "uat-passed",
"mutation": false
},
{
"canonical": "phase.next-decimal",
"aliases": [
"phase next-decimal"
],
"subcommand": "next-decimal",
"mutation": false
},
{
"canonical": "phase.add",
"aliases": [
"phase add"
],
"subcommand": "add",
"mutation": true
},
{
"canonical": "phase.add-batch",
"aliases": [
"phase add-batch"
],
"subcommand": "add-batch",
"mutation": true
},
{
"canonical": "phase.insert",
"aliases": [
"phase insert"
],
"subcommand": "insert",
"mutation": true
},
{
"canonical": "phase.remove",
"aliases": [
"phase remove"
],
"subcommand": "remove",
"mutation": true
},
{
"canonical": "phase.complete",
"aliases": [
"phase complete"
],
"subcommand": "complete",
"mutation": true
},
{
"canonical": "phase.scaffold",
"aliases": [
"phase scaffold"
],
"subcommand": "scaffold",
"mutation": true
},
{
"canonical": "phase.list-plans",
"aliases": [
"phase list-plans"
],
"subcommand": "list-plans",
"mutation": false
}
];
export const PHASES_COMMAND_ALIASES: CommandAlias[] = [
{
"canonical": "phases.list",
"aliases": [
"phases list"
],
"subcommand": "list",
"mutation": false
},
{
"canonical": "phases.clear",
"aliases": [
"phases clear"
],
"subcommand": "clear",
"mutation": true
},
{
"canonical": "phases.archive",
"aliases": [
"phases archive"
],
"subcommand": "archive",
"mutation": true
}
];
export const VALIDATE_COMMAND_ALIASES: CommandAlias[] = [
{
"canonical": "validate.consistency",
"aliases": [
"validate consistency"
],
"subcommand": "consistency",
"mutation": false
},
{
"canonical": "validate.health",
"aliases": [
"validate health"
],
"subcommand": "health",
"mutation": false
},
{
"canonical": "validate.agents",
"aliases": [
"validate agents"
],
"subcommand": "agents",
"mutation": false
},
{
"canonical": "validate.context",
"aliases": [
"validate context"
],
"subcommand": "context",
"mutation": false
}
];
export const ROADMAP_COMMAND_ALIASES: CommandAlias[] = [
{
"canonical": "roadmap.analyze",
"aliases": [
"roadmap analyze"
],
"subcommand": "analyze",
"mutation": false
},
{
"canonical": "roadmap.milestone-scope",
"aliases": [
"roadmap milestone-scope"
],
"subcommand": "milestone-scope",
"mutation": false
},
{
"canonical": "roadmap.get-phase",
"aliases": [
"roadmap get-phase"
],
"subcommand": "get-phase",
"mutation": false
},
{
"canonical": "roadmap.update-plan-progress",
"aliases": [
"roadmap update-plan-progress"
],
"subcommand": "update-plan-progress",
"mutation": true
},
{
"canonical": "roadmap.annotate-dependencies",
"aliases": [
"roadmap annotate-dependencies"
],
"subcommand": "annotate-dependencies",
"mutation": true
},
{
"canonical": "roadmap.validate",
"aliases": [
"roadmap validate"
],
"subcommand": "validate",
"mutation": false
},
{
"canonical": "roadmap.upgrade",
"aliases": [
"roadmap upgrade"
],
"subcommand": "upgrade",
"mutation": true
}
];
export const NON_FAMILY_COMMAND_ALIASES: NonFamilyCommandAlias[] = [
{
"canonical": "agent.classify-failure",
"aliases": [
"agent classify-failure"
],
"mutation": false
},
{
"canonical": "check-commit",
"aliases": [],
"mutation": true
},
{
"canonical": "check.decision-coverage-plan",
"aliases": [
"check decision-coverage-plan"
],
"mutation": false
},
{
"canonical": "check.decision-coverage-verify",
"aliases": [
"check decision-coverage-verify"
],
"mutation": false
},
{
"canonical": "commit",
"aliases": [],
"mutation": true
},
{
"canonical": "commit-docs-guard.disable",
"aliases": [
"commit-docs-guard disable"
],
"mutation": true
},
{
"canonical": "commit-docs-guard.enable",
"aliases": [
"commit-docs-guard enable"
],
"mutation": true
},
{
"canonical": "commit-to-subrepo",
"aliases": [],
"mutation": true
},
{
"canonical": "config-ensure-section",
"aliases": [],
"mutation": true
},
{
"canonical": "config-new-project",
"aliases": [],
"mutation": true
},
{
"canonical": "config-set",
"aliases": [],
"mutation": true
},
{
"canonical": "config-set-model-profile",
"aliases": [],
"mutation": true
},
{
"canonical": "docs-init",
"aliases": [],
"mutation": true
},
{
"canonical": "frontmatter.get",
"aliases": [],
"mutation": false
},
{
"canonical": "frontmatter.merge",
"aliases": [],
"mutation": true
},
{
"canonical": "frontmatter.set",
"aliases": [],
"mutation": true
},
{
"canonical": "frontmatter.validate",
"aliases": [
"frontmatter validate"
],
"mutation": true
},
{
"canonical": "generate-claude-md",
"aliases": [],
"mutation": true
},
{
"canonical": "generate-claude-profile",
"aliases": [],
"mutation": true
},
{
"canonical": "generate-dev-preferences",
"aliases": [],
"mutation": true
},
{
"canonical": "learnings.copy",
"aliases": [
"learnings copy"
],
"mutation": true
},
{
"canonical": "learnings.delete",
"aliases": [
"learnings delete"
],
"mutation": true
},
{
"canonical": "learnings.prune",
"aliases": [
"learnings prune"
],
"mutation": true
},
{
"canonical": "milestone.complete",
"aliases": [
"milestone complete"
],
"mutation": true
},
{
"canonical": "phase.mvp-mode",
"aliases": [
"phase mvp-mode"
],
"mutation": false
},
{
"canonical": "progress.bar",
"aliases": [
"progress bar"
],
"mutation": false
},
{
"canonical": "requirements.mark-complete",
"aliases": [
"requirements mark-complete"
],
"mutation": true
},
{
"canonical": "requirements.ready-ids",
"aliases": [
"requirements ready-ids"
],
"mutation": false
},
{
"canonical": "requirements.revert-phase",
"aliases": [
"requirements revert-phase"
],
"mutation": true
},
{
"canonical": "stats.json",
"aliases": [
"stats json"
],
"mutation": false
},
{
"canonical": "task.is-behavior-adding",
"aliases": [
"task is-behavior-adding"
],
"mutation": false
},
{
"canonical": "template.fill",
"aliases": [],
"mutation": true
},
{
"canonical": "template.select",
"aliases": [
"template select"
],
"mutation": true
},
{
"canonical": "todo.complete",
"aliases": [
"todo complete"
],
"mutation": true
},
{
"canonical": "todo.match-phase",
"aliases": [
"todo match-phase"
],
"mutation": false
},
{
"canonical": "uat.render-checkpoint",
"aliases": [
"uat render-checkpoint"
],
"mutation": false
},
{
"canonical": "verify-summary",
"aliases": [
"verify.summary",
"verify summary"
],
"mutation": false
},
{
"canonical": "workstream.complete",
"aliases": [
"workstream complete"
],
"mutation": true
},
{
"canonical": "workstream.create",
"aliases": [
"workstream create"
],
"mutation": true
},
{
"canonical": "workstream.list",
"aliases": [
"workstream list"
],
"mutation": false
},
{
"canonical": "workstream.progress",
"aliases": [
"workstream progress"
],
"mutation": true
},
{
"canonical": "workstream.set",
"aliases": [
"workstream set"
],
"mutation": true
},
{
"canonical": "write-profile",
"aliases": [],
"mutation": true
}
];
export const STATE_SUBCOMMANDS: string[] = STATE_COMMAND_ALIASES.map((entry) => entry.subcommand);
export const VERIFY_SUBCOMMANDS: string[] = VERIFY_COMMAND_ALIASES.map((entry) => entry.subcommand);
export const INIT_SUBCOMMANDS: string[] = INIT_COMMAND_ALIASES.map((entry) => entry.subcommand);
export const PHASE_SUBCOMMANDS: string[] = PHASE_COMMAND_ALIASES.map((entry) => entry.subcommand);
export const PHASES_SUBCOMMANDS: string[] = PHASES_COMMAND_ALIASES.map((entry) => entry.subcommand);
export const VALIDATE_SUBCOMMANDS: string[] = VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand);
export const ROADMAP_SUBCOMMANDS: string[] = ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand);
export const EVAL_COMMAND_ALIASES: CommandAlias[] = [
{
"canonical": "eval.score",
"aliases": ["eval score"],
"subcommand": "score",
"mutation": false
}
];
export const EVAL_SUBCOMMANDS: string[] = EVAL_COMMAND_ALIASES.map((entry) => entry.subcommand);