0.10.0
Minor npm ↗- 0.10.0 is the v2 migration release.
ndx migratemoves a v1 PRD (prd_tree/) to the v2 product and change layers,ndx initstarts new projects on v2, and rex commands on a v1 PRD print one deprecation line namingndx migrate; the release after this one refuses v1 PRDs. Every@n-dx/*package is versioned together, so they all move to 0.10.0. #749 ndx configskips__proto__,constructorandprototypekeys while merging.n-dx.jsonand.n-dx.local.json. A literal"__proto__"key in either file used to set the merged object's prototype, so every reader of the project config saw fields the file never wrote as settings. #846ndx config <pkg>.<key> <value>refuses a key with a__proto__,constructororprototypesegment.ndx config llm.__proto__.polluted xfollowedconfigs.llm["__proto__"]ontoObject.prototypeand assigned there, in the CLI's own process; the write was never persisted, but the command now exits non-zero naming the segment and leaves every config file untouched. hench'ssetConfigValueapplies the same guard behind itsCONFIG_FIELDSallowlist. #846- The Analyze dashboard and the Overview page gain a Delivery & infrastructure panel. It shows the SDLC readiness score from
readiness.json, with each practice's score (tests, CI, CD, rollback, migrations, feature flags, quality gates, observability, agent safety), and the resources ininfrastructure.jsoncounted by kind. Opened, it lists each practice's evidence and gaps and every resource with the file that declares it. When an analysis has not written either file, it says to runndx analyze.ndx exportnow copies both files, so a static dashboard shows the panel too. #838 - One host and port shape rule for the local LLM endpoint.
@n-dx/llm-clientexportsisPlausibleLlmHost,isValidPortanddescribeLocalEndpointProblem, and the local inference provider refuses a savedllm.local.hostorllm.local.portthat fails them before building its URL — a hand-edited.n-dx.jsonused to reachfetchunchecked. The dashboard imports the rule instead of carrying a copy, and reports a saved port written as a string as invalid rather than silently probing 1234.ndx config llm.local.host(andllm.local.verifier.host) refuses a host carrying a path, credentials, a second port or whitespace, and acceptslocalhost,127.1,[::1]and a LAN name —127.1was previously coerced to a number and refused. A parity test holds core's inline copy equal to llm-client's. #846 ndx migrate --apply [--plan=<path>] [--dry-run](rex migrate-apply --plan=<path.json>) applies a reviewedndx migrate --planplan, reading./migration-plan.jsonby default, wherendx migrate --planwrites. It writesproduct/andchanges/and removesprd_tree/andtree-meta.jsonunder one hold of the PRD lock, so the two layouts never sit side by side. Every v1 id is checked by count and id before anything is written and again on the tree read back. Changes the rules placed land on their capability; held changes, and items made after the plan, land in the Inbox, with any model pick recorded as a suggestion only. Release epics become no node, and ablockedByref to one is repointed at the changes it split into. AblockedByref to a capability, constraint or area names that node's open changes (a change held for the Inbox counts as open on the product node its v1 parent became, for this rewrite only), a product node's ownblockedBymoves onto its open changes, and a ref with no open change to name is dropped as met; the cut change lists every rewrite and drop. The first change, "Product map and change layer", records the cut. A held change completed at or before the cut cannot be placed, sorex healthreports itschange-placed-at-closeas a warning, not an error. So is v1 work completed between plan and apply, which the cut change lists; a held change completed in v2 after apply is still an error. Apply refuses a plan whose model pass stopped, and refuses while.rex/prd.md,.rex/prd.jsonor a branch-scoped PRD file sits beside the tree. On a v2 tree it reports so and exits zero. Aprd_tree/left beside the v2 tree is removed only when it is what an interrupted apply left (it matches the plan's source or, with no plan, every id it holds is in the v2 tree); any other is refused and kept, naming the ids to re-capture, and--dry-runsays which. #749ndx migrate [dir], with neither--plannor--apply, runs every migration step the project still needs, in order: the layout move into.ndx/, theprd.md/prd.jsonconversion to the folder tree, thenrex migrate-plan(to--out, default./migration-plan.json) andrex migrate-apply. A step already done is skipped, so a failed run resumes where it stopped;--dry-runrehearses each step that can. #749ndx migrate --planshows the preflight banner and a run summary that lists the--outplan as written and says model calls are not counted.llm.routesforprd.migrate.judge(or*) turns the Jev review off. #749ndx install-sampleon a v2 project adds the sample as a change with two tasks throughrex add, instead of writing aprd_tree/besideproduct/;ndx destroy-sampleremoves it. A v1 project is unchanged. #749rex migrate-plan,rex migrate-apply --dry-runandrex migrate-applyrefuse a v1 tree with an item file the parser cannot read, such as unclosed front matter left by a hand edit or a merge conflict. The refusal names each file. Before this, the store's load dropped the parser's warning, so the plan and the dry run left the item out without saying so, and apply then removedprd_tree/along with that item's file. The v1 parser now marks a warningskippedwhen it drops an item, as the v2 reader already did. #749- The Windows spawn guard now refuses what cmd.exe would actually expand. #790 refused a
%NAME%token only whenNAMEwas in the environment, but cmd.exe also expands variables it computes itself (%CD%,%RANDOM%,%CMDCMDLINE%, the undocumented%__APPDIR__%and%__CD__%, …) and reads the name before a:~or:a=bmodifier, so%CMDCMDLINE:~-1%slipped through and expanded to a bare"that ended the argument. A token carrying a line break is refused too, since cmd.exe silently drops everything after it. Every cmd.exe invocation passes/v:off, and sourcevision'sexecFileSyncCliis llm-client's, so there is one recipe. Those sourcevision spawns (rex tree,ps) now appear inclaude_commands.loglike every other spawn through these helpers. #846 - Windows: no more console windows from n-dx's child processes.
exec()andspawnCliin llm-client gain awindowsHideoption that defaults to true, and every other production spawn in the monorepo passes it; a new root policy test (windows-hide-policy) fails any spawn that does not. The vendor-CLI logclaude_commands.logis now gitignored byndx initand discounted by hench's uncommitted-work gate, so a fresh project's first agent run no longer refuses completion over it. The Overview page's isometric hero shows its Options button again. #839
Full release notes
ndx config <pkg>.<key> <value> refuses a key with a __proto__, constructor or prototype segment. ndx config llm.__proto__.polluted x followed configs.llm["__proto__"] onto Object.prototype and assigned there, in the CLI's own process; the write was never persisted, but the command now exits non-zero naming the segment and leaves every config file untouched. hench's setConfigValue applies the same guard behind its CONFIG_FIELDS allowlist.
The ndx work command that ndx work --resolve prints is now correct on Windows for a path that ends in a backslash or contains a quote: backslashes before a " or the closing quote are doubled, as the argv rules every Node program decodes with require, so C:\My Repo\ prints as "C:\My Repo\\" rather than "C:\My Repo\".
Four regular expressions that were quadratic on hostile input scan linearly: the icacls ACE parser behind owner-only config files (the same result on every icacls line; it now rejects a line with a ( inside a flag group or a :( inside an identity, which icacls never prints), the trailing-separator trim in the MCP shim's workspace key, and the project-id slug in ndx start and its twin in the hub's new-project page (the same result on every input). ndx ci's data-layer check now escapes every regex metacharacter in a layout directory name rather than only the dot.
ndx migrate --apply [--plan=<path>] [--dry-run] (rex migrate-apply --plan=<path.json>) applies a reviewed ndx migrate --plan plan, reading ./migration-plan.json by default, where ndx migrate --plan writes. It writes product/ and changes/ and removes prd_tree/ and tree-meta.json under one hold of the PRD lock, so the two layouts never sit side by side. Every v1 id is checked by count and id before anything is written and again on the tree read back. Changes the rules placed land on their capability; held changes, and items made after the plan, land in the Inbox, with any model pick recorded as a suggestion only. Release epics become no node, and a blockedBy ref to one is repointed at the changes it split into. A blockedBy ref to a capability, constraint or area names that node's open changes (a change held for the Inbox counts as open on the product node its v1 parent became, for this rewrite only), a product node's own blockedBy moves onto its open changes, and a ref with no open change to name is dropped as met; the cut change lists every rewrite and drop. The first change, "Product map and change layer", records the cut. A held change completed at or before the cut cannot be placed, so rex health reports its change-placed-at-close as a warning, not an error. So is v1 work completed between plan and apply, which the cut change lists; a held change completed in v2 after apply is still an error. Apply refuses a plan whose model pass stopped, and refuses while .rex/prd.md, .rex/prd.json or a branch-scoped PRD file sits beside the tree. On a v2 tree it reports so and exits zero. A prd_tree/ left beside the v2 tree is removed only when it is what an interrupted apply left (it matches the plan's source or, with no plan, every id it holds is in the v2 tree); any other is refused and kept, naming the ids to re-capture, and --dry-run says which.
--dry-run runs apply's checks, including the staged write and read-back, and prints per v1 kind how many items become each v2 type, how many are held for the Inbox and how many metadata values are dropped (droppedMeta, droppedLog), and each blockedBy rewrite and drop with its old and new refs; it writes nothing in the project.
Soft freeze: rex migrate-apply is a new rex command and --apply, --plan=<path> and --dry-run are new ndx migrate flags; the --plan and --out behaviour of ndx migrate --plan and rex migrate-plan is unchanged.
The v2 writer refuses to turn a folder into a leaf while its state.yaml holds top-level keys other than schema and items, naming the folder and the keys, instead of dropping them. rex migrate-to-folder-tree renames a converted prd.json to prd.json.migrated.
ndx migrate [dir], with neither --plan nor --apply, runs every migration step the project still needs, in order: the layout move into .ndx/, the prd.md / prd.json conversion to the folder tree, then rex migrate-plan (to --out, default ./migration-plan.json) and rex migrate-apply. A step already done is skipped, so a failed run resumes where it stopped; --dry-run rehearses each step that can.
rex commands on a v1 PRD (prd_tree/ and no product/) keep working and print one deprecation line on stderr naming ndx migrate; the next release refuses v1 PRDs. The line is dropped under --quiet, and NDX_LEGACY_NOTICE=off silences it. ndx ci, status, plan, add, recommend, refresh, work, analyze, self-heal, start, web and pair-programming print it once per command, not once per rex process they spawn.
Agents are never told to migrate and cannot: every hench run that ndx work, ndx self-heal and ndx pair-programming spawn gets NDX_AGENT_RUN=1 and NDX_LEGACY_NOTICE=off, and ndx migrate (bare, --plan and --apply) refuses while NDX_AGENT_RUN is set: an operator runs the migration from their own shell.
Soft freeze: bare ndx migrate used to exit 1 asking for --plan; it now runs the chain. --plan and --apply are unchanged. NDX_LEGACY_NOTICE (off) and NDX_AGENT_RUN are new environment variables read by rex and core.
ndx migrate --plan shows the preflight banner and a run summary that lists the --out plan as written and says model calls are not counted. llm.routes for prd.migrate.judge (or *) turns the Jev review off.
Adds a config key under the soft freeze: llm.routes accepts prd.migrate.judge (the migration's Jev review), and a route to a tier turns that review off. ndx migrate --plan prints the preflight banner and run summary. No flag or MCP tool changes shape.
On Windows, the guard that keeps rex export, rex migrate-plan --out/--summary and rex tree-diff from writing inside the rex directory now sees through 8.3 short-name aliases such as RUNNER~1. It resolved paths with fs.realpathSync, which leaves short names unexpanded, so a project or output path written in short form slipped past the check. It now uses realpathSync.native; macOS and Linux behave as before.
Fixes a CLI guard under the soft freeze (Windows only): the same commands refuse a path they already meant to refuse. No flag or MCP tool changes shape.
ndx install-sample on a v2 project adds the sample as a change with two tasks through rex add, instead of writing a prd_tree/ beside product/; ndx destroy-sample removes it. A v1 project is unchanged.
Changes what a CLI command writes under the soft freeze: on a v2 project ndx install-sample and ndx destroy-sample write the change layer ({rex}/changes/), not prd_tree/. No flag changes shape.
rex remove <id> now works on a v2 PRD, removing a change, task or subtask inside the PRD lock. ndx destroy-sample on a v2 project uses it instead of deleting change folders itself, and fails naming the lock holder when another writer holds the lock.
Widens a CLI command under the soft freeze: rex remove accepts v2 change, task and subtask ids (it refused v2 trees before). No flag or MCP tool changes shape.
The first /api/rex/prd read after ndx start no longer answers 404 on a folder tree: the boot PRD cache is written before the server listens, so a request cannot beat it.
Changes a v2 rule under the soft freeze: change-placed-at-close is a warning, not an error, for a change the migration held for placement that completed in v1, at or before the plan's cut or between the plan and apply (the cut change lists the latter). A held change that completes in v2 after apply is still an error. A blockedBy ref to a product node whose pending work is held in the Inbox now names the held changes instead of being dropped as met.
The apply-side fixes on this layer (blockedBy rewrites onto open changes, the guarded prd_tree/ leftover removal, the dry run listing each rewrite) are described in the ndx migrate --apply changeset, migrate-apply.md.
rex migrate-plan, rex migrate-apply --dry-run and rex migrate-apply refuse a v1 tree with an item file the parser cannot read, such as unclosed front matter left by a hand edit or a merge conflict. The refusal names each file. Before this, the store's load dropped the parser's warning, so the plan and the dry run left the item out without saying so, and apply then removed prd_tree/ along with that item's file. The v1 parser now marks a warning skipped when it drops an item, as the v2 reader already did.
rex migrate-apply and its --dry-run print the v2 rule findings on the migrated tree, counted by rule (errors and warnings), as rex health then reports them. They are reported, not refused. A test now pins apply's refusal when the tree reads back without a v1 id.
rex migrate-apply keeps a dissolved release epic's description and acceptance criteria under its line in the cut change's body, and moves its own blockedBy onto its open children, so a release's work still waits for the release it depended on.
migrate --apply: when removing prd_tree/ or tree-meta.json fails after the v2 tree is switched in (a handle held on Windows), apply now says the v2 tree is in place, names the leftovers and the underlying error, and gives the manual step to clear them, instead of throwing the raw error.
migrate --apply: every leftover prd_tree/ message now names the leftover and the same manual step (check the v2 tree holds everything in it, then delete it by hand). A failed removal after the switch no longer promises a re-run finishes the job, and a failed removal on an already-v2 apply says the v2 tree is in place instead of throwing the raw error.
A migration plan's metAt now follows a capability's spec when a later pass redrafts it, instead of keeping the rules-stage draft's hash.
ndx migrate (the bare chain) now shows the preflight banner, and its --dry-run runs no model unless --with-models is given
rex migrate-plan, rex migrate-apply and ndx migrate-layout refuse inside an agent run (NDX_AGENT_RUN set), as ndx migrate already does.
rex migrate-to-folder-tree keeps the PRD title
The command wrote the tree but not .rex/tree-meta.json, so a migrated project read back as "PRD". It now writes the sidecar with the source document's title.
rex migrate-slugs on a v2 tree now says slugs are frozen there and applies to v1 trees only, instead of pointing at the v1 rex migrate-to-folder-tree command.
Placing an Inbox change (rex change place) carries the non-tree files in its folder into changes/<slug>/ with it, and removes the empty Inbox folder; a name clash refuses before anything is written.
The v2 writer keeps a moved folder node's extra top-level state.yaml keys (keys a newer rex wrote) in its new folder. A node that moves and loses its last child in the same write is refused, naming its old folder and the keys, instead of dropping them.
Writes on a v2 tree now stamp lastModified and lastModifiedBy on every node they change, create or move, as v1 writes do. Before, update_task_status, edit_item, add_item and move_item left a node's old stamp in place and gave a new change none, so remote sync and the dashboard read v2 writes as stale. A write that changes nothing stamps nothing. The stamp lands in the node's state.yaml row, so a write now also rewrites that file.
The rex MCP get_capabilities tool now reports "schemaVersion": "rex/v2" on a v2 tree. It answered "rex/v1" beside "adapter": "v2-change-layer", so a client that reads schemaVersion took the tree for v1. A v1 tree still reports rex/v1. The change layer store declares its version through the new optional schemaVersion on StoreCapabilities.
get_product now answers with a summary by default: each area with its counts of capabilities and constraints by status, defective nodes and open changes. Pass area (an id or display id) for that area's capabilities and constraints, or full: true for the whole map, the previous default. On a large v2 tree the whole map exceeded what an MCP client accepts and was refused. The /ndx-capture, /ndx-plan, /ndx-reshape and /ndx-adversarial-review skills read the summary, then the areas they need.
The preflight banner and the dashboard's command effects name changes/ and product/ for the PRD on a v2 project instead of prd_tree/, which a v2 project does not have.
The dashboard server turns off rex's v1-layout deprecation notice for the commands it spawns, so a failing rex spawn on a v1 project shows its real error and not the notice first.
rex next labels v2 work by type ([change], [task], [subtask]), never [epic]. On a v2 tree, level in MCP and JSON payloads is the v1 projection of type and nesting: it stays for compatibility in this release and is deprecated. type is authoritative; the release that refuses v1 PRDs removes level from every v2 payload.
Changes under the soft freeze: - ndx migrate --dry-run (the bare chain) runs no model unless the new --with-models flag is given. - rex migrate-plan, rex migrate-apply and ndx migrate-layout refuse inside an agent run. - rex migrate-plan, migrate-apply and its dry run refuse a v1 tree with an item file the parser skipped. - MCP get_capabilities reports schemaVersion: rex/v2 on a v2 tree. - MCP get_product answers with a per-area summary by default; area and full: true reach the detail, and full: true is the previous default. - v2 writes stamp lastModified / lastModifiedBy. - level is deprecated on v2 payloads (kept in this release); rex next text labels v2 work by type. - The preflight banner and command effects name changes/ and product/ on a v2 project.