@abgov/nx-agent
v1.12.0
Published
Government of Alberta - Nx plugin for AI-agent development tooling.
Keywords
Readme
@abgov/nx-agent
Nx plugin for AI-agent development tooling — capabilities that steer a coding agent's day-to-day
work, as opposed to @abgov/nx-adsp/@abgov/nx-oc's scaffolding and deployment concerns.
TLDR
# 1. Install
npm i -D @abgov/nx-agent
# 2. Set up recommended AI-agent tooling for this workspace
npx nx g @abgov/nx-agent:initinit
A single, prescriptive entry point — run once per workspace. It's expected to grow as more capabilities are added; running it again after an upgrade re-applies whatever's new without disturbing anything it already set up.
Currently sets up:
A Husky pre-commit hook (
.husky/pre-commit) that runsnx affectedlint/test/build against your staged changes before every commit:git diff --cached --name-only --diff-filter=ACMR | npx nx affected -t lint,test,build --stdinAdds
huskyas a devDependency and a"prepare": "husky"script if not already present.A secret-scanning hook block, appended to the same
.husky/pre-commit, scanning staged files for committed credentials with secretlint:secretlint_files=$(git diff --cached --name-only --diff-filter=ACMR) if [ -n "$secretlint_files" ]; then echo "$secretlint_files" | xargs npx secretlint || exit 1 fiAdds
secretlintand@secretlint/secretlint-rule-preset-recommendas devDependencies, and a.secretlintrc.jsonif one doesn't already exist (never overwritten once created — rules are the kind of thing a team tunes, unlike the AGENTS.md guidance below).Baseline
.gitignoreentries for common local-credential filenames —.env.local,.env.*.local,*.pem,*.key,id_rsa,id_ed25519,credentials.json— added only if missing, appended alongside whatever's already there. Deliberately excludes bare.env: it's dual-purpose (plain workspace config as well as secrets), so a blanket rule would be a false positive on legitimate use. This is preventive rather than detective — once a pattern is in.gitignore, git itself refuses to stage a matching file viagit add ./git add -A, so no separate pre-commit check is needed on top of it. It does nothing for a file that was already tracked beforeinitran; gitignore never retroactively untracks anything.One consolidated
AGENTS.mdsection,## Working with a coding agent, organized into six###groups — ordered roughly by stakes, highest first — each holding several related**items rather than one flat, ever-growing list of top-level headings:- Security and safety — secrets, PII/sensitive data, destructive operations, untrusted content and instructions, trust boundaries.
- Dependency hygiene — choosing a dependency (existence/currency/license, and checking whether an existing dependency already covers the need before adding another one).
- Verifying your work — the pre-commit-check habit above, plus respecting whatever style/format/complexity tooling a project already has configured.
- Version control practices — atomic Conventional Commits, GitHub Flow, linear history.
- Conventions and consistency — ubiquitous language (domain vocabulary), matching this project's own established patterns, following framework/library idioms, and checking a connected MCP server before recalling a platform/design-system API from memory.
- Code quality — scope discipline, comments (why, not what), reuse before reinventing, error handling, TODO transparency, test quality.
The whole section is centrally maintained: re-running
initrefreshes it in place rather than leaving it frozen at first-generation wording, assembled fromguidance/<group>/<item>.mdfiles (one file per item, grouped into folders matching the six groups above) so the content previews as plain markdown rather than escaped TypeScript string literals.initis also self-migrating — if it finds section markers from an older, pre-consolidation version, it removes them and writes the current structure in their place, so simply re-runninginitis enough to pick up changes; no separate migration step.Also ensures
CLAUDE.mdimports it (@AGENTS.md, appended if missing) — Claude Code readsCLAUDE.mdnatively, notAGENTS.mddirectly, so without this the guidance above never reaches a Claude Code session. Same one-line convention nx-adsp's own generators already use.A Claude Code deny-list (
.claude/settings.json), hard-blocking shell patterns with no legitimate agent-initiated use case —rm -rfrooted at//~/$HOME,sudo,mkfs,chmod -R 777 /, system shutdown/reboot, history-rewriting/reflog-destroying git commands, and whole-namespace OpenShift/Kubernetes deletion — absolute per Claude Code's own permission model, holding even under--dangerously-skip-permissions. Merges into an existing file rather than overwriting it. No equivalent exists yet for other tools (checked GitHub Copilot CLI specifically — its absolute deny mechanism is CLI-flag-only, with no repo-committed file to seed).
Options
| Option | Default | Description |
| --------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| targets | lint,test,build | Targets run by both the pre-commit hook and the AGENTS.md guidance's self-check command |
| base | main | Base branch used only in the AGENTS.md guidance's self-check command (not the pre-commit hook, which always diffs against staged changes) |
npx nx g @abgov/nx-agent:init --targets=lint,test --base=developdomain-term
Adds one domain term — the ubiquitous language init's guidance asks the agent to use, but gives
it nowhere to check or record. Unlike init, this is repeatable — run it once per term, whenever
one needs adding, not once per workspace:
npx nx g @abgov/nx-agent:domain-term CaseCreates project-docs/domain-terms/case.md:
---
term: Case
aliases: []
not_confused_with: []
project-docs-ancestors: []
resolves: []
---
<!-- Definition: describe this term in the domain's own language. -->term— the canonical name, matching the filename.aliases— other words that mean the same thing.not_confused_with— similar-sounding terms this one is deliberately distinct from, and why.project-docs-ancestors— otherproject-docs/artifacts this term derives from (seeproject-docs-lineagebelow) — set via--project-docs-ancestors, never by hand.resolves— which of those ancestors (typically anopen-question/blocker) this term specifically resolves, not just builds on — set via--resolves, a distinct flag from--project-docs-ancestorseven though the same ref also lands there.
One file per term rather than a single flat glossary, so frontmatter (a per-file construct in every tool that uses the term) is meaningful, and so listing the folder — cheap, just filenames — is enough to check the existing vocabulary before coining a new name.
Also bootstraps project-docs/domain-terms/README.md on first use, explaining the convention to
whoever (human or agent) opens the folder next. That file has no value on its own — it exists only
to explain the convention for the term about to be added — so there's no separate "set up the
glossary" generator; domain-term composes it as an internal step.
Options
| Option | Default | Description |
| ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| term | — (required, positional) | The canonical domain term, as domain experts use it |
| project | workspace root | Scope the term to a specific project's project-docs/domain-terms/ instead — prefer this whenever the term already has a natural code home (usually a domain library), not just workspace root by default |
| projectDocsAncestors | none | Paths to existing project-docs/ artifacts this term derives from — repeatable; resolved into project-docs-ancestors (see below) |
| resolves | none | Paths to existing open-question/blocker artifacts this term resolves — repeatable; also added to project-docs-ancestors, but recorded distinctly so project-docs-lineage can report the resolution as such |
npx nx g @abgov/nx-agent:domain-term Case --project=domain-lib
npx nx g @abgov/nx-agent:domain-term "Collision Report" --project-docs-ancestors=project-docs/bounded-contexts/collision-reporting.mdRe-adding a term that already exists throws rather than silently overwriting or duplicating it —
edit the file directly instead. A --project-docs-ancestors path that doesn't resolve to an existing
artifact throws the same way, before anything is written.
bounded-context
A domain term's meaning only holds within a bounded context — the boundary past which the same word can mean something else entirely. Adds one:
npx nx g @abgov/nx-agent:bounded-context "Collision Reporting"Creates project-docs/bounded-contexts/collision-reporting.md:
---
name: Collision Reporting
aliases: []
not_confused_with: []
project-docs-ancestors: []
resolves: []
---
<!-- Definition: describe what's inside this boundary, and what's explicitly outside it. -->Options
| Option | Default | Description |
| ---------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | — (required, positional) | The canonical name of the bounded context |
| project | workspace root | Scope the context to a specific project's project-docs/bounded-contexts/ instead — prefer this whenever the context already has a natural code home (usually a domain library), not just workspace root by default |
| projectDocsAncestors | none | Paths to existing project-docs/ artifacts this context derives from — repeatable; resolved into project-docs-ancestors |
| resolves | none | Paths to existing open-question/blocker artifacts this context resolves — repeatable; also added to project-docs-ancestors, but recorded distinctly so project-docs-lineage can report the resolution as such |
npx nx g @abgov/nx-agent:bounded-context "Collision Reporting" --project=domain-libRe-adding a context that already exists throws rather than silently overwriting or duplicating it.
domain-model
The actual design — aggregates, entities, invariants — built from a bounded context and the domain terms it's composed from:
npx nx g @abgov/nx-agent:domain-model "Collision Report Lifecycle" \
--project-docs-ancestors=project-docs/bounded-contexts/collision-reporting.md \
--project-docs-ancestors=project-docs/domain-terms/collision-report.mdCreates project-docs/domain-models/collision-report-lifecycle.md:
---
name: Collision Report Lifecycle
project-docs-ancestors: [bounded-contexts:collision-reporting, domain-terms:collision-report]
resolves: []
---
<!-- Design: describe the aggregates, entities, value objects, and invariants here. -->Options
| Option | Default | Description |
| ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | — (required, positional) | The canonical name of the domain model |
| project | workspace root | Scope the model to a specific project's project-docs/domain-models/ instead — prefer this whenever the model already has a natural code home (usually a domain library), not just workspace root by default |
| projectDocsAncestors | none | Paths to existing project-docs/ artifacts this model derives from — repeatable; normally the bounded context it belongs to plus the domain terms it's composed from |
| resolves | none | Paths to existing open-question/blocker artifacts this model resolves — repeatable; also added to project-docs-ancestors, but recorded distinctly so project-docs-lineage can report the resolution as such |
Re-adding a model that already exists throws rather than silently overwriting or duplicating it. A
--project-docs-ancestors path that doesn't resolve to an existing artifact throws the same way,
before anything is written.
open-question
Something undecided that can't be guessed at — needs input, a decision, or more information before work depending on it can proceed:
npx nx g @abgov/nx-agent:open-question "Reviewer Authorization" \
--project-docs-ancestors=project-docs/requirements/reviewer-role.mdCreates project-docs/open-questions/reviewer-authorization.md:
---
project-docs-ancestors: [requirements:reviewer-role]
resolves: []
---
<!-- What's undecided, and why it can't be guessed at. -->Options
| Option | Default | Description |
| ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| question | — (required, positional) | A short slug for what's undecided |
| project | workspace root | Scope the question to a specific project's project-docs/open-questions/ instead — prefer this whenever it already has a natural code home |
| projectDocsAncestors | none | Paths to existing project-docs/ artifacts this question grounds on — repeatable; an open question can ground on any artifact kind |
A question is never marked resolved by editing its own file — some other artifact resolves it via
its own --resolves flag (see domain-term/bounded-context/domain-model above). Re-adding a
question that already exists throws rather than silently overwriting or duplicating it.
blocker
An existing artifact that needs revision — something already established but wrong, incomplete, or in conflict with something discovered later:
npx nx g @abgov/nx-agent:blocker "Cant Ship Payment Flow" \
--project-docs-ancestors=project-docs/domain-models/collision-report-lifecycle.mdCreates project-docs/blockers/cant-ship-payment-flow.md:
---
project-docs-ancestors: [domain-models:collision-report-lifecycle]
resolves: []
---
<!-- What needs fixing, and why it is blocking. -->Options
| Option | Default | Description |
| ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| description | — (required, positional) | A short slug for what needs fixing |
| project | workspace root | Scope the blocker to a specific project's project-docs/blockers/ instead — prefer this whenever it already has a natural code home |
| projectDocsAncestors | none | Paths to existing project-docs/ artifacts this blocker relates to — repeatable; typically the artifact that needs revision |
Same resolution model as open-question: never mark it resolved by editing its own file — the
artifact that actually revises the thing it's blocking resolves it via --resolves.
project-docs-lineage
Scans the whole workspace for project-docs/ artifacts and project-docs-ancestors references —
across both doc frontmatter and code comments — and writes the resulting graph to
.nx-agent/lineage.json (gitignored automatically; it's fully derived from other files, so
committing it would just create a second, driftable source of truth). Throws if it finds a
reference that doesn't resolve to anything; reports an artifact nothing references yet (an orphan)
without failing, since that's a normal, temporary state, not a mistake.
npx nx g @abgov/nx-agent:project-docs-lineage
npx nx g @abgov/nx-agent:project-docs-lineage --dry-run # compute and report, write nothingThe project-docs-ancestors convention itself: a directive used identically in frontmatter (a YAML
list) and code comments (comma-separated on one line), shaped <type>[:<id>][#fragment]. type is
the literal project-docs/ subfolder name — no singular/plural guessing, so a new artifact kind
works immediately with no schema to update. id is the filename minus extension, present only for
a collection artifact (many instances, one file each, inside a type-named folder); a singular
artifact (exactly one file directly under project-docs/, no subfolder) is referenced by its bare
type, no id — e.g. domain-terms:case for a term, architecture-overview alone for a one-off doc.
An optional project qualifier (<project>/type:id) scopes the reference to that project's own
project-docs/ instead of the workspace root's — never implicit, even from within that same
project, so a reference's meaning never depends on where it's found.
Not yet wired into the pre-commit hook or an Nx inferred plugin — run it yourself (or --dry-run
it in your own CI) after adding or changing a reference.
Also reports which open-question/blocker artifacts are still open versus resolved — see
resolutionStatus below.
project-docs/artifact-schema.json
An artifact-producing generator (domain-term, bounded-context, domain-model, open-question,
blocker) self-registers its own entry here on first use, declaring what ancestor type its kind
normally expects — e.g. domain-terms expects a bounded-contexts ancestor — and, separately,
whether its kind has a resolution lifecycle at all:
{
"bounded-contexts": { "expectedAncestorTypes": [] },
"domain-terms": { "expectedAncestorTypes": ["bounded-contexts"] },
"domain-models": { "expectedAncestorTypes": ["bounded-contexts", "domain-terms"] },
"open-questions": { "expectedAncestorTypes": [], "tracksResolution": true },
"blockers": { "expectedAncestorTypes": [], "tracksResolution": true }
}project-docs-lineage reads this generically — it has no knowledge of any specific type baked in, so
a hand-added entry for a custom artifact kind gets the same checks for free. expectedAncestorTypes is
an all-of list, not any-of: domain-models above requires an ancestor of both bounded-contexts
and domain-terms, not either — a model with only one is still missing part of the vocabulary it
should be built from. An artifact whose type has an entry here but is missing an ancestor of one of
the expected types is reported (not thrown, since this is a convention nudge rather than a hard rule)
as unscoped in .nx-agent/lineage.json's violations.
tracksResolution: true is what makes open-questions/blockers show up in resolutionStatus
(below) — a custom artifact kind with the same lifecycle (something that starts undecided/blocking
and gets settled by another artifact) gets the same open/resolved report for free by declaring it.
resolutionStatus
violations.resolutionStatus in .nx-agent/lineage.json splits every artifact whose type has
tracksResolution: true into open and resolved:
{ "open": ["open-questions:reviewer-authorization"], "resolved": ["blockers:cant-ship-payment-flow"] }"Resolved" means some artifact's own resolves field names this key — not merely that something
references it via project-docs-ancestors. That distinction matters: a blocker or another
open-question citing an existing one because it's still unresolved would, under a looser
"anything references it" test, get misread as having resolved it. A deferral (explicitly punted, not
decided) isn't a third computed bucket — it stays open, with the why left to the artifact's own
prose, same as an orphan doesn't try to distinguish "temporary" from "abandoned."
Programmatic access
@abgov/nx-agent also exports two read functions — its first public, importable API; everything
else in the package is consumed only via nx g @abgov/nx-agent:x. Meant for a caller that needs a
stable contract (an ESLint rule, an agent resolving context for a file it's about to touch), not
one that wants to parse .nx-agent/lineage.json directly — that file's exact shape stays an
internal implementation detail, free to change as long as these signatures don't.
import { getAncestors, getDescendants } from '@abgov/nx-agent';
getAncestors(tree, 'apps/my-service/src/routes/collision-reports.ts');
// => [{ type: 'domain-terms', id: 'collision-report' }]
getDescendants(tree, 'domain-terms:collision-report');
// => [{ file: 'apps/my-service/src/routes/collision-reports.ts' }]The two directions have genuinely different costs, which the API makes explicit rather than
hiding: getAncestors reads just the one file you ask about (the reference is embedded in it), so
it's always cheap. getDescendants has no such shortcut — nothing an artifact stores on itself
says who points at it, since references are backward-only by design — so answering it means
checking every file in the workspace. It rebuilds fresh on every call rather than trusting a
persisted cache that could go stale the moment something changes without project-docs-lineage
re-running (measured on this ~14k-file workspace: about 50ms end to end, which is why that's an
acceptable default rather than something worth caching).
Both take an optional depth (default 1, direct parents/children only — pass Infinity for the
full ancestry/descendancy). depth doesn't change the cost model above, it just decides whether to
pay it: at depth 1, getAncestors still touches only the one file and getDescendants still
does its one full-workspace scan. Beyond that, each function builds the graph once — not once per
hop — and walks it in memory, so asking for depth: 5 costs the same one-time build as depth: 2.
A cycle in the references (two artifacts deriving from each other) terminates correctly rather than
looping forever.
getAncestors(tree, 'apps/my-service/src/routes/collision-reports.ts', Infinity);
// everything this file derives from, transitively