npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@floh-solutions/plan-to-board

v0.2.0

Published

Turn an implementation-plan .md into an Azure DevOps Epic -> Issue -> Task tree, and write the resulting ids back into the file so re-running updates instead of duplicating.

Readme

@floh-solutions/plan-to-board

An implementation plan becomes an Azure DevOps Epic → Issue → Task tree, and the resulting work item ids go back into the plan file.

That last part is the whole point. A tool that can only be run once is a demo: the second run duplicates the board. With the ids recorded in the file's front matter, the second run updates — and the plan file and the board stay one thing rather than two that drift.

npm i -g @floh-solutions/plan-to-board
export ADO_ORG=your-org ADO_PROJECT=YourProject ADO_PAT=…

plan-to-board impl-plan.md              # dry run: print the tree, write nothing
plan-to-board impl-plan.md --live       # ask, then apply, then record the ids
plan-to-board impl-plan.md --live       # …and again: a no-op

It also ships as pharos plan inside @floh-solutions/pharos-cli, which is the same code behind one command with the rest of the Azure DevOps surface next to it.

The five steps:

| | | | |---|---|---| | 1 | read impl-plan.md | parse.ts | | 2 | emit a structured intermediate to approve | render.ts — the dry run, or --json | | 3 | create Epic → Issue → Task with parent links and iteration paths | sync.ts, apply.ts | | 4 | publish the plan to the project wiki, link it from the Epic | wiki.tsbehind a switch, see below | | 5 | write the ids back as front matter | frontmatter.ts |

The plan file

---
project: Contoso
iterationPath: Contoso\Iteration\Sprint 1     # defaults for every node below
assignedTo: [email protected]
---

# The initiative                     <- the Epic. Exactly one.

key: the-initiative                  <- metadata, only at the top of a section

Prose here becomes the Epic's description.

## A phase                           <- an Issue

owner: [email protected]

### A task                           <- a Task

…or, if a phase has no `###` headings, its checkboxes are its tasks:

- [ ] A task
  - key: stable-name
  - files: packages/thing/src/a.ts
  - depends: some-other-task
- [x] One that is already done

Three levels. A task cannot have subtasks; there is deliberately no fourth level to design around.

Epic, Issue and Task above are the plan file's own words, not the names this will create. Every run reads the project and maps each level to the type that plays its role there — Microsoft.EpicCategory, then Microsoft.RequirementCategory, then Microsoft.TaskCategory:

| plan | Basic | Agile | |---|---|---| | the initiative | Epic | Epic | | a phase | Issue | User Story | | a task | Task | Task |

So one plan file produces the right tree on either template. It used to write those three names literally, which was correct on Basic and failed on the first create anywhere else: on Agile an Issue is an impediment tracked off the backlog, not a requirement. The tree a run prints names what it resolved, and says assumed when it could not read the project.

Either ### headings or checkboxes, per phase. A phase with ### headings uses those and leaves its checkboxes as prose — so acceptance criteria under a task stay acceptance criteria. A phase with none uses its outermost checkboxes.

Metadata is only read at the top of a section (or directly under a checkbox), and only for keys it knows: key, owner/assigned to, state, iteration/sprint, area, priority, files, depends on. The first line that isn't one of those ends the block and starts the description, so a paragraph opening Note: … is prose, not a field. **Owner:** x works too.

- [ ] is To Do, - [x] is Done, - [~]/- [/] is Doing. An explicit state: beats the box. Headings and checkboxes inside fenced code blocks are left alone.

Those three are mapped per type the same way the levels are, and by the state's category rather than by its name: To Do becomes wherever a create of that type lands (its first state — which matters, because Azure DevOps refuses a create naming any other), Doing its first InProgress state, Done its first Completed one. On Agile that is New, Active, Closed. Completed beats Removed deliberately: a - [x] says the work was finished, never that it was cancelled.

Keys are identity

A node's key is how the next run finds the work item it made last time. Without an explicit one it is slugged from the title — so renaming a node without a key: reads as a delete plus a create. Give anything you might retitle a key: and renaming becomes a title update.

Step 5: the board: block

After a live run the tool adds, and thereafter maintains, exactly one block:

board:
  organization: contoso
  project: Contoso
  syncedAt: "2026-08-03T10:52:11.402Z"
  epic:
    id: 120
    key: p2b-smoke
    hash: 6e25cf957642
  phases:
    - id: 121
      key: round-trip
      hash: b326aeafc741
      tasks:
        - id: 122
          key: first
          hash: cc657588319c

Everything else in the front matter — key order, comments, quoting, other tools' keys — is preserved byte for byte.

  • organization / project — the ids only mean anything in the project that issued them, so pointing the file at another one stops with an error rather than rewriting unrelated work items. --force overrides.
  • hash is a digest of the description last written. See below.
  • orphans: holds items the plan no longer describes. They are kept, not dropped: forgetting an id strands a real work item, deleting it is not this tool's call, and if the section comes back its id comes back with it.

The file is written after every single create, not once at the end. A run that dies halfway has already recorded what it made, so the next run finishes the job. This is not hypothetical — the first live run of this package died on its fourth item, and the re-run created exactly that one.

Two pending grants, two switches, one file

grants.ts owns both, and nothing else reads them. Both default off, and every run prints one line per denied grant saying what it did instead — a silently skipped step is indistinguishable from one that was never built.

| grant | fails as | fallback | switch | |---|---|---|---| | Create tag definition | TF401289 | title prefix + Area Path | --tags, PLAN_TO_BOARD_TAGS=1 | | Contribute on the project wiki | see below | the plan file stays local | --wiki, PLAN_TO_BOARD_WIKI=1 |

Neither workaround is scattered: labels.ts is the only thing that prefixes a title, and flipping --tags on makes the same update path strip the prefix back off and tag instead — no migration script.

The wiki is the designed home for plans, not a nice-to-have. Per project, inside Azure DevOps, readable by anyone with board access, git-backed, linkable from the Epic it generated. Once this is pointed at someone else's project, their plan cannot live in this repo. The publish step is built, tested, and switched off — the local file is the fallback, not the destination.

The wiki is blocked by two different things

Verified live 2026-08-03, and they have different fixes:

  1. The PAT lacks the wiki scope. The same token reads a work item with 200 and GET _apis/wiki/wikis with 401. That is the token, not the account — regenerate it with vso.wiki_write alongside vso.work_write (PLAN.md §5 always said it should carry both). No admin needed.
  2. The account lacks Contribute on the wiki repo, which is the grant pending with the project admins (PLAN.md §9). It surfaces as WikiPageOperationFailedException on a write.

--wiki reports whichever one it hits, by name, rather than a bare 401.

What the plan owns, and what the board owns

The plan file owns titles, descriptions, area path, iteration path, priority and tags. The board owns System.State and System.AssignedTo once an item exists — those are what a person changes while working, and pushing a stale - [ ] over someone's "Doing" every run is how a tool gets turned off.

Drift is always reported:

= Task  #122    [SMOKETEST] First task
    board has state Doing, the plan says To Do — left alone

--push-state and --push-assignee make the plan win instead.

Findings this package encodes

Each cost a live call to learn.

  • System.IterationPath is not the classification node path. The node is \Contoso\Iteration\Sprint 1; the field holds Contoso\Sprint 1. PLAN.md §8 records the node form, so passing it through unchanged is a 400 that reads like the iteration does not exist. normalizeClassificationPath converts either form, and Sprint 1 alone, into the field form.
  • A create may only land in the initial state. A create patch carrying System.State: Done is refused with "the value 'Done' … is not in the list of supported values" — though Done is perfectly legal on the type. So a - [x] task is created in To Do and transitioned by a second call.
  • The description cannot be compared against what the server returns. The read path HTML-encodes long text and runs a sanitizer that drops tag-shaped runs: a description containing Array<String> reads back as Array. Comparing text would rewrite it on every run forever, so the comparison is against the recorded hash instead. Confirmed live — the smoke-test Epic contains exactly that, and its third run was still a no-op.
  • workitemsbatch with errorPolicy: omit returns null in place, so an unreadable id arrives as a hole in the array rather than a shorter array.
  • A rev mismatch is not proof of a collision. A comment alone bumps System.Rev, so a rejected patch is diffed field by field before anyone is told they have a conflict.

Every update is guarded by the test op on /rev (PLAN.md §5), so a teammate editing mid-run gets a rejected patch instead of losing their edit. A conflict is reported and the run carries on with the rest of the tree.

Usage

plan-to-board <impl-plan.md> [options]

  --live                 actually write. Without it this is a dry run.
  --yes, -y              skip the confirmation prompt (required off a terminal)
  --json                 print the structured intermediate as JSON and exit
  --offline              render the plan without checking the board (no PAT needed)

  --tags / --wiki        turn on a step that is waiting on a grant (see above)
  --wiki-path <path>     wiki page path (default /Plans/<epic key>)
  --wiki-id <id>         target a specific wiki (default: the project wiki)

  --prefix <text>        title prefix for generated items (default [Pharos])
  --org / --project      Azure DevOps organization and project
  --area / --iteration / --assign     defaults for nodes that set none
  --push-state / --push-assignee      let the plan overwrite the board
  --no-write-back        do not touch the plan file
  --force                proceed even if the file was synced to another project

Auth is ADO_PAT / ADO_ORG / ADO_PROJECT, the same as @floh-solutions/ado-core. A live run asks before writing unless --yes is given, and refuses to write unconfirmed when there is no terminal to ask at.

Tests

pnpm test        # 66 tests against a mocked fetch. Offline. This is the gate.
pnpm typecheck

The suite covers the round trip end to end: the first run creates and records, the second reads the batch and issues no writes at all.

Verified live against contoso/Contoso on 2026-08-03 — work items #120–#123, prefixed [SMOKETEST] and assigned to [email protected], queued for deletion with the rest under board todo #527. Four runs: create, recover-from-a-crash, no-op, and a retitle that updated exactly one item.