ex-workflow 0.3.0-stage → 0.6.0-stage

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/README.md +73 -30
  3. package/dist/adapters/index.cjs +1 -1
  4. package/dist/adapters/index.mjs +1 -1
  5. package/dist/{roles-BLBRKhUZ.d.ts → budget-Cx63FX2K.d.ts} +9 -2
  6. package/dist/{roles-aneKYJ3Z.d.cts → budget-JyNKxP5N.d.cts} +9 -2
  7. package/dist/{chunk-7HZXZF3L.cjs → chunk-2OVYXXNV.cjs} +2 -2
  8. package/dist/chunk-2OVYXXNV.cjs.map +1 -0
  9. package/dist/chunk-3CLDZCHN.cjs +2 -0
  10. package/dist/chunk-3CLDZCHN.cjs.map +1 -0
  11. package/dist/chunk-5TXYMLBF.mjs +3 -0
  12. package/dist/chunk-5TXYMLBF.mjs.map +1 -0
  13. package/dist/chunk-7CDGPTEJ.mjs +2 -0
  14. package/dist/chunk-7CDGPTEJ.mjs.map +1 -0
  15. package/dist/chunk-7IG6EQI4.mjs +6 -0
  16. package/dist/chunk-7IG6EQI4.mjs.map +1 -0
  17. package/dist/chunk-GT57PLUT.mjs +2 -0
  18. package/dist/chunk-GT57PLUT.mjs.map +1 -0
  19. package/dist/chunk-IHE5K4X3.mjs +2 -0
  20. package/dist/chunk-IHE5K4X3.mjs.map +1 -0
  21. package/dist/chunk-KF5VQH3M.mjs +2 -0
  22. package/dist/chunk-KF5VQH3M.mjs.map +1 -0
  23. package/dist/chunk-MH66G5KV.cjs +2 -0
  24. package/dist/chunk-MH66G5KV.cjs.map +1 -0
  25. package/dist/chunk-N3QB2S7R.cjs +2 -0
  26. package/dist/chunk-N3QB2S7R.cjs.map +1 -0
  27. package/dist/chunk-NA53TZE4.mjs +2 -0
  28. package/dist/chunk-NA53TZE4.mjs.map +1 -0
  29. package/dist/chunk-TEPK5Y36.mjs +2 -0
  30. package/dist/chunk-TEPK5Y36.mjs.map +1 -0
  31. package/dist/chunk-UBFIKDIP.cjs +2 -0
  32. package/dist/chunk-UBFIKDIP.cjs.map +1 -0
  33. package/dist/chunk-UUOMQMWT.cjs +2 -0
  34. package/dist/chunk-UUOMQMWT.cjs.map +1 -0
  35. package/dist/chunk-WOGK54HB.cjs +3 -0
  36. package/dist/chunk-WOGK54HB.cjs.map +1 -0
  37. package/dist/chunk-YRATJT7P.cjs +6 -0
  38. package/dist/chunk-YRATJT7P.cjs.map +1 -0
  39. package/dist/chunk-Z3PU6EBI.mjs +2 -0
  40. package/dist/chunk-Z3PU6EBI.mjs.map +1 -0
  41. package/dist/chunk-ZTE7LREB.cjs +2 -0
  42. package/dist/chunk-ZTE7LREB.cjs.map +1 -0
  43. package/dist/cli/bin.cjs +1 -1
  44. package/dist/cli/bin.cjs.map +1 -1
  45. package/dist/cli/bin.mjs +1 -1
  46. package/dist/cli/bin.mjs.map +1 -1
  47. package/dist/cli/index.cjs +1 -1
  48. package/dist/cli/index.mjs +1 -1
  49. package/dist/index.cjs +1 -1
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +36 -13
  52. package/dist/index.d.ts +36 -13
  53. package/dist/index.mjs +1 -1
  54. package/dist/index.mjs.map +1 -1
  55. package/dist/kanban/index.cjs +1 -1
  56. package/dist/kanban/index.mjs +1 -1
  57. package/dist/schema/index.cjs +2 -0
  58. package/dist/schema/index.cjs.map +1 -0
  59. package/dist/schema/index.d.cts +57 -0
  60. package/dist/schema/index.d.ts +57 -0
  61. package/dist/schema/index.mjs +2 -0
  62. package/dist/schema/index.mjs.map +1 -0
  63. package/dist/scrum/index.cjs +1 -1
  64. package/dist/scrum/index.d.cts +18 -3
  65. package/dist/scrum/index.d.ts +18 -3
  66. package/dist/scrum/index.mjs +1 -1
  67. package/dist/types.d.cts +32 -3
  68. package/dist/types.d.ts +32 -3
  69. package/dist/waterfall/index.cjs +1 -1
  70. package/dist/waterfall/index.d.cts +2 -0
  71. package/dist/waterfall/index.d.ts +2 -0
  72. package/dist/waterfall/index.mjs +1 -1
  73. package/package.json +18 -2
  74. package/dist/chunk-23P7C25E.cjs +0 -2
  75. package/dist/chunk-23P7C25E.cjs.map +0 -1
  76. package/dist/chunk-2IKIBB73.cjs +0 -2
  77. package/dist/chunk-2IKIBB73.cjs.map +0 -1
  78. package/dist/chunk-4A4TD4MH.cjs +0 -2
  79. package/dist/chunk-4A4TD4MH.cjs.map +0 -1
  80. package/dist/chunk-6R3YFUUV.cjs +0 -6
  81. package/dist/chunk-6R3YFUUV.cjs.map +0 -1
  82. package/dist/chunk-7HZXZF3L.cjs.map +0 -1
  83. package/dist/chunk-AUZIQPFT.cjs +0 -2
  84. package/dist/chunk-AUZIQPFT.cjs.map +0 -1
  85. package/dist/chunk-BJEOTG6P.mjs +0 -2
  86. package/dist/chunk-BJEOTG6P.mjs.map +0 -1
  87. package/dist/chunk-FLCYD2QF.mjs +0 -2
  88. package/dist/chunk-FLCYD2QF.mjs.map +0 -1
  89. package/dist/chunk-NN3XJUHP.cjs +0 -2
  90. package/dist/chunk-NN3XJUHP.cjs.map +0 -1
  91. package/dist/chunk-P4P4JTE5.mjs +0 -2
  92. package/dist/chunk-P4P4JTE5.mjs.map +0 -1
  93. package/dist/chunk-R27STIER.mjs +0 -2
  94. package/dist/chunk-R27STIER.mjs.map +0 -1
  95. package/dist/chunk-TMKCALFT.cjs +0 -2
  96. package/dist/chunk-TMKCALFT.cjs.map +0 -1
  97. package/dist/chunk-UOGNVWVO.mjs +0 -2
  98. package/dist/chunk-UOGNVWVO.mjs.map +0 -1
  99. package/dist/chunk-VGD4V5BA.mjs +0 -6
  100. package/dist/chunk-VGD4V5BA.mjs.map +0 -1
  101. package/dist/chunk-WOKNWJKU.mjs +0 -2
  102. package/dist/chunk-WOKNWJKU.mjs.map +0 -1
  103. package/dist/chunk-ZX4DAMTG.mjs +0 -2
  104. package/dist/chunk-ZX4DAMTG.mjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,57 @@
1
1
  # Changelog
2
2
 
3
- ## 0.3.0-stage (unreleased)
3
+ ## 0.6.0-stage (unreleased)
4
+
5
+ Pre-release under the `stage` dist-tag. Phase 6 (backlog zero) fixes or explicitly accepts all 24 items the 1.0 definition named; the decisions are in the design spec, section 22, and the accepted limitations are in the README.
6
+
7
+ - Stale-lock takeover (B01): the lock is re-stat'd (inode and mtime) immediately before `unlink`; a lock changed between the staleness check and the unlink is not taken over; the takeover is retried and the lock-held error follows after the retry cap. The residual window between the stat and the unlink is accepted.
8
+ - Completion rules (B02): `complete()` runs the methodology rules through `applyAction` with action type `item.complete` after validation, so rule hooks see completions; `checkGate` still answers `gate-unmet` for a conflict of interest.
9
+ - Gate approval (B05): `decide(approve)` honours a `needs-review` verdict from the item's last `item.completed` event and advances to the review stage.
10
+ - Scrum status (B04): `exwf status` reports a `report` for Scrum (`{ closing, sprint, reason, inFlight }`), so a sprint left in `closing` is flagged.
11
+ - Scrum closing (B07): the `sprint-closing` rule refuses `ceremony commit` into a closing sprint; `uncommit` stays allowed.
12
+ - Scrum stage ids (B06): `init` refuses a Scrum config whose stages lack `backlog`, `sprint` or `review` (`config-invalid`).
13
+ - CLI write order (B08): `next` and `complete` write briefs and handoff files before the state.
14
+ - Evidence freshness (B10, semantic change, own commit): policies and gates count only evidence recorded after the item's last `item.rework` event; revert that commit alone to restore append-only evidence.
15
+ - Test gaps (B15): `writeAtomic` failure path, `decide` reject and unknown-pending cases, and a usage exit-2 test per command group.
16
+ - Planner budgets (B20): `plan auto` applies the `tokens` and `turns` limits together.
17
+ - Carryover (B22): a carryover blocked or awaiting human in the review stage resumes at review, as `review` already did.
18
+ - Waterfall fallback (B18): `WaterfallConfig.fallbackTokensPerWeight` (optional) feeds the planner like Scrum's `fallbackTokensPerWeight`.
19
+ - Waterfall lessons (B21): phase retrospective lessons carry `from` and `item`, as in Scrum (`from` defaults to the retro's `by`).
20
+ - Valueless flags (B24): a value-taking flag given without a value is `invalid-input` naming the flag; `--estimate` is the only boolean flag.
21
+
22
+ Compatibility: no `state.json` schema change and no new event kind; every `.workflow/state.json` written by an earlier stage loads unchanged. The generated `WaterfallConfig` schema document gains one optional key, `fallbackTokensPerWeight`. Behaviour changes, all deliberate: rules run on `item.complete` (B02); custom rules on `item.complete` can refuse a completion (`rule-violation`) and their warnings are returned; `decide(approve)` honours `needs-review` (B05); `ceremony commit` is refused into a closing sprint (B07); a freshly reworked item needs fresh evidence before it passes (B10); a blocked or awaiting-human carryover in review resumes at review (B22); `plan auto` applies tokens and turns together (B20); a value-taking flag without a value is refused (B24); Scrum's `status.report` is no longer `null`.
23
+
24
+ ## 0.5.0-stage (unreleased)
25
+
26
+ Pre-release under the `stage` dist-tag. Phase 5 closes the engine gaps the workflowAI scrum-master dogfood raised (issues #8–#21); the decisions are in the design spec, section 21.
27
+
28
+ - Planner budget gates and `closing` (#16): the planner gates on `turns` and `wallclockMs` as well as `tokens`; in-flight items are counted by the iteration's committed list as well as `item.iteration`; a spent dimension skips each ready item with reason `budget` and answers `budget-exhausted`; an iteration in `closing` answers `budget-exhausted`, not `no-candidates`. `ScrumConfig.autoClose: false` keeps an exhausted sprint in `closing` until `review` or `cancel` closes it.
29
+ - Estimate mode, fallback and CLI flag (#21): `ScrumConfig.estimateMode` (`per-weight` or `per-call`) and `ScrumConfig.fallbackTokensPerWeight`; `exwf next --fallback-tokens-per-weight n`.
30
+ - Zero accounting and `exwf account` (#9): `complete()` refuses 0 tokens unless the handoff is declared an estimate (`exwf complete --estimate`); `correctAccounting` and `exwf account` correct one line of an item's accounting, adjust the owning sprint or phase, and record `accounting.corrected`; Scrum metrics fold corrections made after a sprint closed into its frozen `spent`.
31
+ - `metrics.current` (#15): mid-sprint figures for the open sprint (spent, committed and done weight, cost per point, budget utilisation), all zero when no sprint is open.
32
+ - `changeConfig` / `exwf config set` (#8): change the methodology config between iterations, re-validated and recorded as `config.changed`; keys a methodology locks (Kanban `columns` and `classesOfService`, Waterfall `phases`, `traceability` and `humanGates`) are refused.
33
+ - `exwf unblock` (#17): clears a named blocker (`--blocker`) or every open one, with an optional `--note`.
34
+ - Open-questions warning and `status.openQuestions` (#14): a `done` or `needs-review` completion that carries open questions warns `open-questions-ignored`; `exwf status` lists the questions still open.
35
+ - Question brief `parent` (#19): a brief for a question item carries its parent's last completion (handoff, produced, open questions, do-not).
36
+ - Review carryover (#10): an item carried over from the review stage re-enters at `review` on the next `plan` or `commit` and is not re-estimated.
37
+ - `refine.reestimate` (#11): confirm a backlog item's weight without changing it; satisfies the carryover re-estimate rule.
38
+ - `plan auto` skipped (#18): `plan auto` skips a candidate that fails the backlog exit policy or does not fit the remaining budget, keeps walking, and reports each skip in `sprint.opened`.
39
+ - `dod-not-weakened` at retro (#20): a retro between sprints that drops a Definition of Done policy is refused unless `allowWeaken: true`; `dodChanges` replaces the whole list, `suggestedChange { kind: "dod" }` appends one.
40
+ - Agreements replace and retire (#12): a retro lesson's agreement may `replaces` an earlier one, and `retire` lists agreements to remove, matched by exact text; Waterfall's phase retrospective shares the helper.
41
+ - Lesson `from` and `item` (#13): a retro lesson is attributed to a role (or `human`) and may name an item; `from` defaults to the retro's `by`.
42
+ - Events and hooks: `Methodology.lockedConfigKeys` and `afterConfigChange`; `estimateTokens(state, weight, fallback, mode)`; new event kinds `agreement.retired` and `item.reestimated`; payload additions `item.completed.open_questions`, `sprint.opened.skipped` / `resumed`, `sprint.committed.resumed`, `blocker.cleared.note` and retro `retired`.
43
+
44
+ Compatibility: every `.workflow/state.json` written by an earlier stage loads unchanged (every new field is optional). Behaviour changes, all deliberate: a 0-token completion now needs `--estimate`; `plan auto` no longer aborts on an incomplete candidate and keeps walking past one that does not fit; a review-stage carryover re-enters at `review`; a retro that drops a DoD policy between sprints needs `allowWeaken: true`; `next` reports `budget-exhausted` for a spent turns budget and for a sprint in `closing`.
45
+
46
+ ## 0.4.0-stage (2026-10-23)
47
+
48
+ - Schema entry (`ex-workflow/schema`): JSON Schema (draft-07) documents generated from the TypeScript types for `WorkflowState`, `Role` (manifest), `NewItem`, `Handoff`, `ScrumConfig`, `KanbanConfig`, `WaterfallConfig`, plus a dependency-free `validate` / `assertValid` for the subset the generator emits. `pnpm schema:check` fails the build when the types and the committed schema drift.
49
+ - Adapters: `loadState` validates `.workflow/state.json` and every `roles/*.json` manifest against the schema after the version check; a state that fails is never returned.
50
+ - CLI: `exwf add`, `exwf complete` and `exwf role add` validate their input file before the engine sees it; errors name the file, the JSON pointer and the expected shape.
51
+ - Compatibility: a `.workflow/state.json` written by any earlier stage loads unchanged (every field added since 0.1.0-stage is optional). Two inputs that used to be accepted are now refused, with the file and JSON pointer named: extra keys in `state.json` or in an item (every object is closed), and `roles/*.json` manifests that carry only an `id` (a manifest is a full `Role`). `saveState` refuses to write a state the loader would reject, and `exwf edit` / `exwf init --roles` validate their files.
52
+ - Size: `dist/schema/index.mjs` and `schema` budgeted at measured size; `core+scrum`, `core+kanban`, `core+waterfall` unchanged.
53
+
54
+ ## 0.3.0-stage (2026-10-23)
4
55
 
5
56
  Pre-release under the `stage` dist-tag.
6
57
 
package/README.md CHANGED
@@ -10,7 +10,7 @@ State is plain JSON, every operation is `(state, action) -> state`, and every er
10
10
  pnpm add ex-workflow@stage
11
11
  ```
12
12
 
13
- Pre-releases are published under the `stage` dist-tag (currently `0.3.0-stage`); `latest` is reserved for 0.1.0.
13
+ Pre-releases are published under the `stage` dist-tag (currently `0.6.0-stage`); `latest` is reserved for 0.1.0.
14
14
 
15
15
  Node >= 18. Runs on macOS, Linux and Windows.
16
16
 
@@ -61,7 +61,7 @@ board(s2, new Date().toISOString()); // columns with active/queued counts and ca
61
61
  kanbanMetrics(s2); // lead time, cycle time per column, throughput, WIP, flow efficiency, aging, CFD, SLE compliance
62
62
  ```
63
63
 
64
- Cadences are ceremonies: `replenishment`, `delivery-planning`, `service-delivery-review` (WIP, policies, SLEs), `operations-review` (blockers, aging WIP), `risk-review` (fixed-date risk, stale blockers), `strategy-review` (classes of service). `exwf status` reports which cadences are due (`config.cadences.<name>.everyTurns`). `test/kanban.e2e.test.ts` is a full week with an expedite, an escalation and a blocker.
64
+ Cadences are ceremonies: `replenishment`, `delivery-planning`, `service-delivery-review` (WIP, policies, SLEs), `operations-review` (blockers, aging WIP), `risk-review` (fixed-date risk, stale blockers), `strategy-review` (classes of service). `exwf status` reports which cadences are due (`config.cadences.<name>.everyTurns`). `delivery-planning` needs a non-empty `items` list, and the `operations-review` assign entries need an existing blocker id. `test/kanban.e2e.test.ts` is a full week with an expedite, an escalation and a blocker.
65
65
 
66
66
  `next()` also returns `skipped`: each ready card it did not pull, with `reason` (`entry-policy`, `wip`, `resource-cap` or `budget`) and a `detail`; the commitment column's policies gate the pull out of it (escalation `question` cards are exempt, as from exit policies and gates). Ordering is by class band (question, expedite, fixed-date, standard, intangible) and, with `fairness.aging` on (the default), priority stays primary inside a window of `fairness.maxDeferralRounds` passes (default 5); each full window a card is passed over promotes it one step above any priority difference, and an intangible card reaching the first window is ranked as standard, so nothing is starved.
67
67
 
@@ -99,23 +99,61 @@ State lives in `.workflow/`. Every command prints JSON; errors exit 1, bad usage
99
99
  | `init <scrum\|kanban\|waterfall> --config <file> [--roles <file>]` | `{ ok, state: { methodology, stages, roles } }` |
100
100
  | `add <item.json>` | `{ ok, warnings, item }` (checked by the methodology rules) |
101
101
  | `edit <id> <patch.json>` | `{ ok, warnings, item }` (checked by the methodology rules) |
102
- | `next [--max n]` | `{ ok, reason, skipped, briefs }`; writes `.workflow/briefs/<id>.json` |
103
- | `complete <id> <handoff.json> --tokens-in n --tokens-out n --duration-ms n --model m [--role r]` | `{ ok, item, warnings }` |
102
+ | `next [--max n] [--fallback-tokens-per-weight n]` | `{ ok, reason, skipped, briefs }`; writes `.workflow/briefs/<id>.json`. `--fallback-tokens-per-weight` prices a candidate when there is no accounting history (default 20000 per weight unit, or `ScrumConfig.fallbackTokensPerWeight`) |
103
+ | `complete <id> <handoff.json> --tokens-in n --tokens-out n --duration-ms n --model m [--role r] [--estimate]` | `{ ok, item, warnings }`. Zero tokens is refused unless `--estimate` marks the line as a placeholder; correct it later with `account` |
104
+ | `account <item> [--line n] [--tokens-in n] [--tokens-out n] [--duration-ms n] [--model m] [--by who]` | `{ ok, line, event }`: rewrites one accounting line of an item (the last by default), adjusts the owning sprint or phase, clears the placeholder flag and records `accounting.corrected` |
104
105
  | `ceremony <name> [input.json] [--by who]` | `{ ok, warnings, lastEvent }` |
105
106
  | `decide <pendingId> approve\|reject --by who [--note text]` | `{ ok, item }` (runs the methodology's `afterDecide`, e.g. a Waterfall phase baseline) |
106
- | `status` | `{ ok, methodology, openIteration, counts, pending, lastEvent, report }` (`report` is the methodology's own status: Kanban's cadences due, Waterfall's phases, pending gates and traceability coverage; `null` for Scrum) |
107
+ | `config set <patch.json> [--by who]` | `{ ok, warnings, changed, config }`: merges a patch into the methodology config between iterations (refused while an iteration is open, and for locked keys); the merge is shallow: top-level keys replace wholesale, so `{ "sprintBudget": { "tokens": N } }` drops `turns`/`wallclockMs` |
108
+ | `unblock <item> [--blocker id] [--note text] [--by who]` | `{ ok, warnings, item }`: clears the named open blocker, or every open one |
109
+ | `status` | `{ ok, methodology, openIteration, counts, pending, openQuestions, lastEvent, report }` (`openQuestions` lists the questions a `done` completion left open; `report` is the methodology's own status: Scrum's closing sprint, Kanban's cadences due, Waterfall's phases, pending gates and traceability coverage) |
107
110
  | `board` | `{ ok, columns }` |
108
- | `metrics` | `{ ok, metrics }` |
111
+ | `metrics` | `{ ok, metrics }` (Scrum adds `current`, the open sprint's spent, committed and done weight, cost per point and budget utilisation; zeros when no sprint is open) |
109
112
  | `pending` / `lessons` | the arrays |
110
113
  | `validate <action.json>` | `{ ok: true, violations }` |
111
114
  | `role add <manifest.json>` / `role list` | `{ ok, roles }` |
112
115
 
113
116
  `--by` defaults to `human`.
114
117
 
118
+ ### Scrum configuration
119
+
120
+ `init scrum --config <file>` takes a `ScrumConfig`. Three keys were added in 0.5.0-stage, all optional:
121
+
122
+ - `autoClose` (default `true`): close the sprint automatically when its budget is spent and nothing is in flight. `false` leaves an exhausted sprint in `closing` until `ceremony review` or `ceremony cancel` closes it, so the human gate is never skipped.
123
+ - `estimateMode` (`"per-weight"` by default, or `"per-call"`): how the planner and `plan auto` price a candidate. `per-weight` uses the median tokens per weight unit from history. `per-call` uses the median tokens per completion regardless of weight, which fits a host that bills one all-inclusive total per worker call.
124
+ - `fallbackTokensPerWeight` (default 20000): tokens per weight unit when there is no history.
125
+
126
+ The planner gates on every budget dimension. `sprintBudget.turns` counts one per completion, so a host with a calls budget sets `turns` and `estimateMode: "per-call"` rather than a `calls` key.
127
+
128
+ ### Scrum retrospective
129
+
130
+ `ceremony retro <input.json>` runs between sprints. Its input:
131
+
132
+ - `lessons[]`: `{ text, scope, from?, item?, suggestedChange? }`. `from` is `"human"` or a role id (default: the `--by` who); `item` must name an existing item.
133
+ - `adopt[]` / `dismiss[]`: raised lessons, by id, each once.
134
+ - `retire: string[]`: agreement texts to remove, matched by exact text (never by index). An agreement that is not current fails `invalid-input` and lists the current ones.
135
+ - `suggestedChange` (on a lesson or an adopt entry): `{ kind: "agreement", text, replaces? }` adds a working agreement, and `replaces` retires the earlier agreement with that exact text; `{ kind: "dod", policy }` appends one Definition of Done policy; `{ kind: "backlog", item }` queues work; `{ kind: "skill", note }` is for the host.
136
+ - `dodChanges: Policy[]` replaces the whole Definition of Done. Dropping a current policy is refused by `dod-not-weakened` unless the retro input also sets `allowWeaken: true`.
137
+
138
+ `ceremony refine` takes `reestimate: string[]`: confirm the weight of backlog items (each gets `item.reestimated`) without changing them. An item carried over from `review` re-enters at review and is not re-estimated.
139
+
115
140
  ### Concurrency
116
141
 
117
142
  One writer at a time per `.workflow/`: `exwf` serialises writers with `.workflow/state.lock` (created exclusively, retried for about two seconds, taken over when older than 60 s). If a crashed process left the lock behind and a command reports that another `exwf` holds it, delete the file. Read-only commands do not lock. Use `withStateLock` from `ex-workflow/adapters` when you write state yourself.
118
143
 
144
+ ### Schema
145
+
146
+ `ex-workflow/schema` exports JSON Schema (draft-07) documents generated from the TypeScript types — `workflowStateSchema`, `roleSchema`, `newItemSchema`, `itemPatchSchema`, `handoffSchema`, `scrumConfigSchema`, `kanbanConfigSchema`, `waterfallConfigSchema` — and a small validator with no runtime dependencies:
147
+
148
+ ```ts
149
+ import { assertValid, validate, workflowStateSchema } from "ex-workflow/schema";
150
+
151
+ const errors = validate(workflowStateSchema, JSON.parse(raw)); // SchemaError[] with JSON-pointer paths
152
+ assertValid(workflowStateSchema, JSON.parse(raw), ".workflow/state.json"); // throws WorkflowError("invalid-input")
153
+ ```
154
+
155
+ `exwf` validates `state.json`, role manifests, item files, `exwf edit` patches, `--roles` files and handoff files with these schemas before acting on them, and `saveState` validates a state before writing it, so the CLI cannot write a file the loader would refuse. The documents are standard JSON Schema, so editors and ajv read them too. They are regenerated with `pnpm schema:gen`; `pnpm schema:check` (in `ci` and the pre-commit hook) fails when they drift from the types.
156
+
119
157
  ## Layers
120
158
 
121
159
  ```
@@ -129,37 +167,42 @@ This package is L0. All layers read and write the same `.workflow/` files.
129
167
 
130
168
  ## Status
131
169
 
132
- 0.3.0-stage adds the Waterfall module; all three methodologies ship. `ex-workflow/schema` is still pending. Design: [docs/superpowers/specs/2026-10-19-ex-workflow-design.md](docs/superpowers/specs/2026-10-19-ex-workflow-design.md).
133
-
134
- ## Known backlog
135
-
136
- Minor items deferred past 0.3.0-stage:
137
-
138
- - Stale-lock takeover can race when a lock is older than 60 s.
139
- - `decide(approve)` ignores a `needs-review` verdict.
140
- - `setDod` hard-codes the `review` stage id in the Scrum methodology.
141
- - The `closing` sprint state is not enforced on commit, plan or uncommit.
142
- - State is written before briefs; a crash between the two leaves them out of step.
143
- - `roles/*.json` are re-merged on every load.
144
- - Evidence can go stale across a rework cycle.
145
- - The scrum stub size row in `size-budget.json` guards little; the `core+scrum` budget ratchets down from 45238 B (spec target 40960 B).
146
- - Kanban WIP limits bound active cards (`in_progress`, `blocked`, `awaiting_human`); cards the engine advanced into a column's queue are reported as `queued` and surfaced by operations-review, not refused.
147
- - Kanban `wipBypass` is the total number of extra slots a column grants while any expedite is active, not one per expedite.
148
- - Kanban cadences use `item.completed` counts as turns; a wall-clock cadence schedule is the host's job.
149
- - Minor test gaps: retro duplicate-ref rejection, `writeAtomic` failure path, thin CLI usage and `decide` tests.
150
- - Waterfall traceability is keyed by phase ids, not item types (spec §10 wording); links are read in both directions.
151
- - Waterfall `editItem`/`addItem` called directly (not through `applyAction` or the CLI) bypass `baseline-immutable`.
152
- - Waterfall phase budgets reuse `Iteration`; `exwf status` shows the open phase as `openIteration`. With no accounting history the planner estimates 20000 tokens per weight unit, so a small phase budget dispatches nothing until `next({ fallbackTokensPerWeight })` is passed or history exists.
153
- - Phase retrospective applies `agreement` lessons only; other suggested changes are stored for the host.
170
+ 0.6.0-stage closes the Phase 6 backlog: every item the 1.0 definition named is either fixed or an accepted limitation (see [Accepted limitations](#accepted-limitations)). 0.5.0-stage closes the engine gaps from the workflowAI dogfood (Scrum budget gates, configuration between iterations, accounting corrections, agreements that can be replaced or retired, question briefs with their parent); see [CHANGELOG.md](CHANGELOG.md). 0.4.0-stage added `ex-workflow/schema` (JSON Schema documents and a validator, checked by `exwf` on load and on input). 0.3.0-stage added the Waterfall module; all three methodologies ship. Design: [docs/superpowers/specs/2026-10-19-ex-workflow-design.md](docs/superpowers/specs/2026-10-19-ex-workflow-design.md).
171
+
172
+ ## Accepted limitations
173
+
174
+ The 1.0 definition requires every known gap to be fixed or accepted as a documented limitation. These are the accepted ones (design spec, section 22). Each has one line of reasoning.
175
+
176
+ - Stale-lock takeover (B01): the re-stat narrows the race to the gap between the stat and the `unlink`, which no portable call closes; `ino` may be 0 (FAT/exFAT, some shares), in which case only the mtime is compared.
177
+ - Conflict of interest (B02): `"conflict-of-interest"` stays a declared, reserved `ErrorCode` that is never thrown, and the core rule matching action type `complete` stays dormant; `checkGate` already refuses the case as `gate-unmet`, and removing the code would narrow a public type.
178
+ - Blockers (B03): a blocker is owned by a human, and clearing it would need a link from the follow-up that the schema does not carry; `exwf unblock` is the path.
179
+ - Scrum stage ids (B06): `backlog`, `sprint` and `review` are owned by the module (about twenty uses); init refuses a config that lacks them.
180
+ - Uncommit while closing (B07): `ceremony uncommit` stays allowed during `closing` because it only shrinks the sprint.
181
+ - Roles on load (B09): state wins on an id conflict, `exwf role add` is the audited path, and re-merging is idempotent.
182
+ - Scrum size row (B11): budgets sit at measured size by the owner's standing decision; the row stays as a tripwire.
183
+ - Kanban WIP (B12): the engine must not refuse a card it advanced itself; operations-review surfaces queued cards.
184
+ - Kanban `wipBypass` (B13): it is a column total; re-scoping the key after 1.0 would break configs.
185
+ - Kanban cadences (B14): the key is `everyTurns` because the engine has no clock; wall-clock schedules are the host's job.
186
+ - Waterfall traceability (B16): it is keyed by phase ids because the core item union has no requirement, design or test types; phase membership classifies items.
187
+ - Direct primitives (B17): `editItem` and `addItem` bypass `baseline-immutable` by design; `applyAction` and the CLI enforce it.
188
+ - Phase budgets (B18): a phase budget reuses `Iteration` and `status` shows `openIteration`; the naming is accepted, and `fallbackTokensPerWeight` covers the no-history case.
189
+ - Phase retrospective (B19): baselines and config are locked in Waterfall, so only `agreement` lessons apply; `dod` and `backlog` suggestions are stored for the host.
190
+ - Accounting outside sprints (B23): a line whose `endedAt` lies outside every sprint window has no sprint to charge.
191
+ - Waterfall deliverables (B10): `deliverables-present` aggregates raw item evidence while stage gates use fresh evidence; the two diverge only when one config uses both.
192
+ - Rework in flight (B10): an item reworked mid-flight in a state written before 0.6.0-stage needs fresh evidence before it can pass.
193
+ - Flag values (B24): CLI flags take `--flag value` only; a value beginning with `--` must come via a JSON input file; `--flag=value` is not supported.
194
+ - Stale lock unlink: `withStateLock` retries without reaching the retry cap when a stale lock's unlink keeps failing (EPERM, read-only directory); delete the file by hand.
195
+ - Scrum `status.report.reason`: it is always `budget`; the engine has no other closing cause yet.
196
+ - Carryover in an unflagged stage (B22): a carryover that was `blocked` or `awaiting_human` in a stage without `review: true` restarts at the sprint start and needs a reestimate; only a stage flagged `review`, or an item still at status `review`, resumes where it was. Flag the stage `review: true` to resume there.
154
197
 
155
198
  ## Development gate
156
199
 
157
200
  There is no hosted CI. A husky pre-commit hook runs the local gate on every commit
158
- (`pnpm format:check && pnpm lint && pnpm typecheck && pnpm test`); `pnpm run ci` adds the
201
+ (`pnpm format:check && pnpm lint && pnpm typecheck && pnpm schema:check && pnpm test`); `pnpm run ci` adds the
159
202
  minified build, the dist identity check and the size ratchet, and `prepublishOnly` runs it
160
203
  before any publish. oxfmt and oxlint need Node ^20.19 or >=22.12 on the developer machine; the
161
204
  package itself supports Node >=18 (verified on ubuntu, macOS and Windows x Node 18/20/22
162
- before the hosted workflow was removed).
205
+ before the hosted workflow was removed). Schema drift is guarded by `pnpm schema:check` alone, not by a test, because the generator needs Node >=22 and the test suite must run on >=18.
163
206
 
164
207
  ## Publishing
165
208
 
@@ -1,2 +1,2 @@
1
- 'use strict';var chunk6R3YFUUV_cjs=require('../chunk-6R3YFUUV.cjs');require('../chunk-S3BXVHO7.cjs');Object.defineProperty(exports,"WORKFLOW_DIR",{enumerable:true,get:function(){return chunk6R3YFUUV_cjs.a}});Object.defineProperty(exports,"loadState",{enumerable:true,get:function(){return chunk6R3YFUUV_cjs.e}});Object.defineProperty(exports,"saveBrief",{enumerable:true,get:function(){return chunk6R3YFUUV_cjs.f}});Object.defineProperty(exports,"saveHandoff",{enumerable:true,get:function(){return chunk6R3YFUUV_cjs.g}});Object.defineProperty(exports,"saveState",{enumerable:true,get:function(){return chunk6R3YFUUV_cjs.d}});Object.defineProperty(exports,"statePath",{enumerable:true,get:function(){return chunk6R3YFUUV_cjs.b}});Object.defineProperty(exports,"withStateLock",{enumerable:true,get:function(){return chunk6R3YFUUV_cjs.c}});//# sourceMappingURL=index.cjs.map
1
+ 'use strict';var chunkYRATJT7P_cjs=require('../chunk-YRATJT7P.cjs');require('../chunk-WOGK54HB.cjs'),require('../chunk-S3BXVHO7.cjs');Object.defineProperty(exports,"WORKFLOW_DIR",{enumerable:true,get:function(){return chunkYRATJT7P_cjs.a}});Object.defineProperty(exports,"loadState",{enumerable:true,get:function(){return chunkYRATJT7P_cjs.e}});Object.defineProperty(exports,"saveBrief",{enumerable:true,get:function(){return chunkYRATJT7P_cjs.f}});Object.defineProperty(exports,"saveHandoff",{enumerable:true,get:function(){return chunkYRATJT7P_cjs.g}});Object.defineProperty(exports,"saveState",{enumerable:true,get:function(){return chunkYRATJT7P_cjs.d}});Object.defineProperty(exports,"statePath",{enumerable:true,get:function(){return chunkYRATJT7P_cjs.b}});Object.defineProperty(exports,"withStateLock",{enumerable:true,get:function(){return chunkYRATJT7P_cjs.c}});//# sourceMappingURL=index.cjs.map
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1,2 +1,2 @@
1
- export{a as WORKFLOW_DIR,e as loadState,f as saveBrief,g as saveHandoff,d as saveState,b as statePath,c as withStateLock}from'../chunk-VGD4V5BA.mjs';import'../chunk-2FRRD6A3.mjs';//# sourceMappingURL=index.mjs.map
1
+ export{a as WORKFLOW_DIR,e as loadState,f as saveBrief,g as saveHandoff,d as saveState,b as statePath,c as withStateLock}from'../chunk-7IG6EQI4.mjs';import'../chunk-5TXYMLBF.mjs';import'../chunk-2FRRD6A3.mjs';//# sourceMappingURL=index.mjs.map
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1,4 +1,4 @@
1
- import { WorkflowState, Role, Capability } from './types.js';
1
+ import { WorkflowState, Role, Capability, Budget, Accounting } from './types.js';
2
2
 
3
3
  declare function registerRole(state: WorkflowState, role: Role, by: string, now?: string): WorkflowState;
4
4
  declare function retireRole(state: WorkflowState, roleId: string, by: string, now?: string): WorkflowState;
@@ -11,4 +11,11 @@ declare function separationLost(state: WorkflowState, required: Capability[]): R
11
11
  declare function canDo(state: WorkflowState, by: string, ...caps: Capability[]): boolean;
12
12
  declare function requireCap(state: WorkflowState, by: string, ...caps: Capability[]): void;
13
13
 
14
- export { approversFor as a, requireCap as b, canDo as c, requireCapabilities as d, retireRole as e, rolesWith as f, hasCapability as h, registerRole as r, separationLost as s };
14
+ declare const ZERO_SPENT: Required<Budget>;
15
+ declare function addSpent(spent: Required<Budget>, acc: Accounting): Required<Budget>;
16
+ declare function remaining(budget: Budget, spent: Required<Budget>): Budget;
17
+ declare function isExhausted(budget: Budget, spent: Required<Budget>): boolean;
18
+ type EstimateMode = "per-weight" | "per-call";
19
+ declare function estimateTokens(state: WorkflowState, weight: number | undefined, fallbackPerWeight?: number, mode?: EstimateMode): number;
20
+
21
+ export { type EstimateMode as E, ZERO_SPENT as Z, addSpent as a, approversFor as b, canDo as c, remaining as d, estimateTokens as e, requireCap as f, requireCapabilities as g, hasCapability as h, isExhausted as i, retireRole as j, rolesWith as k, registerRole as r, separationLost as s };
@@ -1,4 +1,4 @@
1
- import { WorkflowState, Role, Capability } from './types.cjs';
1
+ import { WorkflowState, Role, Capability, Budget, Accounting } from './types.cjs';
2
2
 
3
3
  declare function registerRole(state: WorkflowState, role: Role, by: string, now?: string): WorkflowState;
4
4
  declare function retireRole(state: WorkflowState, roleId: string, by: string, now?: string): WorkflowState;
@@ -11,4 +11,11 @@ declare function separationLost(state: WorkflowState, required: Capability[]): R
11
11
  declare function canDo(state: WorkflowState, by: string, ...caps: Capability[]): boolean;
12
12
  declare function requireCap(state: WorkflowState, by: string, ...caps: Capability[]): void;
13
13
 
14
- export { approversFor as a, requireCap as b, canDo as c, requireCapabilities as d, retireRole as e, rolesWith as f, hasCapability as h, registerRole as r, separationLost as s };
14
+ declare const ZERO_SPENT: Required<Budget>;
15
+ declare function addSpent(spent: Required<Budget>, acc: Accounting): Required<Budget>;
16
+ declare function remaining(budget: Budget, spent: Required<Budget>): Budget;
17
+ declare function isExhausted(budget: Budget, spent: Required<Budget>): boolean;
18
+ type EstimateMode = "per-weight" | "per-call";
19
+ declare function estimateTokens(state: WorkflowState, weight: number | undefined, fallbackPerWeight?: number, mode?: EstimateMode): number;
20
+
21
+ export { type EstimateMode as E, ZERO_SPENT as Z, addSpent as a, approversFor as b, canDo as c, remaining as d, estimateTokens as e, requireCap as f, requireCapabilities as g, hasCapability as h, isExhausted as i, retireRole as j, rolesWith as k, registerRole as r, separationLost as s };
@@ -1,2 +1,2 @@
1
- 'use strict';var chunk23P7C25E_cjs=require('./chunk-23P7C25E.cjs'),chunkS3BXVHO7_cjs=require('./chunk-S3BXVHO7.cjs'),exsorted=require('exsorted');var M=new Set(["in_progress","blocked","awaiting_human"]),P=new Set(["todo","in_progress","blocked","review","awaiting_human"]);function oe(e){let t=e.stages[0];return t||chunkS3BXVHO7_cjs.b("config-invalid","kanban board has no columns","init with at least options, a commitment column and a delivery column"),t}function B(e){let t=e.stages.find(a=>a.commitmentPoint);return t||chunkS3BXVHO7_cjs.b("config-invalid","no column is the commitment point","set commitmentPoint: true on exactly one column"),t}function X(e){let t=e.stages.find(a=>a.deliveryPoint);return t||chunkS3BXVHO7_cjs.b("config-invalid","no column is the delivery point","set deliveryPoint: true on the last column"),t}function Q(e,t){return chunk23P7C25E_cjs.p(e,t),e.stages.findIndex(a=>a.id===t)}function E(e,t){let a=Q(e,t.stage);return a>=Q(e,B(e).id)&&a<Q(e,X(e).id)}function J(e){return Object.values(e.items).filter(t=>t.classOfService==="expedite"&&M.has(t.status)&&E(e,t)).map(t=>t.id)}function N(e){for(let t=e.transitions.length-1;t>=0;t--){let a=e.transitions[t];if(a.to.stage===e.stage&&a.from.stage!==e.stage)return a.at}}var U=["replenishment","delivery-planning","service-delivery-review","operations-review","risk-review","strategy-review"],D=["expedite","fixed-date","standard","intangible"],ee=864e5;function x(e){return e.config}var Ce=[{id:"options",name:"Options",policies:[{kind:"acceptance-written"}]},{id:"ready",name:"Ready",wipLimit:4,commitmentPoint:true,policies:[{kind:"dependencies-done"}]},{id:"dev",name:"Development",wipLimit:3,policies:[{kind:"evidence",evidenceKind:"test-output"}]},{id:"review",name:"Review",wipLimit:2,review:true,policies:[{kind:"no-open-blockers"}]},{id:"done",name:"Done",deliveryPoint:true,policies:[]}],K=e=>typeof e=="object"&&e!==null&&!Array.isArray(e);function _(e,t,a,s=1){if(e!==void 0)return (typeof e!="number"||!Number.isFinite(e)||e<s)&&chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.${t} must be a number >= ${s} (got ${JSON.stringify(e)})`,a),e}var Ae=["acceptance-written","estimate-present","no-open-blockers","dependencies-done"],se=e=>typeof e=="string"&&e.trim()!=="";function ae(e,t){Array.isArray(e)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.${t} must be an array of Policy objects`,'e.g. [{ "kind": "evidence", "evidenceKind": "test-output" }]');let a=[];return e.forEach((s,m)=>{let u=`kanban config.${t}[${m}]`;K(s)&&(s.kind==="evidence"?se(s.evidenceKind):s.kind==="custom"?se(s.id)&&se(s.description):typeof s.kind=="string"&&Ae.includes(s.kind))||chunkS3BXVHO7_cjs.b("config-invalid",`${u} must be a Policy`,`use a Policy such as { "kind": "evidence", "evidenceKind": "test-output" }, { "kind": "custom", "id": "x", "description": "..." } or one of: ${Ae.join(", ")}`),a.push(s);}),a}function Le(e,t){K(e)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.columns[${t}] must be an object`,'e.g. { "id": "dev", "policies": [] }'),chunkS3BXVHO7_cjs.c(e.id,`kanban config.columns[${t}].id`);let a=e.id;Array.isArray(e.policies)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.columns[${t}] ("${a}") needs policies: Policy[]`,'every column states its policies explicitly, e.g. [{ "kind": "acceptance-written" }]');let s={id:a,name:typeof e.name=="string"&&e.name.trim()!==""?e.name:a,policies:ae(e.policies,`columns[${t}].policies`)},m=_(e.wipLimit,`columns[${t}].wipLimit`,"omit wipLimit for no limit");m!==void 0&&(s.wipLimit=m);for(let u of ["commitmentPoint","deliveryPoint","review"])e[u]!==void 0&&(typeof e[u]!="boolean"&&chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.columns[${t}].${u} must be a boolean`,`set ${u}: true or omit it`),e[u]&&(s[u]=true));return s}function Y(e){let t=e??{};t.columns!==void 0&&!Array.isArray(t.columns)&&chunkS3BXVHO7_cjs.b("config-invalid","kanban config.columns must be an array of columns","omit columns for the default board (options, ready, dev, review, done)");let a=t.columns===void 0?Ce.map(r=>({...r})):t.columns.map(Le),s=new Set;for(let r of a)s.has(r.id)&&chunkS3BXVHO7_cjs.b("config-invalid",`duplicate column id "${r.id}"`,"give every column a unique id"),s.add(r.id);let m={},u=t.classesOfService??{};K(u)||chunkS3BXVHO7_cjs.b("config-invalid","kanban config.classesOfService must be an object keyed by class",'e.g. { "expedite": { "wipBypass": 1 } }');for(let[r,c]of Object.entries(u)){D.includes(r)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.classesOfService.${r} is not a class of service`,`use one of: ${D.join(", ")}`),K(c)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.classesOfService.${r} must be an object`,'e.g. { "wipBypass": 1, "sle": { "percentile": 85, "targetMs": 604800000 } }');let d={},f=_(c.wipBypass,`classesOfService.${r}.wipBypass`,"omit it for the default 1",0);if(f!==void 0&&(d.wipBypass=f),c.sle!==void 0){let y=c.sle;(!K(y)||typeof y.percentile!="number"||y.percentile<0||y.percentile>100)&&chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.classesOfService.${r}.sle.percentile must be 0..100`,'e.g. { "percentile": 85, "targetMs": 604800000 }');let k=_(y.targetMs,`classesOfService.${r}.sle.targetMs`,"state the service level target in milliseconds");k===void 0&&chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.classesOfService.${r}.sle.targetMs is required`,"state the service level target in milliseconds"),d.sle={percentile:y.percentile,targetMs:k};}m[r]=d;}let g=t.fairness??{};(!K(g)||g.aging!==void 0&&typeof g.aging!="boolean")&&chunkS3BXVHO7_cjs.b("config-invalid","kanban config.fairness must be { aging: boolean, maxDeferralRounds?: number }","omit it for aging on");let l=_(g.maxDeferralRounds,"fairness.maxDeferralRounds","omit it for 5 rounds"),i={aging:g.aging===void 0?true:g.aging,maxDeferralRounds:l??5},o={},n=t.cadences??{};K(n)||chunkS3BXVHO7_cjs.b("config-invalid","kanban config.cadences must be an object keyed by cadence",'e.g. { "replenishment": { "everyTurns": 10 } }');for(let[r,c]of Object.entries(n)){U.includes(r)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.cadences.${r} is not a cadence`,`use one of: ${U.join(", ")}`),K(c)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.cadences.${r} must be an object`,'e.g. { "everyTurns": 10 }');let d=_(c.everyTurns,`cadences.${r}.everyTurns`,"omit it when the cadence is run by hand");o[r]=d===void 0?{}:{everyTurns:d};}return {columns:a,classesOfService:m,fairness:i,cadences:o,throughputWindowMs:_(t.throughputWindowMs,"throughputWindowMs","omit it for one week")??7*ee,maxExpedite:_(t.maxExpedite,"maxExpedite","omit it for 1")??1}}function te(e,t){return e.events.find(s=>s.type==="item.added"&&s.payload.id===t)?.at??e.events[0].at}function ne(e,t,a){return Date.parse(a)-Date.parse(N(t)??te(e,t.id))}function Ke(e,t){return e.stages.map(a=>{let s=Object.values(e.items).filter(l=>l.stage===a.id&&l.status!=="dropped"),m=exsorted.timSort(s.map(l=>({id:l.id,title:l.title,type:l.type,status:l.status,classOfService:l.classOfService??"standard",role:l.role??null,ageMs:ne(e,l,t),blocked:l.blockers.some(i=>!i.clearedAt),deadline:l.deadline??null})),(l,i)=>i.ageMs-l.ageMs||(l.id<i.id?-1:1)),u=m.filter(l=>M.has(l.status)).length,g=m.filter(l=>l.status==="todo"||l.status==="review").length;return {id:a.id,name:a.name,wipLimit:a.wipLimit??null,active:u,queued:g,overLimit:a.wipLimit!==void 0&&u>a.wipLimit,commitmentPoint:a.commitmentPoint===true,deliveryPoint:a.deliveryPoint===true,cards:m}})}var Re=e=>`cadence.${e}`;function ce(e){let t=x(e);return U.map(a=>{let s=0,m=null;for(let l of e.events)l.type===Re(a)&&(s=l.seq,m=l.at);let u=e.events.filter(l=>l.type==="item.completed"&&l.seq>s).length,g=t.cadences[a]?.everyTurns??null;return {cadence:a,everyTurns:g,lastAt:m,turnsSince:u,due:g!==null&&u>=g}})}var $e=(e,t)=>Date.parse(t)-Date.parse(e),Me=e=>e.transition??null;function ie(e,t,a){let s=new Map,m=[],u=e[0]?.at??"1970-01-01T00:00:00.000Z",g=(i,o,n)=>{o===t&&i.committedAt===void 0&&(i.committedAt=n),o===a&&i.deliveredAt===void 0&&(i.deliveredAt=n);},l=(i,o,n)=>{i.since.status==="in_progress"&&(i.activeMs+=$e(i.since.at,n)),i.since.status==="blocked"&&(i.blockedMs+=$e(i.since.at,n)),i.since={status:o.status,at:n},o.stage!==i.stage&&(i.stays[i.stays.length-1].leftAt=n,i.stays.push({stage:o.stage,enteredAt:n}),g(i,o.stage,n)),i.stage=o.stage,i.status=o.status;};for(let i of e){u=i.at;let o=i.payload??{};switch(i.type){case "item.added":{let n=o.id,r=o.stage,c={id:n,type:typeof o.type=="string"?o.type:"task",stage:r,status:"todo",stays:[{stage:r,enteredAt:i.at}],classOfService:"standard",activeMs:0,blockedMs:0,since:{status:"todo",at:i.at}};g(c,r,i.at),s.set(n,c);break}case "item.transition":{let n=s.get(o.id);if(!n)break;l(n,o.to,i.at);break}case "cadence.replenishment":{let n=o.items??[];for(let r of n){let c=s.get(r.id);c&&(c.classOfService=r.classOfService,r.deadline?c.deadline=r.deadline:delete c.deadline);}break}case "blocker.raised":{m.push({item:o.id,blocker:o.blocker,raisedAt:i.at});let n=s.get(o.id),r=Me(o);n&&r&&l(n,r.to,i.at);break}case "blocker.cleared":{let n=m.find(d=>d.item===o.id&&d.blocker===o.blocker&&d.clearedAt===void 0);n&&(n.clearedAt=i.at);let r=s.get(o.id),c=Me(o);r&&c&&l(r,c.to,i.at);break}}}return {items:s,blockers:m,lastAt:u}}function q(e,t){if(e.length===0)return 0;let a=exsorted.timSort([...e],(m,u)=>m-u),s=Math.min(a.length,Math.max(1,Math.ceil(t/100*a.length)));return a[s-1]}var H=(e,t)=>Date.parse(t)-Date.parse(e),Pe=(e,t)=>t===0?0:e/t;function Ee(e){let t=e.stages.find(s=>s.commitmentPoint)?.id??"",a=e.stages.find(s=>s.deliveryPoint)?.id??"";return {commitment:t,delivery:a,ci:e.stages.findIndex(s=>s.id===t),di:e.stages.findIndex(s=>s.id===a)}}function De(e){let t={};for(let a of e.items.values())for(let s of a.stays)s.leftAt!==void 0&&(t[s.stage]??=[]).push(H(s.enteredAt,s.leftAt));return t}function de(e){let t=Ee(e);return De(ie(e.events,t.commitment,t.delivery))}function G(e){let t=x(e),a=Ee(e),s=ie(e.events,a.commitment,a.delivery),m=[...s.items.values()],u=e.stages.map(p=>p.id),g=new Map(u.map((p,b)=>[p,b])),l=exsorted.timSort(m.filter(p=>p.type!=="question"&&p.committedAt!==void 0&&p.deliveredAt!==void 0),(p,b)=>Date.parse(p.deliveredAt)-Date.parse(b.deliveredAt)),i=l.map(p=>H(p.committedAt,p.deliveredAt)),o={};for(let[p,b]of Object.entries(De(s)))o[p]=b.reduce((h,S)=>h+S,0)/b.length;let n=m.filter(p=>p.type!=="question"&&p.committedAt!==void 0).map(p=>Date.parse(p.committedAt)),r=[];if(n.length>0){let p=n.reduce((h,S)=>S<h?S:h,n[0]),b=Math.floor((Date.parse(s.lastAt)-p)/t.throughputWindowMs)+1;r=Array.from({length:b},()=>0);for(let h of l){let S=Math.floor((Date.parse(h.deliveredAt)-p)/t.throughputWindowMs);S>=0&&S<b&&(r[S]+=1);}}let c=Object.fromEntries(u.map(p=>[p,0])),d={};for(let p of m){if(!P.has(p.status))continue;Object.hasOwn(c,p.stage)&&(c[p.stage]+=1);let b=g.get(p.stage);b!==void 0&&b>=a.ci&&b<a.di&&(d[p.id]=H(p.stays[p.stays.length-1].enteredAt,s.lastAt));}let f=new Map;for(let p of m)for(let b of p.stays){let h=f.get(b.stage)??new Map;h.has(p.id)||h.set(p.id,b.enteredAt),f.set(b.stage,h);}let y=exsorted.timSort([...new Set(e.events.map(p=>p.at.slice(0,10)))],(p,b)=>p<b?-1:p>b?1:0),k={};for(let p of y)for(let b of u)k[`${p}:${b}`]=[...f.get(b)?.values()??[]].filter(h=>h.slice(0,10)<=p).length;let w={};for(let[p,b]of Object.entries(t.classesOfService)){if(!b?.sle)continue;let h=l.filter(S=>S.classOfService===p);h.length!==0&&(w[p]=h.filter(S=>H(S.committedAt,S.deliveredAt)<=b.sle.targetMs).length/h.length);}let I=s.blockers.reduce((p,b)=>p+H(b.raisedAt,b.clearedAt??s.lastAt),0);return {leadTimeMs:i,leadTimeP50:q(i,50),leadTimeP85:q(i,85),cycleTimeMsByColumn:o,throughput:r,wipByColumn:c,flowEfficiency:Pe(l.reduce((p,b)=>p+b.activeMs,0),i.reduce((p,b)=>p+b,0)),agingWipMs:d,cumulativeFlow:k,sleCompliance:w,blockerTimeMs:I,meanBlockerTimeMs:Pe(I,s.blockers.length)}}function me(e,t){let a=Date.parse(t),s=de(e),m=[],u=[],g=Object.fromEntries(e.stages.map(i=>[i.id,0])),l=Object.fromEntries(e.stages.map(i=>[i.id,0]));for(let i of Object.values(e.items)){for(let o of i.blockers)o.clearedAt||m.push({item:i.id,blocker:o.id,owner:o.owner,reason:o.reason,ageMs:a-Date.parse(o.raisedAt)});if(M.has(i.status)?l[i.stage]=(l[i.stage]??0)+1:(i.status==="todo"||i.status==="review")&&(g[i.stage]=(g[i.stage]??0)+1),P.has(i.status)&&E(e,i)){let o=s[i.stage]??[],n=o.length?q(o,85):null,r=ne(e,i,t);u.push({item:i.id,column:i.stage,status:i.status,ageMs:r,p85Ms:n,overP85:n!==null&&r>n});}}return {blockers:m,agingWip:exsorted.timSort(u,(i,o)=>o.ageMs-i.ageMs||(i.item<o.item?-1:1)),queued:g,active:l}}function Ne(e,t){let a=B(e).id;return e.items[t].transitions.find(u=>u.to.stage===a&&u.from.stage!==a)?.at??te(e,t)}function fe(e,t,a){let s=Date.parse(t),m=x(e),u=G(e).leadTimeMs,g=m.classesOfService["fixed-date"]?.sle?.percentile??85,l=u.length?q(u,g):null,i=[],o=[];for(let n of Object.values(e.items)){for(let f of n.blockers){let y=f.clearedAt?0:s-Date.parse(f.raisedAt);!f.clearedAt&&y>a&&o.push({item:n.id,blocker:f.id,owner:f.owner,ageMs:y});}if(n.classOfService!=="fixed-date"||!n.deadline||!P.has(n.status)||!E(e,n))continue;let r=s-Date.parse(Ne(e,n.id)),c=l===null?null:Math.max(0,l-r),d=Date.parse(n.deadline)-s;i.push({item:n.id,deadline:n.deadline,timeLeftMs:d,projectedRemainingMs:c,atRisk:d<0||c!==null&&c>d});}return {fixedDate:i,staleBlockers:o,expediteActive:J(e)}}var V=e=>typeof e=="object"&&e!==null&&!Array.isArray(e);function ue(e,t,a){let s=t.items;(!Array.isArray(s)||s.length===0||!s.every(u=>typeof u=="string"))&&chunkS3BXVHO7_cjs.b("invalid-input",`${a} needs items: string[]`,'pass ["A", "B"]');let m=s.find((u,g)=>s.indexOf(u)!==g);m!==void 0&&chunkS3BXVHO7_cjs.b("invalid-input",`item "${m}" appears twice in items[]`,"list each item once",{items:[m]});for(let u of s)chunk23P7C25E_cjs.u(e,u);return s}function _e(e){return V(e)&&typeof e.percentile=="number"&&e.percentile>=0&&e.percentile<=100&&typeof e.targetMs=="number"&&Number.isFinite(e.targetMs)&&e.targetMs>0}function Ie(e,t){let{wipLimit:a,...s}=e;return t===null?s:{...s,wipLimit:t}}function pe(e){let t=(i,o,n,r)=>chunk23P7C25E_cjs.M(i,e(),{type:o,by:n.by,payload:n},c=>r(c,chunk23P7C25E_cjs.a(n.now)));return {replenishment:(i,o)=>{chunk23P7C25E_cjs.o(i,o.by,"order-backlog","manage-flow");let n=ue(i,o,"replenishment"),r=B(i),c=oe(i);for(let d of n)chunk23P7C25E_cjs.E(i,chunk23P7C25E_cjs.u(i,d),c.exitPolicy,`leave "${c.id}"`);return t(i,"ceremony.replenishment",o,(d,f)=>{for(let y of n)d=chunk23P7C25E_cjs.x(d,y,{stage:r.id,status:"todo"},o.by,f);return chunk23P7C25E_cjs.c(d,"cadence.replenishment",o.by,{column:r.id,items:n.map(y=>{let k=chunk23P7C25E_cjs.u(d,y);return {id:y,classOfService:k.classOfService??"standard",deadline:k.deadline??null}})},f)})},"delivery-planning":(i,o)=>{chunk23P7C25E_cjs.o(i,o.by,"facilitate","manage-flow");let n=ue(i,o,"delivery-planning"),r=X(i),c=i.stages[i.stages.findIndex(d=>d.id===r.id)-1];for(let d of n){let f=chunk23P7C25E_cjs.u(i,d);f.stage!==c.id&&f.stage!==r.id&&chunkS3BXVHO7_cjs.b("invalid-input",`item "${d}" is in "${f.stage}", not ready for delivery`,`only cards in "${c.id}" or "${r.id}" can be planned for delivery`,{items:[d]});}return t(i,"ceremony.delivery-planning",o,(d,f)=>chunk23P7C25E_cjs.c(d,"cadence.delivery-planning",o.by,{items:n,window:typeof o.window=="string"?o.window:null,delivered:n.filter(y=>chunk23P7C25E_cjs.u(d,y).status==="done")},f))},"service-delivery-review":(i,o)=>{chunk23P7C25E_cjs.o(i,o.by,"manage-flow");let n=o.wip??{},r=o.policies??{},c=o.sle??{};(!V(n)||!V(r)||!V(c))&&chunkS3BXVHO7_cjs.b("invalid-input","service-delivery-review takes wip, policies and sle objects keyed by column or class",'e.g. { "wip": { "dev": 2 }, "policies": { "dev": [{ "kind": "no-open-blockers" }] }, "sle": { "standard": { "percentile": 85, "targetMs": 604800000 } } }');for(let[f,y]of Object.entries(n))chunk23P7C25E_cjs.p(i,f),y!==null&&(typeof y!="number"||!Number.isFinite(y)||y<1)&&chunkS3BXVHO7_cjs.b("invalid-input",`wip.${f} must be a positive number or null`,"null removes the limit");let d={};for(let[f,y]of Object.entries(r))chunk23P7C25E_cjs.p(i,f),d[f]=ae(y,`policies.${f}`);for(let[f,y]of Object.entries(c))D.includes(f)||chunkS3BXVHO7_cjs.b("invalid-input",`sle.${f} is not a class of service`,`use one of: ${D.join(", ")}`),y!==null&&!_e(y)&&chunkS3BXVHO7_cjs.b("invalid-input",`sle.${f} must be { percentile: 0..100, targetMs: > 0 } or null`,"null removes the service level");return t(i,"ceremony.service-delivery-review",o,(f,y)=>{let k=x(f),w=f.stages,I=k.columns,p={...k.classesOfService},b=[],h=[],S=[];for(let[O,A]of Object.entries(n)){let W=w.find(R=>R.id===O).wipLimit??null;W!==A&&(b.push({column:O,before:W,after:A}),w=w.map(R=>R.id===O?Ie(R,A):R),I=I.map(R=>R.id===O?Ie(R,A):R));}for(let[O,A]of Object.entries(d)){let W=w.find($=>$.id===O).exitPolicy;if(JSON.stringify(W)===JSON.stringify(A))continue;h.push({column:O,before:W,after:A});let R=w.findIndex($=>$.id===O),z=w[R].commitmentPoint?w[R+1]?.id:void 0;w=w.map($=>$.id===O?{...$,exitPolicy:A}:$.id===z?{...$,entryPolicy:A}:$),I=I.map($=>$.id===O?{...$,policies:A}:$);}for(let[O,A]of Object.entries(c)){let W=p[O]?.sle??null;if(JSON.stringify(W)===JSON.stringify(A))continue;S.push({classOfService:O,before:W,after:A});let{sle:R,...z}=p[O]??{};p={...p,[O]:A===null?z:{...z,sle:A}};}return f={...f,stages:w,config:{...k,columns:I,classesOfService:p}},chunk23P7C25E_cjs.c(f,"cadence.service-delivery-review",o.by,{wip:b,policies:h,sle:S},y)})},"operations-review":(i,o)=>{chunk23P7C25E_cjs.o(i,o.by,"facilitate","manage-flow");let n=o.assign??[];(!Array.isArray(n)||!n.every(r=>V(r)&&typeof r.item=="string"&&typeof r.blocker=="string"))&&chunkS3BXVHO7_cjs.b("invalid-input","operations-review.assign must be [{ item, blocker, owner }]",'e.g. { "assign": [{ "item": "A", "blocker": "A-b1", "owner": "service-delivery-manager" }] }');for(let r of n){let c=chunk23P7C25E_cjs.u(i,r.item);c.blockers.some(d=>d.id===r.blocker&&!d.clearedAt)||chunkS3BXVHO7_cjs.b("invalid-input",`item "${r.item}" has no open blocker "${r.blocker}"`,`open blockers: ${c.blockers.filter(d=>!d.clearedAt).map(d=>d.id).join(", ")||"(none)"}`,{items:[r.item]});}return t(i,"ceremony.operations-review",o,(r,c)=>{for(let d of n){let f=chunk23P7C25E_cjs.u(r,d.item).blockers.find(y=>y.id===d.blocker).owner;r=chunk23P7C25E_cjs.s(r,d.item,y=>({...y,blockers:y.blockers.map(k=>k.id===d.blocker?{...k,owner:d.owner}:k)})),r=chunk23P7C25E_cjs.c(r,"blocker.reassigned",o.by,{id:d.item,blocker:d.blocker,from:f,to:d.owner},c);}return chunk23P7C25E_cjs.c(r,"cadence.operations-review",o.by,me(r,c),c)})},"risk-review":(i,o)=>{chunk23P7C25E_cjs.o(i,o.by,"facilitate","manage-flow");let n=o.staleAfterMs??7*ee;return (typeof n!="number"||!Number.isFinite(n)||n<0)&&chunkS3BXVHO7_cjs.b("invalid-input","risk-review.staleAfterMs must be a non-negative number of milliseconds","omit it for one week"),t(i,"ceremony.risk-review",o,(r,c)=>chunk23P7C25E_cjs.c(r,"cadence.risk-review",o.by,fe(r,c,n),c))},"strategy-review":(i,o)=>{chunk23P7C25E_cjs.o(i,o.by,"manage-flow");let n=x(i),r=o.classesOfService??{};V(r)||chunkS3BXVHO7_cjs.b("invalid-input","strategy-review.classesOfService must be an object keyed by class",'e.g. { "expedite": { "wipBypass": 2 }, "intangible": null }');let c={...n.classesOfService};for(let[f,y]of Object.entries(r))D.includes(f)||chunkS3BXVHO7_cjs.b("invalid-input",`classesOfService.${f} is not a class of service`,`use one of: ${D.join(", ")}`),y===null?delete c[f]:c[f]=y;let d=Y({...n,classesOfService:c,maxExpedite:o.maxExpedite??n.maxExpedite});return t(i,"ceremony.strategy-review",o,(f,y)=>{let k=Object.fromEntries(D.map(w=>[w,0]));for(let w of Object.values(f.items))P.has(w.status)&&E(f,w)&&(k[w.classOfService??"standard"]+=1);return f={...f,config:{...x(f),classesOfService:d.classesOfService,maxExpedite:d.maxExpedite}},chunk23P7C25E_cjs.c(f,"cadence.strategy-review",o.by,{mix:k,before:{classesOfService:n.classesOfService,maxExpedite:n.maxExpedite},after:{classesOfService:d.classesOfService,maxExpedite:d.maxExpedite}},y)})}}}function ge(){return [{id:"service-request-manager",name:"Service Request Manager",modelTier:"large",capabilities:["order-backlog","define-acceptance","manage-flow","escalate"],canApprove:[],inputs:["stakeholder-requests","options"],outputs:["ordered-options","acceptance-criteria","classes-of-service"],do_not:["do not implement items yourself","do not push work past the commitment point outside replenishment"]},{id:"service-delivery-manager",name:"Service Delivery Manager",modelTier:"large",capabilities:["facilitate","manage-flow","escalate"],canApprove:[],inputs:["board","flow-metrics","blockers"],outputs:["cadence-records","wip-and-policy-changes","lessons"],do_not:["do not reorder the options","do not assign work; the planner pulls"]},{id:"worker",name:"Worker",modelTier:"small",capabilities:["deliver","review","verify"],canApprove:[],inputs:["brief"],outputs:["code","test-output"],do_not:["do not change acceptance criteria","do not pull more work while your card is open"]}]}var C=(e,t,a,s,m)=>m?{rule:e,severity:t,item:m,message:a,fix:s}:{rule:e,severity:t,message:a,fix:s},Z=e=>e.payload??{},re=e=>{let t=Z(e).items;return Array.isArray(t)?t:[]},qe=(e,t)=>Object.values(e.items).filter(a=>a.stage===t&&M.has(a.status)).length;function ye(){return [{id:"policy-explicit",severity:"error",check(l,i){let o=l.stages.find(n=>n.deliveryPoint)?.id;if(i.type==="init")return l.stages.filter(n=>n.id!==o&&n.exitPolicy.length===0).map(n=>C("policy-explicit","error",`column "${n.id}" has no explicit policy`,`give column "${n.id}" at least one policy, e.g. { "kind": "acceptance-written" }`));if(i.type==="ceremony.service-delivery-review"){let n=Z(i).policies??{};return Object.entries(n).filter(([r,c])=>r!==o&&Array.isArray(c)&&c.length===0).map(([r])=>C("policy-explicit","error",`column "${r}" would be left without a policy`,"keep at least one policy per column; Kanban policies are explicit"))}return []}},{id:"commitment-point-once",severity:"error",check(l,i){if(i.type!=="init")return [];let o=[],n=l.stages.filter(c=>c.commitmentPoint),r=l.stages.filter(c=>c.deliveryPoint);if(n.length!==1&&o.push(C("commitment-point-once","error",`${n.length} columns are marked commitmentPoint (${n.map(c=>c.id).join(", ")||"none"})`,"mark exactly one column commitmentPoint: true")),r.length!==1&&o.push(C("commitment-point-once","error",`${r.length} columns are marked deliveryPoint (${r.map(c=>c.id).join(", ")||"none"})`,"mark exactly one column deliveryPoint: true")),n.length===1&&r.length===1){let c=l.stages.indexOf(n[0]),d=l.stages.indexOf(r[0]);d!==l.stages.length-1&&o.push(C("commitment-point-once","error",`delivery column "${r[0].id}" is not the last column`,"move the delivery column to the end; finished cards leave the board there")),c===0?o.push(C("commitment-point-once","error",`commitment column "${n[0].id}" is the first column`,"keep an Options column before the commitment point; replenishment pulls from it")):c>=d&&o.push(C("commitment-point-once","error",`commitment column "${n[0].id}" comes after the delivery column`,"order columns options \u2192 commitment \u2192 \u2026 \u2192 delivery"));}return o}},{id:"wip-respected",severity:"error",check(l,i){if(i.type==="ceremony.replenishment"){let o=l.stages.find(f=>f.commitmentPoint);if(!o||o.wipLimit===void 0)return [];let n=re(i),r=Object.values(l.items).filter(f=>f.stage===o.id&&P.has(f.status)).length,c=n.some(f=>l.items[f]?.classOfService==="expedite")?x(l).classesOfService.expedite?.wipBypass??1:0,d=o.wipLimit+c;return r+n.length<=d?[]:[C("wip-respected","error",`column "${o.id}" holds ${r} card(s); pulling ${n.length} more exceeds its WIP limit of ${o.wipLimit}${c?` (+${c} expedite bypass)`:""}`,`pull at most ${Math.max(0,d-r)} card(s) now, or raise the limit in a service-delivery-review`)]}if(i.type==="ceremony.service-delivery-review"){let o=Z(i).wip??{},n=[];for(let[r,c]of Object.entries(o)){if(typeof c!="number")continue;let d=qe(l,r);d>c&&n.push(C("wip-respected","warning",`column "${r}" has ${d} active card(s), above the new limit of ${c}`,"nothing new is pulled into it until it drains; finish or move cards"));}return n}return []}},{id:"pull-not-push",severity:"error",check(l,i){if(i.type!=="ceremony.replenishment")return [];let o=l.stages[0],n=l.stages.find(d=>d.commitmentPoint);if(!o||!n)return [];let r=[],c=Z(i).column;c!==void 0&&c!==n.id&&r.push(C("pull-not-push","error",`replenishment may only pull into the commitment column "${n.id}", not "${String(c)}"`,"drop column or set it to the commitment column; later columns are pulled by the planner"));for(let d of re(i)){let f=l.items[d];f&&(f.stage!==o.id||f.status!=="todo")&&r.push(C("pull-not-push","error",`item "${d}" is ${f.status} in "${f.stage}", not an option`,`only todo cards in "${o.id}" can be pulled; cards already on the board flow by pull`,d));}return r}},{id:"blocker-has-owner",severity:"error",check(l,i){if(!i.type.startsWith("ceremony."))return [];let o=[];if(i.type==="ceremony.operations-review"){let n=Z(i).assign??[];for(let r of n)(typeof r.owner!="string"||r.owner.trim()==="")&&o.push(C("blocker-has-owner","error",`blocker "${r.blocker}" on "${r.item}" would have no owner`,"name the role or person who clears it",r.item));}for(let n of Object.values(l.items))for(let r of n.blockers)!r.clearedAt&&r.owner.trim()===""&&o.push(C("blocker-has-owner","warning",`blocker "${r.id}" on "${n.id}" has no owner`,`assign one in operations-review: { "assign": [{ "item": "${n.id}", "blocker": "${r.id}", "owner": "<who>" }] }`,n.id));return o}},{id:"one-expedite",severity:"error",check(l,i){if(i.type!=="ceremony.replenishment")return [];let o=re(i).filter(c=>l.items[c]?.classOfService==="expedite");if(o.length===0)return [];let n=Object.values(l.items).filter(c=>c.classOfService==="expedite"&&P.has(c.status)&&E(l,c)).map(c=>c.id),r=x(l).maxExpedite;return n.length+o.length<=r?[]:[C("one-expedite","error",`${n.length} expedite card(s) already on the board (${n.join(", ")||"none"}); pulling ${o.join(", ")} exceeds maxExpedite ${r}`,"deliver the current expedite first, reclassify the new card, or raise maxExpedite in a strategy-review")]}},{id:"fixed-date-needs-deadline",severity:"error",check(l,i){return i.type!=="ceremony.replenishment"?[]:re(i).filter(o=>{let n=l.items[o];return n?.classOfService==="fixed-date"&&!(n.deadline&&Number.isFinite(Date.parse(n.deadline)))}).map(o=>C("fixed-date-needs-deadline","error",`fixed-date item "${o}" has no deadline`,`editItem("${o}", { deadline: "<ISO date>" }) before pulling it`,o))}}]}function be(e){let t=e.columns.map(m=>{let u={id:m.id,name:m.name,entryPolicy:[],exitPolicy:m.policies};return m.wipLimit!==void 0&&(u.wipLimit=m.wipLimit),m.commitmentPoint&&(u.commitmentPoint=true),m.deliveryPoint&&(u.deliveryPoint=true),m.review&&(u.review=true),u}),a=t.findIndex(m=>m.commitmentPoint),s=a>=0?t[a+1]:void 0;return s&&(s.entryPolicy=t[a].exitPolicy),t}var Fe={backlog:"Options",item:"Work Item",stage:"Column",iteration:"Cadence",increment:"Delivery",ceremony:"Cadence",gate:"Policy"},Je={expedite:0,"fixed-date":1,standard:2,intangible:3},We=e=>Je[e.classOfService??"standard"]??2,ve=1e15,Ue=4e9,Ye=(e,t,a)=>Math.min(a,Math.max(t,e));function He(e,t){let a=e.classOfService??"standard";if(e.type==="question")return 5*ve;if(a==="fixed-date"&&e.deadline!==void 0){let l=Date.parse(e.deadline);if(Number.isFinite(l))return 3*ve+1e10-Math.floor(l/1e3)}let s=t.aging?Math.floor(t.deferrals/t.maxDeferralRounds):0,m=Ye(e.priority??0,-1e9,1e9)+s*Ue,u=t.aging&&t.deferrals>=t.maxDeferralRounds;return (a==="expedite"?4:a==="fixed-date"?3:a==="intangible"&&!u?1:2)*ve+m}function je(e,t){let a=We(e)-We(t);if(a!==0)return a;let s=N(e),m=N(t),u=s===void 0?0:Date.parse(s),g=m===void 0?0:Date.parse(m);return u!==g?u-g:e.id<t.id?-1:e.id>t.id?1:0}function Ge(e){return e.map(t=>t.kind==="evidence"?`evidence ${t.evidenceKind}`:t.kind==="custom"?`${t.id}: ${t.description}`:t.kind).join(", ")||"(none)"}function Ze(){return {name:"kanban",vocabulary:Fe,validateConfig:Y,defaultStages:be,defaultRoles:ge,requiredCapabilities:["manage-flow","deliver"],exclusiveCapabilities:[],plannerOptions(e){let t=x(e),a=B(e),s=e.stages[e.stages.findIndex(n=>n.id===a.id)+1],m=t.classesOfService.expedite?.wipBypass??1,u=[],g=[0],l=new Map;for(let n of e.events)u.push(Date.parse(n.at)),g.push(g[g.length-1]+(n.type==="planner.resolved"?1:0)),n.type==="item.added"&&l.set(n.payload.id,n.at);let i=n=>{let r=0,c=u.length;for(;r<c;){let d=r+c>>1;u[d]<=n?r=d+1:c=d;}return g[u.length]-g[r]},o={schedulerMode:"throughput",fairnessPolicy:t.fairness.aging?"aging":"none",deadlineStrategy:"earliest-first",dispatchStage:n=>n.stage===a.id&&s?s.id:void 0,priorityOf:n=>{let r=Date.parse(N(n)??l.get(n.id)??"");return He(n,{deferrals:i(Number.isFinite(r)?r:0),aging:t.fairness.aging,maxDeferralRounds:t.fairness.maxDeferralRounds})},tieBreaker:je,wipCounts:[...M],wipLimitOf:(n,r)=>n.wipLimit===void 0?void 0:r.classOfService==="expedite"?n.wipLimit+m:n.wipLimit};return o.maxDeferralRounds=t.fairness.maxDeferralRounds,o},candidates(e){let t=Math.max(0,x(e).maxExpedite-J(e).length),a=Object.values(e.items).filter(m=>(m.status==="todo"||m.status==="review")&&E(e,m)),s=exsorted.timSort(a.filter(m=>m.classOfService==="expedite"),je).slice(0,t);return a.filter(m=>m.classOfService!=="expedite"||s.includes(m))},invariants(e,t){let a=x(e),s=e.stages.find(l=>l.id===t.stage),m=t.classOfService??"standard",u=a.classesOfService[m]?.sle,g=Object.values(e.items).filter(l=>l.stage===s.id&&M.has(l.status)).length;return [`Column "${s.name}" exit policies: ${Ge(s.exitPolicy)}`,`Class of service: ${m}${t.deadline?`; deadline ${t.deadline}`:""}${u?`; service level p${u.percentile} <= ${u.targetMs} ms`:""}`,`WIP limit of "${s.name}": ${s.wipLimit??"none"} (active now: ${g}); finish this card before pulling another`,`Do not change the scope of "${t.id}" or its acceptance criteria; raise open_questions instead`]},ceremonies:pe(Ze),rules:ye(),metrics:e=>({...G(e)}),humanDefaults:{gates:"none"},report:e=>({cadencesDue:ce(e)})}}exports.A=ue;exports.B=pe;exports.C=ge;exports.D=ye;exports.E=be;exports.F=Fe;exports.G=He;exports.H=je;exports.I=Ze;exports.a=M;exports.b=P;exports.c=oe;exports.d=B;exports.e=X;exports.f=Q;exports.g=E;exports.h=J;exports.i=N;exports.j=U;exports.k=D;exports.l=ee;exports.m=x;exports.n=Ce;exports.o=Y;exports.p=te;exports.q=ne;exports.r=Ke;exports.s=Re;exports.t=ce;exports.u=ie;exports.v=q;exports.w=de;exports.x=G;exports.y=me;exports.z=fe;//# sourceMappingURL=chunk-7HZXZF3L.cjs.map
2
- //# sourceMappingURL=chunk-7HZXZF3L.cjs.map
1
+ 'use strict';var chunkZTE7LREB_cjs=require('./chunk-ZTE7LREB.cjs'),chunkS3BXVHO7_cjs=require('./chunk-S3BXVHO7.cjs'),exsorted=require('exsorted');var M=new Set(["in_progress","blocked","awaiting_human"]),P=new Set(["todo","in_progress","blocked","review","awaiting_human"]);function oe(e){let t=e.stages[0];return t||chunkS3BXVHO7_cjs.b("config-invalid","kanban board has no columns","init with at least options, a commitment column and a delivery column"),t}function K(e){let t=e.stages.find(a=>a.commitmentPoint);return t||chunkS3BXVHO7_cjs.b("config-invalid","no column is the commitment point","set commitmentPoint: true on exactly one column"),t}function X(e){let t=e.stages.find(a=>a.deliveryPoint);return t||chunkS3BXVHO7_cjs.b("config-invalid","no column is the delivery point","set deliveryPoint: true on the last column"),t}function Q(e,t){return chunkZTE7LREB_cjs.p(e,t),e.stages.findIndex(a=>a.id===t)}function E(e,t){let a=Q(e,t.stage);return a>=Q(e,K(e).id)&&a<Q(e,X(e).id)}function J(e){return Object.values(e.items).filter(t=>t.classOfService==="expedite"&&M.has(t.status)&&E(e,t)).map(t=>t.id)}function N(e){for(let t=e.transitions.length-1;t>=0;t--){let a=e.transitions[t];if(a.to.stage===e.stage&&a.from.stage!==e.stage)return a.at}}var U=["replenishment","delivery-planning","service-delivery-review","operations-review","risk-review","strategy-review"],D=["expedite","fixed-date","standard","intangible"],ee=864e5;function x(e){return e.config}var Ce=[{id:"options",name:"Options",policies:[{kind:"acceptance-written"}]},{id:"ready",name:"Ready",wipLimit:4,commitmentPoint:true,policies:[{kind:"dependencies-done"}]},{id:"dev",name:"Development",wipLimit:3,policies:[{kind:"evidence",evidenceKind:"test-output"}]},{id:"review",name:"Review",wipLimit:2,review:true,policies:[{kind:"no-open-blockers"}]},{id:"done",name:"Done",deliveryPoint:true,policies:[]}],B=e=>typeof e=="object"&&e!==null&&!Array.isArray(e);function _(e,t,a,s=1){if(e!==void 0)return (typeof e!="number"||!Number.isFinite(e)||e<s)&&chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.${t} must be a number >= ${s} (got ${JSON.stringify(e)})`,a),e}var Ae=["acceptance-written","estimate-present","no-open-blockers","dependencies-done"],se=e=>typeof e=="string"&&e.trim()!=="";function ae(e,t){Array.isArray(e)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.${t} must be an array of Policy objects`,'e.g. [{ "kind": "evidence", "evidenceKind": "test-output" }]');let a=[];return e.forEach((s,m)=>{let u=`kanban config.${t}[${m}]`;B(s)&&(s.kind==="evidence"?se(s.evidenceKind):s.kind==="custom"?se(s.id)&&se(s.description):typeof s.kind=="string"&&Ae.includes(s.kind))||chunkS3BXVHO7_cjs.b("config-invalid",`${u} must be a Policy`,`use a Policy such as { "kind": "evidence", "evidenceKind": "test-output" }, { "kind": "custom", "id": "x", "description": "..." } or one of: ${Ae.join(", ")}`),a.push(s);}),a}function Le(e,t){B(e)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.columns[${t}] must be an object`,'e.g. { "id": "dev", "policies": [] }'),chunkS3BXVHO7_cjs.c(e.id,`kanban config.columns[${t}].id`);let a=e.id;Array.isArray(e.policies)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.columns[${t}] ("${a}") needs policies: Policy[]`,'every column states its policies explicitly, e.g. [{ "kind": "acceptance-written" }]');let s={id:a,name:typeof e.name=="string"&&e.name.trim()!==""?e.name:a,policies:ae(e.policies,`columns[${t}].policies`)},m=_(e.wipLimit,`columns[${t}].wipLimit`,"omit wipLimit for no limit");m!==void 0&&(s.wipLimit=m);for(let u of ["commitmentPoint","deliveryPoint","review"])e[u]!==void 0&&(typeof e[u]!="boolean"&&chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.columns[${t}].${u} must be a boolean`,`set ${u}: true or omit it`),e[u]&&(s[u]=true));return s}function Y(e){let t=e??{};t.columns!==void 0&&!Array.isArray(t.columns)&&chunkS3BXVHO7_cjs.b("config-invalid","kanban config.columns must be an array of columns","omit columns for the default board (options, ready, dev, review, done)");let a=t.columns===void 0?Ce.map(r=>({...r})):t.columns.map(Le),s=new Set;for(let r of a)s.has(r.id)&&chunkS3BXVHO7_cjs.b("config-invalid",`duplicate column id "${r.id}"`,"give every column a unique id"),s.add(r.id);let m={},u=t.classesOfService??{};B(u)||chunkS3BXVHO7_cjs.b("config-invalid","kanban config.classesOfService must be an object keyed by class",'e.g. { "expedite": { "wipBypass": 1 } }');for(let[r,c]of Object.entries(u)){D.includes(r)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.classesOfService.${r} is not a class of service`,`use one of: ${D.join(", ")}`),B(c)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.classesOfService.${r} must be an object`,'e.g. { "wipBypass": 1, "sle": { "percentile": 85, "targetMs": 604800000 } }');let d={},f=_(c.wipBypass,`classesOfService.${r}.wipBypass`,"omit it for the default 1",0);if(f!==void 0&&(d.wipBypass=f),c.sle!==void 0){let y=c.sle;(!B(y)||typeof y.percentile!="number"||y.percentile<0||y.percentile>100)&&chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.classesOfService.${r}.sle.percentile must be 0..100`,'e.g. { "percentile": 85, "targetMs": 604800000 }');let k=_(y.targetMs,`classesOfService.${r}.sle.targetMs`,"state the service level target in milliseconds");k===void 0&&chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.classesOfService.${r}.sle.targetMs is required`,"state the service level target in milliseconds"),d.sle={percentile:y.percentile,targetMs:k};}m[r]=d;}let g=t.fairness??{};(!B(g)||g.aging!==void 0&&typeof g.aging!="boolean")&&chunkS3BXVHO7_cjs.b("config-invalid","kanban config.fairness must be { aging: boolean, maxDeferralRounds?: number }","omit it for aging on");let l=_(g.maxDeferralRounds,"fairness.maxDeferralRounds","omit it for 5 rounds"),i={aging:g.aging===void 0?true:g.aging,maxDeferralRounds:l??5},o={},n=t.cadences??{};B(n)||chunkS3BXVHO7_cjs.b("config-invalid","kanban config.cadences must be an object keyed by cadence",'e.g. { "replenishment": { "everyTurns": 10 } }');for(let[r,c]of Object.entries(n)){U.includes(r)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.cadences.${r} is not a cadence`,`use one of: ${U.join(", ")}`),B(c)||chunkS3BXVHO7_cjs.b("config-invalid",`kanban config.cadences.${r} must be an object`,'e.g. { "everyTurns": 10 }');let d=_(c.everyTurns,`cadences.${r}.everyTurns`,"omit it when the cadence is run by hand");o[r]=d===void 0?{}:{everyTurns:d};}return {columns:a,classesOfService:m,fairness:i,cadences:o,throughputWindowMs:_(t.throughputWindowMs,"throughputWindowMs","omit it for one week")??7*ee,maxExpedite:_(t.maxExpedite,"maxExpedite","omit it for 1")??1}}function te(e,t){return e.events.find(s=>s.type==="item.added"&&s.payload.id===t)?.at??e.events[0].at}function ne(e,t,a){return Date.parse(a)-Date.parse(N(t)??te(e,t.id))}function Be(e,t){return e.stages.map(a=>{let s=Object.values(e.items).filter(l=>l.stage===a.id&&l.status!=="dropped"),m=exsorted.timSort(s.map(l=>({id:l.id,title:l.title,type:l.type,status:l.status,classOfService:l.classOfService??"standard",role:l.role??null,ageMs:ne(e,l,t),blocked:l.blockers.some(i=>!i.clearedAt),deadline:l.deadline??null})),(l,i)=>i.ageMs-l.ageMs||(l.id<i.id?-1:1)),u=m.filter(l=>M.has(l.status)).length,g=m.filter(l=>l.status==="todo"||l.status==="review").length;return {id:a.id,name:a.name,wipLimit:a.wipLimit??null,active:u,queued:g,overLimit:a.wipLimit!==void 0&&u>a.wipLimit,commitmentPoint:a.commitmentPoint===true,deliveryPoint:a.deliveryPoint===true,cards:m}})}var Re=e=>`cadence.${e}`;function ce(e){let t=x(e);return U.map(a=>{let s=0,m=null;for(let l of e.events)l.type===Re(a)&&(s=l.seq,m=l.at);let u=e.events.filter(l=>l.type==="item.completed"&&l.seq>s).length,g=t.cadences[a]?.everyTurns??null;return {cadence:a,everyTurns:g,lastAt:m,turnsSince:u,due:g!==null&&u>=g}})}var $e=(e,t)=>Date.parse(t)-Date.parse(e),Me=e=>e.transition??null;function ie(e,t,a){let s=new Map,m=[],u=e[0]?.at??"1970-01-01T00:00:00.000Z",g=(i,o,n)=>{o===t&&i.committedAt===void 0&&(i.committedAt=n),o===a&&i.deliveredAt===void 0&&(i.deliveredAt=n);},l=(i,o,n)=>{i.since.status==="in_progress"&&(i.activeMs+=$e(i.since.at,n)),i.since.status==="blocked"&&(i.blockedMs+=$e(i.since.at,n)),i.since={status:o.status,at:n},o.stage!==i.stage&&(i.stays[i.stays.length-1].leftAt=n,i.stays.push({stage:o.stage,enteredAt:n}),g(i,o.stage,n)),i.stage=o.stage,i.status=o.status;};for(let i of e){u=i.at;let o=i.payload??{};switch(i.type){case "item.added":{let n=o.id,r=o.stage,c={id:n,type:typeof o.type=="string"?o.type:"task",stage:r,status:"todo",stays:[{stage:r,enteredAt:i.at}],classOfService:"standard",activeMs:0,blockedMs:0,since:{status:"todo",at:i.at}};g(c,r,i.at),s.set(n,c);break}case "item.transition":{let n=s.get(o.id);if(!n)break;l(n,o.to,i.at);break}case "cadence.replenishment":{let n=o.items??[];for(let r of n){let c=s.get(r.id);c&&(c.classOfService=r.classOfService,r.deadline?c.deadline=r.deadline:delete c.deadline);}break}case "blocker.raised":{m.push({item:o.id,blocker:o.blocker,raisedAt:i.at});let n=s.get(o.id),r=Me(o);n&&r&&l(n,r.to,i.at);break}case "blocker.cleared":{let n=m.find(d=>d.item===o.id&&d.blocker===o.blocker&&d.clearedAt===void 0);n&&(n.clearedAt=i.at);let r=s.get(o.id),c=Me(o);r&&c&&l(r,c.to,i.at);break}}}return {items:s,blockers:m,lastAt:u}}function q(e,t){if(e.length===0)return 0;let a=exsorted.timSort([...e],(m,u)=>m-u),s=Math.min(a.length,Math.max(1,Math.ceil(t/100*a.length)));return a[s-1]}var H=(e,t)=>Date.parse(t)-Date.parse(e),Pe=(e,t)=>t===0?0:e/t;function Ee(e){let t=e.stages.find(s=>s.commitmentPoint)?.id??"",a=e.stages.find(s=>s.deliveryPoint)?.id??"";return {commitment:t,delivery:a,ci:e.stages.findIndex(s=>s.id===t),di:e.stages.findIndex(s=>s.id===a)}}function De(e){let t={};for(let a of e.items.values())for(let s of a.stays)s.leftAt!==void 0&&(t[s.stage]??=[]).push(H(s.enteredAt,s.leftAt));return t}function de(e){let t=Ee(e);return De(ie(e.events,t.commitment,t.delivery))}function G(e){let t=x(e),a=Ee(e),s=ie(e.events,a.commitment,a.delivery),m=[...s.items.values()],u=e.stages.map(p=>p.id),g=new Map(u.map((p,b)=>[p,b])),l=exsorted.timSort(m.filter(p=>p.type!=="question"&&p.committedAt!==void 0&&p.deliveredAt!==void 0),(p,b)=>Date.parse(p.deliveredAt)-Date.parse(b.deliveredAt)),i=l.map(p=>H(p.committedAt,p.deliveredAt)),o={};for(let[p,b]of Object.entries(De(s)))o[p]=b.reduce((h,S)=>h+S,0)/b.length;let n=m.filter(p=>p.type!=="question"&&p.committedAt!==void 0).map(p=>Date.parse(p.committedAt)),r=[];if(n.length>0){let p=n.reduce((h,S)=>S<h?S:h,n[0]),b=Math.floor((Date.parse(s.lastAt)-p)/t.throughputWindowMs)+1;r=Array.from({length:b},()=>0);for(let h of l){let S=Math.floor((Date.parse(h.deliveredAt)-p)/t.throughputWindowMs);S>=0&&S<b&&(r[S]+=1);}}let c=Object.fromEntries(u.map(p=>[p,0])),d={};for(let p of m){if(!P.has(p.status))continue;Object.hasOwn(c,p.stage)&&(c[p.stage]+=1);let b=g.get(p.stage);b!==void 0&&b>=a.ci&&b<a.di&&(d[p.id]=H(p.stays[p.stays.length-1].enteredAt,s.lastAt));}let f=new Map;for(let p of m)for(let b of p.stays){let h=f.get(b.stage)??new Map;h.has(p.id)||h.set(p.id,b.enteredAt),f.set(b.stage,h);}let y=exsorted.timSort([...new Set(e.events.map(p=>p.at.slice(0,10)))],(p,b)=>p<b?-1:p>b?1:0),k={};for(let p of y)for(let b of u)k[`${p}:${b}`]=[...f.get(b)?.values()??[]].filter(h=>h.slice(0,10)<=p).length;let w={};for(let[p,b]of Object.entries(t.classesOfService)){if(!b?.sle)continue;let h=l.filter(S=>S.classOfService===p);h.length!==0&&(w[p]=h.filter(S=>H(S.committedAt,S.deliveredAt)<=b.sle.targetMs).length/h.length);}let I=s.blockers.reduce((p,b)=>p+H(b.raisedAt,b.clearedAt??s.lastAt),0);return {leadTimeMs:i,leadTimeP50:q(i,50),leadTimeP85:q(i,85),cycleTimeMsByColumn:o,throughput:r,wipByColumn:c,flowEfficiency:Pe(l.reduce((p,b)=>p+b.activeMs,0),i.reduce((p,b)=>p+b,0)),agingWipMs:d,cumulativeFlow:k,sleCompliance:w,blockerTimeMs:I,meanBlockerTimeMs:Pe(I,s.blockers.length)}}function me(e,t){let a=Date.parse(t),s=de(e),m=[],u=[],g=Object.fromEntries(e.stages.map(i=>[i.id,0])),l=Object.fromEntries(e.stages.map(i=>[i.id,0]));for(let i of Object.values(e.items)){for(let o of i.blockers)o.clearedAt||m.push({item:i.id,blocker:o.id,owner:o.owner,reason:o.reason,ageMs:a-Date.parse(o.raisedAt)});if(M.has(i.status)?l[i.stage]=(l[i.stage]??0)+1:(i.status==="todo"||i.status==="review")&&(g[i.stage]=(g[i.stage]??0)+1),P.has(i.status)&&E(e,i)){let o=s[i.stage]??[],n=o.length?q(o,85):null,r=ne(e,i,t);u.push({item:i.id,column:i.stage,status:i.status,ageMs:r,p85Ms:n,overP85:n!==null&&r>n});}}return {blockers:m,agingWip:exsorted.timSort(u,(i,o)=>o.ageMs-i.ageMs||(i.item<o.item?-1:1)),queued:g,active:l}}function Ne(e,t){let a=K(e).id;return e.items[t].transitions.find(u=>u.to.stage===a&&u.from.stage!==a)?.at??te(e,t)}function fe(e,t,a){let s=Date.parse(t),m=x(e),u=G(e).leadTimeMs,g=m.classesOfService["fixed-date"]?.sle?.percentile??85,l=u.length?q(u,g):null,i=[],o=[];for(let n of Object.values(e.items)){for(let f of n.blockers){let y=f.clearedAt?0:s-Date.parse(f.raisedAt);!f.clearedAt&&y>a&&o.push({item:n.id,blocker:f.id,owner:f.owner,ageMs:y});}if(n.classOfService!=="fixed-date"||!n.deadline||!P.has(n.status)||!E(e,n))continue;let r=s-Date.parse(Ne(e,n.id)),c=l===null?null:Math.max(0,l-r),d=Date.parse(n.deadline)-s;i.push({item:n.id,deadline:n.deadline,timeLeftMs:d,projectedRemainingMs:c,atRisk:d<0||c!==null&&c>d});}return {fixedDate:i,staleBlockers:o,expediteActive:J(e)}}var V=e=>typeof e=="object"&&e!==null&&!Array.isArray(e);function ue(e,t,a){let s=t.items;(!Array.isArray(s)||s.length===0||!s.every(u=>typeof u=="string"))&&chunkS3BXVHO7_cjs.b("invalid-input",`${a} needs items: string[]`,'pass ["A", "B"]');let m=s.find((u,g)=>s.indexOf(u)!==g);m!==void 0&&chunkS3BXVHO7_cjs.b("invalid-input",`item "${m}" appears twice in items[]`,"list each item once",{items:[m]});for(let u of s)chunkZTE7LREB_cjs.u(e,u);return s}function _e(e){return V(e)&&typeof e.percentile=="number"&&e.percentile>=0&&e.percentile<=100&&typeof e.targetMs=="number"&&Number.isFinite(e.targetMs)&&e.targetMs>0}function Ie(e,t){let{wipLimit:a,...s}=e;return t===null?s:{...s,wipLimit:t}}function pe(e){let t=(i,o,n,r)=>chunkZTE7LREB_cjs.W(i,e(),{type:o,by:n.by,payload:n},c=>r(c,chunkZTE7LREB_cjs.a(n.now)));return {replenishment:(i,o)=>{chunkZTE7LREB_cjs.o(i,o.by,"order-backlog","manage-flow");let n=ue(i,o,"replenishment"),r=K(i),c=oe(i);for(let d of n)chunkZTE7LREB_cjs.E(i,chunkZTE7LREB_cjs.u(i,d),c.exitPolicy,`leave "${c.id}"`);return t(i,"ceremony.replenishment",o,(d,f)=>{for(let y of n)d=chunkZTE7LREB_cjs.x(d,y,{stage:r.id,status:"todo"},o.by,f);return chunkZTE7LREB_cjs.c(d,"cadence.replenishment",o.by,{column:r.id,items:n.map(y=>{let k=chunkZTE7LREB_cjs.u(d,y);return {id:y,classOfService:k.classOfService??"standard",deadline:k.deadline??null}})},f)})},"delivery-planning":(i,o)=>{chunkZTE7LREB_cjs.o(i,o.by,"facilitate","manage-flow");let n=ue(i,o,"delivery-planning"),r=X(i),c=i.stages[i.stages.findIndex(d=>d.id===r.id)-1];for(let d of n){let f=chunkZTE7LREB_cjs.u(i,d);f.stage!==c.id&&f.stage!==r.id&&chunkS3BXVHO7_cjs.b("invalid-input",`item "${d}" is in "${f.stage}", not ready for delivery`,`only cards in "${c.id}" or "${r.id}" can be planned for delivery`,{items:[d]});}return t(i,"ceremony.delivery-planning",o,(d,f)=>chunkZTE7LREB_cjs.c(d,"cadence.delivery-planning",o.by,{items:n,window:typeof o.window=="string"?o.window:null,delivered:n.filter(y=>chunkZTE7LREB_cjs.u(d,y).status==="done")},f))},"service-delivery-review":(i,o)=>{chunkZTE7LREB_cjs.o(i,o.by,"manage-flow");let n=o.wip??{},r=o.policies??{},c=o.sle??{};(!V(n)||!V(r)||!V(c))&&chunkS3BXVHO7_cjs.b("invalid-input","service-delivery-review takes wip, policies and sle objects keyed by column or class",'e.g. { "wip": { "dev": 2 }, "policies": { "dev": [{ "kind": "no-open-blockers" }] }, "sle": { "standard": { "percentile": 85, "targetMs": 604800000 } } }');for(let[f,y]of Object.entries(n))chunkZTE7LREB_cjs.p(i,f),y!==null&&(typeof y!="number"||!Number.isFinite(y)||y<1)&&chunkS3BXVHO7_cjs.b("invalid-input",`wip.${f} must be a positive number or null`,"null removes the limit");let d={};for(let[f,y]of Object.entries(r))chunkZTE7LREB_cjs.p(i,f),d[f]=ae(y,`policies.${f}`);for(let[f,y]of Object.entries(c))D.includes(f)||chunkS3BXVHO7_cjs.b("invalid-input",`sle.${f} is not a class of service`,`use one of: ${D.join(", ")}`),y!==null&&!_e(y)&&chunkS3BXVHO7_cjs.b("invalid-input",`sle.${f} must be { percentile: 0..100, targetMs: > 0 } or null`,"null removes the service level");return t(i,"ceremony.service-delivery-review",o,(f,y)=>{let k=x(f),w=f.stages,I=k.columns,p={...k.classesOfService},b=[],h=[],S=[];for(let[O,A]of Object.entries(n)){let W=w.find(R=>R.id===O).wipLimit??null;W!==A&&(b.push({column:O,before:W,after:A}),w=w.map(R=>R.id===O?Ie(R,A):R),I=I.map(R=>R.id===O?Ie(R,A):R));}for(let[O,A]of Object.entries(d)){let W=w.find($=>$.id===O).exitPolicy;if(JSON.stringify(W)===JSON.stringify(A))continue;h.push({column:O,before:W,after:A});let R=w.findIndex($=>$.id===O),z=w[R].commitmentPoint?w[R+1]?.id:void 0;w=w.map($=>$.id===O?{...$,exitPolicy:A}:$.id===z?{...$,entryPolicy:A}:$),I=I.map($=>$.id===O?{...$,policies:A}:$);}for(let[O,A]of Object.entries(c)){let W=p[O]?.sle??null;if(JSON.stringify(W)===JSON.stringify(A))continue;S.push({classOfService:O,before:W,after:A});let{sle:R,...z}=p[O]??{};p={...p,[O]:A===null?z:{...z,sle:A}};}return f={...f,stages:w,config:{...k,columns:I,classesOfService:p}},chunkZTE7LREB_cjs.c(f,"cadence.service-delivery-review",o.by,{wip:b,policies:h,sle:S},y)})},"operations-review":(i,o)=>{chunkZTE7LREB_cjs.o(i,o.by,"facilitate","manage-flow");let n=o.assign??[];(!Array.isArray(n)||!n.every(r=>V(r)&&typeof r.item=="string"&&typeof r.blocker=="string"))&&chunkS3BXVHO7_cjs.b("invalid-input","operations-review.assign must be [{ item, blocker, owner }]",'e.g. { "assign": [{ "item": "A", "blocker": "A-b1", "owner": "service-delivery-manager" }] }');for(let r of n){let c=chunkZTE7LREB_cjs.u(i,r.item);c.blockers.some(d=>d.id===r.blocker&&!d.clearedAt)||chunkS3BXVHO7_cjs.b("invalid-input",`item "${r.item}" has no open blocker "${r.blocker}"`,`open blockers: ${c.blockers.filter(d=>!d.clearedAt).map(d=>d.id).join(", ")||"(none)"}`,{items:[r.item]});}return t(i,"ceremony.operations-review",o,(r,c)=>{for(let d of n){let f=chunkZTE7LREB_cjs.u(r,d.item).blockers.find(y=>y.id===d.blocker).owner;r=chunkZTE7LREB_cjs.s(r,d.item,y=>({...y,blockers:y.blockers.map(k=>k.id===d.blocker?{...k,owner:d.owner}:k)})),r=chunkZTE7LREB_cjs.c(r,"blocker.reassigned",o.by,{id:d.item,blocker:d.blocker,from:f,to:d.owner},c);}return chunkZTE7LREB_cjs.c(r,"cadence.operations-review",o.by,me(r,c),c)})},"risk-review":(i,o)=>{chunkZTE7LREB_cjs.o(i,o.by,"facilitate","manage-flow");let n=o.staleAfterMs??7*ee;return (typeof n!="number"||!Number.isFinite(n)||n<0)&&chunkS3BXVHO7_cjs.b("invalid-input","risk-review.staleAfterMs must be a non-negative number of milliseconds","omit it for one week"),t(i,"ceremony.risk-review",o,(r,c)=>chunkZTE7LREB_cjs.c(r,"cadence.risk-review",o.by,fe(r,c,n),c))},"strategy-review":(i,o)=>{chunkZTE7LREB_cjs.o(i,o.by,"manage-flow");let n=x(i),r=o.classesOfService??{};V(r)||chunkS3BXVHO7_cjs.b("invalid-input","strategy-review.classesOfService must be an object keyed by class",'e.g. { "expedite": { "wipBypass": 2 }, "intangible": null }');let c={...n.classesOfService};for(let[f,y]of Object.entries(r))D.includes(f)||chunkS3BXVHO7_cjs.b("invalid-input",`classesOfService.${f} is not a class of service`,`use one of: ${D.join(", ")}`),y===null?delete c[f]:c[f]=y;let d=Y({...n,classesOfService:c,maxExpedite:o.maxExpedite??n.maxExpedite});return t(i,"ceremony.strategy-review",o,(f,y)=>{let k=Object.fromEntries(D.map(w=>[w,0]));for(let w of Object.values(f.items))P.has(w.status)&&E(f,w)&&(k[w.classOfService??"standard"]+=1);return f={...f,config:{...x(f),classesOfService:d.classesOfService,maxExpedite:d.maxExpedite}},chunkZTE7LREB_cjs.c(f,"cadence.strategy-review",o.by,{mix:k,before:{classesOfService:n.classesOfService,maxExpedite:n.maxExpedite},after:{classesOfService:d.classesOfService,maxExpedite:d.maxExpedite}},y)})}}}function ge(){return [{id:"service-request-manager",name:"Service Request Manager",modelTier:"large",capabilities:["order-backlog","define-acceptance","manage-flow","escalate"],canApprove:[],inputs:["stakeholder-requests","options"],outputs:["ordered-options","acceptance-criteria","classes-of-service"],do_not:["do not implement items yourself","do not push work past the commitment point outside replenishment"]},{id:"service-delivery-manager",name:"Service Delivery Manager",modelTier:"large",capabilities:["facilitate","manage-flow","escalate"],canApprove:[],inputs:["board","flow-metrics","blockers"],outputs:["cadence-records","wip-and-policy-changes","lessons"],do_not:["do not reorder the options","do not assign work; the planner pulls"]},{id:"worker",name:"Worker",modelTier:"small",capabilities:["deliver","review","verify"],canApprove:[],inputs:["brief"],outputs:["code","test-output"],do_not:["do not change acceptance criteria","do not pull more work while your card is open"]}]}var C=(e,t,a,s,m)=>m?{rule:e,severity:t,item:m,message:a,fix:s}:{rule:e,severity:t,message:a,fix:s},Z=e=>e.payload??{},re=e=>{let t=Z(e).items;return Array.isArray(t)?t:[]},qe=(e,t)=>Object.values(e.items).filter(a=>a.stage===t&&M.has(a.status)).length;function ye(){return [{id:"policy-explicit",severity:"error",check(l,i){let o=l.stages.find(n=>n.deliveryPoint)?.id;if(i.type==="init")return l.stages.filter(n=>n.id!==o&&n.exitPolicy.length===0).map(n=>C("policy-explicit","error",`column "${n.id}" has no explicit policy`,`give column "${n.id}" at least one policy, e.g. { "kind": "acceptance-written" }`));if(i.type==="ceremony.service-delivery-review"){let n=Z(i).policies??{};return Object.entries(n).filter(([r,c])=>r!==o&&Array.isArray(c)&&c.length===0).map(([r])=>C("policy-explicit","error",`column "${r}" would be left without a policy`,"keep at least one policy per column; Kanban policies are explicit"))}return []}},{id:"commitment-point-once",severity:"error",check(l,i){if(i.type!=="init")return [];let o=[],n=l.stages.filter(c=>c.commitmentPoint),r=l.stages.filter(c=>c.deliveryPoint);if(n.length!==1&&o.push(C("commitment-point-once","error",`${n.length} columns are marked commitmentPoint (${n.map(c=>c.id).join(", ")||"none"})`,"mark exactly one column commitmentPoint: true")),r.length!==1&&o.push(C("commitment-point-once","error",`${r.length} columns are marked deliveryPoint (${r.map(c=>c.id).join(", ")||"none"})`,"mark exactly one column deliveryPoint: true")),n.length===1&&r.length===1){let c=l.stages.indexOf(n[0]),d=l.stages.indexOf(r[0]);d!==l.stages.length-1&&o.push(C("commitment-point-once","error",`delivery column "${r[0].id}" is not the last column`,"move the delivery column to the end; finished cards leave the board there")),c===0?o.push(C("commitment-point-once","error",`commitment column "${n[0].id}" is the first column`,"keep an Options column before the commitment point; replenishment pulls from it")):c>=d&&o.push(C("commitment-point-once","error",`commitment column "${n[0].id}" comes after the delivery column`,"order columns options \u2192 commitment \u2192 \u2026 \u2192 delivery"));}return o}},{id:"wip-respected",severity:"error",check(l,i){if(i.type==="ceremony.replenishment"){let o=l.stages.find(f=>f.commitmentPoint);if(!o||o.wipLimit===void 0)return [];let n=re(i),r=Object.values(l.items).filter(f=>f.stage===o.id&&P.has(f.status)).length,c=n.some(f=>l.items[f]?.classOfService==="expedite")?x(l).classesOfService.expedite?.wipBypass??1:0,d=o.wipLimit+c;return r+n.length<=d?[]:[C("wip-respected","error",`column "${o.id}" holds ${r} card(s); pulling ${n.length} more exceeds its WIP limit of ${o.wipLimit}${c?` (+${c} expedite bypass)`:""}`,`pull at most ${Math.max(0,d-r)} card(s) now, or raise the limit in a service-delivery-review`)]}if(i.type==="ceremony.service-delivery-review"){let o=Z(i).wip??{},n=[];for(let[r,c]of Object.entries(o)){if(typeof c!="number")continue;let d=qe(l,r);d>c&&n.push(C("wip-respected","warning",`column "${r}" has ${d} active card(s), above the new limit of ${c}`,"nothing new is pulled into it until it drains; finish or move cards"));}return n}return []}},{id:"pull-not-push",severity:"error",check(l,i){if(i.type!=="ceremony.replenishment")return [];let o=l.stages[0],n=l.stages.find(d=>d.commitmentPoint);if(!o||!n)return [];let r=[],c=Z(i).column;c!==void 0&&c!==n.id&&r.push(C("pull-not-push","error",`replenishment may only pull into the commitment column "${n.id}", not "${String(c)}"`,"drop column or set it to the commitment column; later columns are pulled by the planner"));for(let d of re(i)){let f=l.items[d];f&&(f.stage!==o.id||f.status!=="todo")&&r.push(C("pull-not-push","error",`item "${d}" is ${f.status} in "${f.stage}", not an option`,`only todo cards in "${o.id}" can be pulled; cards already on the board flow by pull`,d));}return r}},{id:"blocker-has-owner",severity:"error",check(l,i){if(!i.type.startsWith("ceremony."))return [];let o=[];if(i.type==="ceremony.operations-review"){let n=Z(i).assign??[];for(let r of n)(typeof r.owner!="string"||r.owner.trim()==="")&&o.push(C("blocker-has-owner","error",`blocker "${r.blocker}" on "${r.item}" would have no owner`,"name the role or person who clears it",r.item));}for(let n of Object.values(l.items))for(let r of n.blockers)!r.clearedAt&&r.owner.trim()===""&&o.push(C("blocker-has-owner","warning",`blocker "${r.id}" on "${n.id}" has no owner`,`assign one in operations-review: { "assign": [{ "item": "${n.id}", "blocker": "${r.id}", "owner": "<who>" }] }`,n.id));return o}},{id:"one-expedite",severity:"error",check(l,i){if(i.type!=="ceremony.replenishment")return [];let o=re(i).filter(c=>l.items[c]?.classOfService==="expedite");if(o.length===0)return [];let n=Object.values(l.items).filter(c=>c.classOfService==="expedite"&&P.has(c.status)&&E(l,c)).map(c=>c.id),r=x(l).maxExpedite;return n.length+o.length<=r?[]:[C("one-expedite","error",`${n.length} expedite card(s) already on the board (${n.join(", ")||"none"}); pulling ${o.join(", ")} exceeds maxExpedite ${r}`,"deliver the current expedite first, reclassify the new card, or raise maxExpedite in a strategy-review")]}},{id:"fixed-date-needs-deadline",severity:"error",check(l,i){return i.type!=="ceremony.replenishment"?[]:re(i).filter(o=>{let n=l.items[o];return n?.classOfService==="fixed-date"&&!(n.deadline&&Number.isFinite(Date.parse(n.deadline)))}).map(o=>C("fixed-date-needs-deadline","error",`fixed-date item "${o}" has no deadline`,`editItem("${o}", { deadline: "<ISO date>" }) before pulling it`,o))}}]}function be(e){let t=e.columns.map(m=>{let u={id:m.id,name:m.name,entryPolicy:[],exitPolicy:m.policies};return m.wipLimit!==void 0&&(u.wipLimit=m.wipLimit),m.commitmentPoint&&(u.commitmentPoint=true),m.deliveryPoint&&(u.deliveryPoint=true),m.review&&(u.review=true),u}),a=t.findIndex(m=>m.commitmentPoint),s=a>=0?t[a+1]:void 0;return s&&(s.entryPolicy=t[a].exitPolicy),t}var Fe={backlog:"Options",item:"Work Item",stage:"Column",iteration:"Cadence",increment:"Delivery",ceremony:"Cadence",gate:"Policy"},Je={expedite:0,"fixed-date":1,standard:2,intangible:3},We=e=>Je[e.classOfService??"standard"]??2,ve=1e15,Ue=4e9,Ye=(e,t,a)=>Math.min(a,Math.max(t,e));function He(e,t){let a=e.classOfService??"standard";if(e.type==="question")return 5*ve;if(a==="fixed-date"&&e.deadline!==void 0){let l=Date.parse(e.deadline);if(Number.isFinite(l))return 3*ve+1e10-Math.floor(l/1e3)}let s=t.aging?Math.floor(t.deferrals/t.maxDeferralRounds):0,m=Ye(e.priority??0,-1e9,1e9)+s*Ue,u=t.aging&&t.deferrals>=t.maxDeferralRounds;return (a==="expedite"?4:a==="fixed-date"?3:a==="intangible"&&!u?1:2)*ve+m}function je(e,t){let a=We(e)-We(t);if(a!==0)return a;let s=N(e),m=N(t),u=s===void 0?0:Date.parse(s),g=m===void 0?0:Date.parse(m);return u!==g?u-g:e.id<t.id?-1:e.id>t.id?1:0}function Ge(e){return e.map(t=>t.kind==="evidence"?`evidence ${t.evidenceKind}`:t.kind==="custom"?`${t.id}: ${t.description}`:t.kind).join(", ")||"(none)"}function Ze(){return {name:"kanban",vocabulary:Fe,validateConfig:Y,defaultStages:be,defaultRoles:ge,requiredCapabilities:["manage-flow","deliver"],exclusiveCapabilities:[],plannerOptions(e){let t=x(e),a=K(e),s=e.stages[e.stages.findIndex(n=>n.id===a.id)+1],m=t.classesOfService.expedite?.wipBypass??1,u=[],g=[0],l=new Map;for(let n of e.events)u.push(Date.parse(n.at)),g.push(g[g.length-1]+(n.type==="planner.resolved"?1:0)),n.type==="item.added"&&l.set(n.payload.id,n.at);let i=n=>{let r=0,c=u.length;for(;r<c;){let d=r+c>>1;u[d]<=n?r=d+1:c=d;}return g[u.length]-g[r]},o={schedulerMode:"throughput",fairnessPolicy:t.fairness.aging?"aging":"none",deadlineStrategy:"earliest-first",dispatchStage:n=>n.stage===a.id&&s?s.id:void 0,priorityOf:n=>{let r=Date.parse(N(n)??l.get(n.id)??"");return He(n,{deferrals:i(Number.isFinite(r)?r:0),aging:t.fairness.aging,maxDeferralRounds:t.fairness.maxDeferralRounds})},tieBreaker:je,wipCounts:[...M],wipLimitOf:(n,r)=>n.wipLimit===void 0?void 0:r.classOfService==="expedite"?n.wipLimit+m:n.wipLimit};return o.maxDeferralRounds=t.fairness.maxDeferralRounds,o},candidates(e){let t=Math.max(0,x(e).maxExpedite-J(e).length),a=Object.values(e.items).filter(m=>(m.status==="todo"||m.status==="review")&&E(e,m)),s=exsorted.timSort(a.filter(m=>m.classOfService==="expedite"),je).slice(0,t);return a.filter(m=>m.classOfService!=="expedite"||s.includes(m))},invariants(e,t){let a=x(e),s=e.stages.find(l=>l.id===t.stage),m=t.classOfService??"standard",u=a.classesOfService[m]?.sle,g=Object.values(e.items).filter(l=>l.stage===s.id&&M.has(l.status)).length;return [`Column "${s.name}" exit policies: ${Ge(s.exitPolicy)}`,`Class of service: ${m}${t.deadline?`; deadline ${t.deadline}`:""}${u?`; service level p${u.percentile} <= ${u.targetMs} ms`:""}`,`WIP limit of "${s.name}": ${s.wipLimit??"none"} (active now: ${g}); finish this card before pulling another`,`Do not change the scope of "${t.id}" or its acceptance criteria; raise open_questions instead`]},ceremonies:pe(Ze),rules:ye(),metrics:e=>({...G(e)}),humanDefaults:{gates:"none"},lockedConfigKeys:["columns","classesOfService"],report:e=>({cadencesDue:ce(e)})}}exports.A=ue;exports.B=pe;exports.C=ge;exports.D=ye;exports.E=be;exports.F=Fe;exports.G=He;exports.H=je;exports.I=Ze;exports.a=M;exports.b=P;exports.c=oe;exports.d=K;exports.e=X;exports.f=Q;exports.g=E;exports.h=J;exports.i=N;exports.j=U;exports.k=D;exports.l=ee;exports.m=x;exports.n=Ce;exports.o=Y;exports.p=te;exports.q=ne;exports.r=Be;exports.s=Re;exports.t=ce;exports.u=ie;exports.v=q;exports.w=de;exports.x=G;exports.y=me;exports.z=fe;//# sourceMappingURL=chunk-2OVYXXNV.cjs.map
2
+ //# sourceMappingURL=chunk-2OVYXXNV.cjs.map