openplanr 3.5.0 → 3.6.0

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 (217) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/cli/commands/backlog-commands.d.ts +3 -0
  3. package/dist/cli/commands/backlog-commands.d.ts.map +1 -0
  4. package/dist/cli/commands/backlog-commands.js +60 -0
  5. package/dist/cli/commands/backlog-commands.js.map +1 -0
  6. package/dist/cli/commands/company.d.ts.map +1 -1
  7. package/dist/cli/commands/company.js +17 -0
  8. package/dist/cli/commands/company.js.map +1 -1
  9. package/dist/cli/commands/planning-artifacts.d.ts.map +1 -1
  10. package/dist/cli/commands/planning-artifacts.js +13 -5
  11. package/dist/cli/commands/planning-artifacts.js.map +1 -1
  12. package/dist/dashboard/.vite/manifest.json +1 -1
  13. package/dist/dashboard/assets/{index-B-l_941W.js → index-C_OquZVk.js} +2 -2
  14. package/dist/dashboard/assets/{index-B-l_941W.js.map → index-C_OquZVk.js.map} +1 -1
  15. package/dist/dashboard/dashboard-manifest.json +8 -8
  16. package/dist/dashboard/index.html +1 -1
  17. package/dist/services/artifact-service.d.ts.map +1 -1
  18. package/dist/services/artifact-service.js +13 -6
  19. package/dist/services/artifact-service.js.map +1 -1
  20. package/dist/services/company-export-service.d.ts +41 -0
  21. package/dist/services/company-export-service.d.ts.map +1 -0
  22. package/dist/services/company-export-service.js +429 -0
  23. package/dist/services/company-export-service.js.map +1 -0
  24. package/dist/services/company-sync-service.d.ts +24 -1
  25. package/dist/services/company-sync-service.d.ts.map +1 -1
  26. package/dist/services/company-sync-service.js +36 -19
  27. package/dist/services/company-sync-service.js.map +1 -1
  28. package/docs/CLI.md +97 -1
  29. package/lib/backlog-item.d.mts +45 -1
  30. package/lib/backlog-item.mjs +302 -34
  31. package/lib/host-packages/adapter-registry.json +4 -4
  32. package/lib/host-packages/capability-map.json +1 -1
  33. package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
  34. package/lib/host-packages/claude/openplanr/.claude-plugin/icon.png +0 -0
  35. package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
  36. package/lib/host-packages/claude/openplanr/.openplanr-content.json +77 -77
  37. package/lib/host-packages/claude/openplanr/README.md +1 -1
  38. package/lib/host-packages/claude/openplanr/skills/ceo-review/references/note-checklist.md +131 -0
  39. package/lib/host-packages/claude/openplanr/skills/ceo-review/references/operate-advisor-contract.md +2 -7
  40. package/lib/host-packages/claude/openplanr/skills/chair-review/SKILL.md +2 -7
  41. package/lib/host-packages/claude/openplanr/skills/chair-review/references/note-checklist.md +131 -0
  42. package/lib/host-packages/claude/openplanr/skills/challenger-review/SKILL.md +3 -7
  43. package/lib/host-packages/claude/openplanr/skills/challenger-review/references/note-checklist.md +131 -0
  44. package/lib/host-packages/claude/openplanr/skills/cmo-review/references/note-checklist.md +131 -0
  45. package/lib/host-packages/claude/openplanr/skills/cmo-review/references/operate-advisor-contract.md +2 -7
  46. package/lib/host-packages/claude/openplanr/skills/coo-review/references/note-checklist.md +131 -0
  47. package/lib/host-packages/claude/openplanr/skills/coo-review/references/operate-advisor-contract.md +2 -7
  48. package/lib/host-packages/claude/openplanr/skills/cpo-review/references/note-checklist.md +131 -0
  49. package/lib/host-packages/claude/openplanr/skills/cpo-review/references/operate-advisor-contract.md +2 -7
  50. package/lib/host-packages/claude/openplanr/skills/cto-review/references/note-checklist.md +131 -0
  51. package/lib/host-packages/claude/openplanr/skills/cto-review/references/operate-advisor-contract.md +2 -7
  52. package/lib/host-packages/claude/openplanr/skills/investigate/SKILL.md +2 -3
  53. package/lib/host-packages/claude/openplanr/skills/investigate/references/backlog-filing.md +102 -0
  54. package/lib/host-packages/claude/openplanr/skills/investigate/references/findings.md +8 -16
  55. package/lib/host-packages/claude/openplanr/skills/operate/SKILL.md +73 -84
  56. package/lib/host-packages/claude/openplanr/skills/operate/references/note-checklist.md +131 -0
  57. package/lib/host-packages/claude/openplanr/skills/operate/references/return-contract.md +10 -0
  58. package/lib/host-packages/claude/openplanr/skills/plan/SKILL.md +16 -17
  59. package/lib/host-packages/claude/openplanr/skills/plan/references/planning-ids.md +53 -0
  60. package/lib/host-packages/claude/openplanr/skills/plan/references/return-contract.md +10 -0
  61. package/lib/host-packages/claude/openplanr/skills/plan-review/SKILL.md +6 -6
  62. package/lib/host-packages/claude/openplanr/skills/plan-review/references/return-contract.md +10 -0
  63. package/lib/host-packages/claude/openplanr/skills/release/SKILL.md +40 -139
  64. package/lib/host-packages/claude/openplanr/skills/release/references/cadence.md +10 -0
  65. package/lib/host-packages/claude/openplanr/skills/release/references/release-notes.md +1 -1
  66. package/lib/host-packages/claude/openplanr/skills/release/references/return-contract.md +10 -0
  67. package/lib/host-packages/claude/openplanr/skills/release/references/schemes.md +4 -2
  68. package/lib/host-packages/claude/openplanr/skills/ship/SKILL.md +37 -24
  69. package/lib/host-packages/claude/openplanr/skills/ship/references/backlog-filing.md +102 -0
  70. package/lib/host-packages/claude/openplanr/skills/ship/references/findings.md +8 -16
  71. package/lib/host-packages/claude/openplanr/skills/ship/references/return-contract.md +10 -0
  72. package/lib/host-packages/claude/openplanr/skills/spec/SKILL.md +9 -9
  73. package/lib/host-packages/claude/openplanr/skills/spec/references/return-contract.md +10 -0
  74. package/lib/host-packages/cursor/openplanr/.openplanr-content.json +74 -74
  75. package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
  76. package/lib/host-packages/cursor/openplanr/rules/planr-ceo-review/references/note-checklist.md +131 -0
  77. package/lib/host-packages/cursor/openplanr/rules/planr-ceo-review/references/operate-advisor-contract.md +2 -7
  78. package/lib/host-packages/cursor/openplanr/rules/planr-chair-review/references/note-checklist.md +131 -0
  79. package/lib/host-packages/cursor/openplanr/rules/planr-chair-review.mdc +2 -7
  80. package/lib/host-packages/cursor/openplanr/rules/planr-challenger-review/references/note-checklist.md +131 -0
  81. package/lib/host-packages/cursor/openplanr/rules/planr-challenger-review.mdc +3 -7
  82. package/lib/host-packages/cursor/openplanr/rules/planr-cmo-review/references/note-checklist.md +131 -0
  83. package/lib/host-packages/cursor/openplanr/rules/planr-cmo-review/references/operate-advisor-contract.md +2 -7
  84. package/lib/host-packages/cursor/openplanr/rules/planr-coo-review/references/note-checklist.md +131 -0
  85. package/lib/host-packages/cursor/openplanr/rules/planr-coo-review/references/operate-advisor-contract.md +2 -7
  86. package/lib/host-packages/cursor/openplanr/rules/planr-cpo-review/references/note-checklist.md +131 -0
  87. package/lib/host-packages/cursor/openplanr/rules/planr-cpo-review/references/operate-advisor-contract.md +2 -7
  88. package/lib/host-packages/cursor/openplanr/rules/planr-cto-review/references/note-checklist.md +131 -0
  89. package/lib/host-packages/cursor/openplanr/rules/planr-cto-review/references/operate-advisor-contract.md +2 -7
  90. package/lib/host-packages/cursor/openplanr/rules/planr-investigate/references/backlog-filing.md +102 -0
  91. package/lib/host-packages/cursor/openplanr/rules/planr-investigate/references/findings.md +8 -16
  92. package/lib/host-packages/cursor/openplanr/rules/planr-investigate.mdc +2 -3
  93. package/lib/host-packages/cursor/openplanr/rules/planr-operate/references/note-checklist.md +131 -0
  94. package/lib/host-packages/cursor/openplanr/rules/planr-operate/references/return-contract.md +10 -0
  95. package/lib/host-packages/cursor/openplanr/rules/planr-operate.mdc +73 -84
  96. package/lib/host-packages/cursor/openplanr/rules/planr-plan/references/planning-ids.md +53 -0
  97. package/lib/host-packages/cursor/openplanr/rules/planr-plan/references/return-contract.md +10 -0
  98. package/lib/host-packages/cursor/openplanr/rules/planr-plan-review/references/return-contract.md +10 -0
  99. package/lib/host-packages/cursor/openplanr/rules/planr-plan-review.mdc +6 -6
  100. package/lib/host-packages/cursor/openplanr/rules/planr-plan.mdc +16 -17
  101. package/lib/host-packages/cursor/openplanr/rules/planr-release/references/cadence.md +10 -0
  102. package/lib/host-packages/cursor/openplanr/rules/planr-release/references/release-notes.md +1 -1
  103. package/lib/host-packages/cursor/openplanr/rules/planr-release/references/return-contract.md +10 -0
  104. package/lib/host-packages/cursor/openplanr/rules/planr-release/references/schemes.md +4 -2
  105. package/lib/host-packages/cursor/openplanr/rules/planr-release.mdc +40 -139
  106. package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/backlog-filing.md +102 -0
  107. package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/findings.md +8 -16
  108. package/lib/host-packages/cursor/openplanr/rules/planr-ship/references/return-contract.md +10 -0
  109. package/lib/host-packages/cursor/openplanr/rules/planr-ship.mdc +37 -24
  110. package/lib/host-packages/cursor/openplanr/rules/planr-spec/references/return-contract.md +10 -0
  111. package/lib/host-packages/cursor/openplanr/rules/planr-spec.mdc +9 -9
  112. package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
  113. package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
  114. package/lib/host-packages/openai/openplanr/.openplanr-content.json +89 -89
  115. package/lib/host-packages/openai/openplanr/skills/ceo-review/openplanr.skill.json +4 -4
  116. package/lib/host-packages/openai/openplanr/skills/ceo-review/references/note-checklist.md +131 -0
  117. package/lib/host-packages/openai/openplanr/skills/ceo-review/references/operate-advisor-contract.md +2 -7
  118. package/lib/host-packages/openai/openplanr/skills/chair-review/SKILL.md +2 -7
  119. package/lib/host-packages/openai/openplanr/skills/chair-review/openplanr.skill.json +4 -4
  120. package/lib/host-packages/openai/openplanr/skills/chair-review/references/note-checklist.md +131 -0
  121. package/lib/host-packages/openai/openplanr/skills/challenger-review/SKILL.md +3 -7
  122. package/lib/host-packages/openai/openplanr/skills/challenger-review/openplanr.skill.json +4 -4
  123. package/lib/host-packages/openai/openplanr/skills/challenger-review/references/note-checklist.md +131 -0
  124. package/lib/host-packages/openai/openplanr/skills/cmo-review/openplanr.skill.json +4 -4
  125. package/lib/host-packages/openai/openplanr/skills/cmo-review/references/note-checklist.md +131 -0
  126. package/lib/host-packages/openai/openplanr/skills/cmo-review/references/operate-advisor-contract.md +2 -7
  127. package/lib/host-packages/openai/openplanr/skills/coo-review/openplanr.skill.json +4 -4
  128. package/lib/host-packages/openai/openplanr/skills/coo-review/references/note-checklist.md +131 -0
  129. package/lib/host-packages/openai/openplanr/skills/coo-review/references/operate-advisor-contract.md +2 -7
  130. package/lib/host-packages/openai/openplanr/skills/cpo-review/openplanr.skill.json +4 -4
  131. package/lib/host-packages/openai/openplanr/skills/cpo-review/references/note-checklist.md +131 -0
  132. package/lib/host-packages/openai/openplanr/skills/cpo-review/references/operate-advisor-contract.md +2 -7
  133. package/lib/host-packages/openai/openplanr/skills/cto-review/openplanr.skill.json +4 -4
  134. package/lib/host-packages/openai/openplanr/skills/cto-review/references/note-checklist.md +131 -0
  135. package/lib/host-packages/openai/openplanr/skills/cto-review/references/operate-advisor-contract.md +2 -7
  136. package/lib/host-packages/openai/openplanr/skills/investigate/SKILL.md +2 -3
  137. package/lib/host-packages/openai/openplanr/skills/investigate/openplanr.skill.json +4 -4
  138. package/lib/host-packages/openai/openplanr/skills/investigate/references/backlog-filing.md +102 -0
  139. package/lib/host-packages/openai/openplanr/skills/investigate/references/findings.md +8 -16
  140. package/lib/host-packages/openai/openplanr/skills/operate/SKILL.md +73 -84
  141. package/lib/host-packages/openai/openplanr/skills/operate/openplanr.skill.json +15 -4
  142. package/lib/host-packages/openai/openplanr/skills/operate/references/note-checklist.md +131 -0
  143. package/lib/host-packages/openai/openplanr/skills/operate/references/return-contract.md +10 -0
  144. package/lib/host-packages/openai/openplanr/skills/plan/SKILL.md +16 -17
  145. package/lib/host-packages/openai/openplanr/skills/plan/openplanr.skill.json +15 -4
  146. package/lib/host-packages/openai/openplanr/skills/plan/references/planning-ids.md +53 -0
  147. package/lib/host-packages/openai/openplanr/skills/plan/references/return-contract.md +10 -0
  148. package/lib/host-packages/openai/openplanr/skills/plan-review/SKILL.md +6 -6
  149. package/lib/host-packages/openai/openplanr/skills/plan-review/openplanr.skill.json +12 -1
  150. package/lib/host-packages/openai/openplanr/skills/plan-review/references/return-contract.md +10 -0
  151. package/lib/host-packages/openai/openplanr/skills/release/SKILL.md +40 -139
  152. package/lib/host-packages/openai/openplanr/skills/release/openplanr.skill.json +4 -4
  153. package/lib/host-packages/openai/openplanr/skills/release/references/cadence.md +10 -0
  154. package/lib/host-packages/openai/openplanr/skills/release/references/release-notes.md +1 -1
  155. package/lib/host-packages/openai/openplanr/skills/release/references/return-contract.md +10 -0
  156. package/lib/host-packages/openai/openplanr/skills/release/references/schemes.md +4 -2
  157. package/lib/host-packages/openai/openplanr/skills/ship/SKILL.md +37 -24
  158. package/lib/host-packages/openai/openplanr/skills/ship/openplanr.skill.json +9 -9
  159. package/lib/host-packages/openai/openplanr/skills/ship/references/backlog-filing.md +102 -0
  160. package/lib/host-packages/openai/openplanr/skills/ship/references/findings.md +8 -16
  161. package/lib/host-packages/openai/openplanr/skills/ship/references/return-contract.md +10 -0
  162. package/lib/host-packages/openai/openplanr/skills/spec/SKILL.md +9 -9
  163. package/lib/host-packages/openai/openplanr/skills/spec/openplanr.skill.json +12 -1
  164. package/lib/host-packages/openai/openplanr/skills/spec/references/return-contract.md +10 -0
  165. package/package.json +1 -1
  166. package/lib/host-packages/claude/openplanr/.claude-plugin/icon.svg +0 -11
  167. package/lib/host-packages/claude/openplanr/runtime/operate/scripts/errors.mjs +0 -8
  168. package/lib/host-packages/claude/openplanr/runtime/operate/scripts/operate-review-contract.mjs +0 -428
  169. package/lib/host-packages/claude/openplanr/runtime/operate/scripts/operate-review-note.mjs +0 -686
  170. package/lib/host-packages/claude/openplanr/runtime/operate/scripts/validate-note.mjs +0 -39
  171. package/lib/host-packages/claude/openplanr/skills/ceo-review/scripts/validate-note.mjs +0 -12
  172. package/lib/host-packages/claude/openplanr/skills/chair-review/scripts/validate-note.mjs +0 -12
  173. package/lib/host-packages/claude/openplanr/skills/challenger-review/scripts/validate-note.mjs +0 -12
  174. package/lib/host-packages/claude/openplanr/skills/cmo-review/scripts/validate-note.mjs +0 -12
  175. package/lib/host-packages/claude/openplanr/skills/coo-review/scripts/validate-note.mjs +0 -12
  176. package/lib/host-packages/claude/openplanr/skills/cpo-review/scripts/validate-note.mjs +0 -12
  177. package/lib/host-packages/claude/openplanr/skills/cto-review/scripts/validate-note.mjs +0 -12
  178. package/lib/host-packages/claude/openplanr/skills/investigate/scripts/backlog-item.mjs +0 -339
  179. package/lib/host-packages/claude/openplanr/skills/operate/scripts/validate-note.mjs +0 -12
  180. package/lib/host-packages/claude/openplanr/skills/plan/scripts/planning-ids.mjs +0 -166
  181. package/lib/host-packages/claude/openplanr/skills/release/scripts/collect_changes.sh +0 -85
  182. package/lib/host-packages/claude/openplanr/skills/ship/scripts/backlog-item.mjs +0 -339
  183. package/lib/host-packages/claude/openplanr/skills/ship/scripts/discover-verification.mjs +0 -108
  184. package/lib/host-packages/cursor/openplanr/rules/planr-ceo-review/scripts/validate-note.mjs +0 -12
  185. package/lib/host-packages/cursor/openplanr/rules/planr-chair-review/scripts/validate-note.mjs +0 -12
  186. package/lib/host-packages/cursor/openplanr/rules/planr-challenger-review/scripts/validate-note.mjs +0 -12
  187. package/lib/host-packages/cursor/openplanr/rules/planr-cmo-review/scripts/validate-note.mjs +0 -12
  188. package/lib/host-packages/cursor/openplanr/rules/planr-coo-review/scripts/validate-note.mjs +0 -12
  189. package/lib/host-packages/cursor/openplanr/rules/planr-cpo-review/scripts/validate-note.mjs +0 -12
  190. package/lib/host-packages/cursor/openplanr/rules/planr-cto-review/scripts/validate-note.mjs +0 -12
  191. package/lib/host-packages/cursor/openplanr/rules/planr-investigate/scripts/backlog-item.mjs +0 -339
  192. package/lib/host-packages/cursor/openplanr/rules/planr-operate/scripts/validate-note.mjs +0 -12
  193. package/lib/host-packages/cursor/openplanr/rules/planr-plan/scripts/planning-ids.mjs +0 -166
  194. package/lib/host-packages/cursor/openplanr/rules/planr-release/scripts/collect_changes.sh +0 -85
  195. package/lib/host-packages/cursor/openplanr/rules/planr-ship/scripts/backlog-item.mjs +0 -339
  196. package/lib/host-packages/cursor/openplanr/rules/planr-ship/scripts/discover-verification.mjs +0 -108
  197. package/lib/host-packages/cursor/openplanr/runtime/operate/scripts/errors.mjs +0 -8
  198. package/lib/host-packages/cursor/openplanr/runtime/operate/scripts/operate-review-contract.mjs +0 -428
  199. package/lib/host-packages/cursor/openplanr/runtime/operate/scripts/operate-review-note.mjs +0 -686
  200. package/lib/host-packages/cursor/openplanr/runtime/operate/scripts/validate-note.mjs +0 -39
  201. package/lib/host-packages/openai/openplanr/runtime/operate/scripts/errors.mjs +0 -8
  202. package/lib/host-packages/openai/openplanr/runtime/operate/scripts/operate-review-contract.mjs +0 -428
  203. package/lib/host-packages/openai/openplanr/runtime/operate/scripts/operate-review-note.mjs +0 -686
  204. package/lib/host-packages/openai/openplanr/runtime/operate/scripts/validate-note.mjs +0 -39
  205. package/lib/host-packages/openai/openplanr/skills/ceo-review/scripts/validate-note.mjs +0 -12
  206. package/lib/host-packages/openai/openplanr/skills/chair-review/scripts/validate-note.mjs +0 -12
  207. package/lib/host-packages/openai/openplanr/skills/challenger-review/scripts/validate-note.mjs +0 -12
  208. package/lib/host-packages/openai/openplanr/skills/cmo-review/scripts/validate-note.mjs +0 -12
  209. package/lib/host-packages/openai/openplanr/skills/coo-review/scripts/validate-note.mjs +0 -12
  210. package/lib/host-packages/openai/openplanr/skills/cpo-review/scripts/validate-note.mjs +0 -12
  211. package/lib/host-packages/openai/openplanr/skills/cto-review/scripts/validate-note.mjs +0 -12
  212. package/lib/host-packages/openai/openplanr/skills/investigate/scripts/backlog-item.mjs +0 -339
  213. package/lib/host-packages/openai/openplanr/skills/operate/scripts/validate-note.mjs +0 -12
  214. package/lib/host-packages/openai/openplanr/skills/plan/scripts/planning-ids.mjs +0 -166
  215. package/lib/host-packages/openai/openplanr/skills/release/scripts/collect_changes.sh +0 -85
  216. package/lib/host-packages/openai/openplanr/skills/ship/scripts/backlog-item.mjs +0 -339
  217. package/lib/host-packages/openai/openplanr/skills/ship/scripts/discover-verification.mjs +0 -108
@@ -84,6 +84,56 @@ class BacklogInputError extends Error {
84
84
  }
85
85
  }
86
86
 
87
+ const WHEN_PATTERN =
88
+ /^(?:(?:next-unit|trigger|declined):\S(?:[^\r\n]*\S)?|sprint:(?:next|SPRINT-\d{3,}))(?![\s\S])/u;
89
+ const FILE_PATTERN =
90
+ /^(?!\/)(?![A-Za-z]:)(?!.*\\)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*:\d+(?::\d+|-\d+)?$)[^\r\n]+$/u;
91
+
92
+ /** @type {typeof import('./backlog-item.d.mts').parseWhen} */
93
+ export function parseWhen(value) {
94
+ if (typeof value !== 'string' || !WHEN_PATTERN.test(value))
95
+ throw new BacklogInputError(
96
+ `Backlog when ${JSON.stringify(value)} is not valid; use next-unit:<ref>, sprint:<SPRINT-NNN or next>, trigger:<event>, or declined:<reason>. Do not store now.`,
97
+ );
98
+ const separator = value.indexOf(':');
99
+ return {
100
+ kind: /** @type {import('./backlog-item.d.mts').BacklogWhen['kind']} */ (
101
+ value.slice(0, separator)
102
+ ),
103
+ value: value.slice(separator + 1),
104
+ };
105
+ }
106
+
107
+ /** @param {unknown} value */
108
+ function normalizeFilePath(value) {
109
+ return typeof value === 'string'
110
+ ? value
111
+ .trim()
112
+ .replaceAll('\\', '/')
113
+ .replace(/^(?:\.\/)+/u, '')
114
+ : '';
115
+ }
116
+
117
+ /** @type {typeof import('./backlog-item.d.mts').normalizeBacklogFiles} */
118
+ export function normalizeBacklogFiles(value) {
119
+ if (!Array.isArray(value))
120
+ throw new BacklogInputError(
121
+ `Backlog files ${JSON.stringify(value)} must be an array of repo-relative paths.`,
122
+ );
123
+ return [
124
+ ...new Set(
125
+ value.map((item) => {
126
+ const normalized = normalizeFilePath(item);
127
+ if (!FILE_PATTERN.test(normalized))
128
+ throw new BacklogInputError(
129
+ `Backlog files entry ${JSON.stringify(item)} must be a repo-relative path without .. segments, absolute paths, or line numbers.`,
130
+ );
131
+ return normalized;
132
+ }),
133
+ ),
134
+ ];
135
+ }
136
+
87
137
  /** @type {typeof import('./backlog-item.d.mts').backlogFields} */
88
138
  export function backlogFields(input) {
89
139
  const title = String(input.title ?? input.name ?? '').trim();
@@ -96,6 +146,12 @@ export function backlogFields(input) {
96
146
  const criteria = optionalText(input.acceptanceCriteria);
97
147
  const notes = optionalText(input.notes);
98
148
  const epicId = optionalText(input.epicId);
149
+ if (input.when !== undefined) parseWhen(input.when);
150
+ const files = input.files === undefined ? undefined : normalizeBacklogFiles(input.files);
151
+ if (input.unverified !== undefined && input.unverified !== true)
152
+ throw new BacklogInputError(
153
+ `Backlog unverified ${JSON.stringify(input.unverified)} must be true when present.`,
154
+ );
99
155
  return {
100
156
  title,
101
157
  priority,
@@ -105,6 +161,9 @@ export function backlogFields(input) {
105
161
  ? {}
106
162
  : { acceptanceCriteria: Array.isArray(criteria) ? criteria.map(String) : String(criteria) }),
107
163
  ...(notes === undefined ? {} : { notes: String(notes) }),
164
+ ...(input.when === undefined ? {} : { when: String(input.when) }),
165
+ ...(files === undefined ? {} : { files }),
166
+ ...(input.unverified === undefined ? {} : { unverified: /** @type {const} */ (true) }),
108
167
  ...(epicId === undefined ? {} : { epicId: String(epicId) }),
109
168
  };
110
169
  }
@@ -121,6 +180,11 @@ export function renderBacklogItem(id, fields, date) {
121
180
  'status: "open"',
122
181
  `created: ${quote(date)}`,
123
182
  `updated: ${quote(date)}`,
183
+ ...(fields.when === undefined ? [] : [`when: ${quote(fields.when)}`]),
184
+ ...(fields.files === undefined
185
+ ? []
186
+ : [`files: [${fields.files.map((file) => quote(file)).join(', ')}]`]),
187
+ ...(fields.unverified === undefined ? [] : ['unverified: true']),
124
188
  ...(fields.epicId ? [`epicId: ${quote(fields.epicId)}`] : []),
125
189
  '---',
126
190
  '',
@@ -205,60 +269,254 @@ export async function createBacklogItem(
205
269
  return { id, title: fields.title, artifactPath: relativePath(projectDir, filePath) };
206
270
  }
207
271
 
208
- /**
209
- * Top-level scalar frontmatter fields; double-quoted values are read as JSON strings.
210
- * @param {string} text
211
- * @param {string} filePath
212
- */
213
- function frontmatter(text, filePath) {
272
+ const NON_STRING_SCALAR =
273
+ /^(?:null|true|false|~|[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|[+-]?0[xob][0-9a-f]+|[+-]?\.(?:inf|nan))$/iu;
274
+
275
+ /** @param {string} source @returns {string[]} */
276
+ function flowStrings(source) {
277
+ const values = [];
278
+ let rest = source.slice(1).trimStart();
279
+ while (!rest.startsWith(']')) {
280
+ const token = /^(?:"(?:\\.|[^"\\])*"|'(?:''|[^'])*'|[^\s,[\]{}"'][^,[\]{}]*)/u.exec(rest)?.[0];
281
+ if (!token) throw new Error('expected a quoted or plain string');
282
+ const plain = token.trim();
283
+ if (token.startsWith('"')) values.push(JSON.parse(token));
284
+ else if (token.startsWith("'")) values.push(token.slice(1, -1).replaceAll("''", "'"));
285
+ else {
286
+ if (NON_STRING_SCALAR.test(plain) || /^[!&*]/u.test(plain) || /:\s|\s#/u.test(plain))
287
+ throw new Error(`expected a string, received ${JSON.stringify(plain)}`);
288
+ values.push(plain);
289
+ }
290
+ rest = rest.slice(token.length).trimStart();
291
+ if (rest.startsWith(',')) rest = rest.slice(1).trimStart();
292
+ else if (!rest.startsWith(']')) throw new Error('expected a comma or closing bracket');
293
+ }
294
+ if (!/^\](?:\s+#.*)?$/u.test(rest)) throw new Error('unexpected text after the closing bracket');
295
+ return values;
296
+ }
297
+
298
+ /** @param {string} text */
299
+ function frontmatter(text) {
214
300
  const block = /^---\n([\s\S]*?)\n---/u.exec(text.replace(/\r\n/gu, '\n'))?.[1] ?? '';
215
- /** @type {Record<string, string>} */
216
- const fields = {};
301
+ /** @type {Map<string, string>} */
302
+ const fields = new Map();
303
+ const duplicates = new Set();
304
+ let key;
217
305
  for (const line of block.split('\n')) {
218
- const match = /^(\w+):\s*(.*)$/u.exec(line);
219
- if (!match) continue;
220
- const value = match[2].trim();
221
- if (!value.startsWith('"')) {
222
- fields[match[1]] = value.replace(/^'(.*)'$/u, '$1');
223
- continue;
306
+ const match = /^([^ \t:#]+):[ \t]*(.*)$/u.exec(line);
307
+ if (match) {
308
+ key = match[1];
309
+ if (fields.has(key)) duplicates.add(key);
310
+ fields.set(key, match[2].startsWith('#') ? '' : match[2]);
311
+ } else if (key && line.trim() && !line.trimStart().startsWith('#')) {
312
+ fields.set(key, `${fields.get(key)}\n${line}`);
224
313
  }
314
+ }
315
+ return { fields, duplicates };
316
+ }
317
+
318
+ /** @param {string} source */
319
+ function readString(source) {
320
+ if (/^["']/u.test(source)) {
321
+ const quoted = /^("(?:\\.|[^"\\])*"|'(?:''|[^'])*')(?:\s+#.*)?$/u.exec(source)?.[1];
322
+ if (quoted === undefined) throw new Error('expected a quoted string');
323
+ const values = flowStrings(`[${quoted}]`);
324
+ if (values.length !== 1) throw new Error('expected one string');
325
+ return values[0];
326
+ }
327
+ const value = source.replace(/\s+#.*$/u, '').trim();
328
+ if (
329
+ !value ||
330
+ NON_STRING_SCALAR.test(value) ||
331
+ /^[![\]{}&*|>@`"'%#]|^[?-]\s|:\s|[\r\n]/u.test(value)
332
+ )
333
+ throw new Error('expected a string');
334
+ return value;
335
+ }
336
+
337
+ /** @param {string} source */
338
+ function readStrings(source) {
339
+ if (source.startsWith('[')) return flowStrings(source);
340
+ return source.split('\n').map((line) => {
341
+ const match = /^\s*-\s+(.+)$/u.exec(line);
342
+ if (!match) throw new Error('expected a flow array or block sequence of strings');
343
+ return readString(match[1].trim());
344
+ });
345
+ }
346
+
347
+ /** @param {string} source */
348
+ function readFiles(source) {
349
+ const files = readStrings(source);
350
+ if (files.some((file) => !FILE_PATTERN.test(file)) || new Set(files).size !== files.length)
351
+ throw new Error(
352
+ 'expected distinct repo-relative paths without traversal, absolute paths, or line numbers',
353
+ );
354
+ return files;
355
+ }
356
+
357
+ /** @param {string} source */
358
+ function readDate(source) {
359
+ const value = readString(source);
360
+ const date = new Date(`${value}T00:00:00.000Z`);
361
+ if (
362
+ !/^\d{4}-\d{2}-\d{2}$/u.test(value) ||
363
+ !Number.isFinite(date.getTime()) ||
364
+ date.toISOString().slice(0, 10) !== value
365
+ )
366
+ throw new Error('expected a calendar date in YYYY-MM-DD form');
367
+ return value;
368
+ }
369
+
370
+ /** @param {string} text @param {string} path @param {import('./backlog-item.d.mts').BacklogWarning[]} warnings @param {{ id: string, title: string }} fallbacks */
371
+ function readBacklogFields(text, path, warnings, fallbacks) {
372
+ const { fields, duplicates } = frontmatter(text);
373
+ const needed = new Set(['id', 'title', 'status', 'when', 'files']);
374
+ let invalid = false;
375
+ /**
376
+ * @template T
377
+ * @param {string} field
378
+ * @param {(source: string) => T} parse
379
+ * @param {boolean} [optional]
380
+ * @param {T} [fallback]
381
+ */
382
+ const read = (field, parse, optional = false, fallback = undefined) => {
383
+ const source = fields.get(field)?.trim();
225
384
  try {
226
- fields[match[1]] = String(JSON.parse(value));
385
+ if (duplicates.has(field)) throw new Error('duplicate frontmatter key');
386
+ if (source === undefined) {
387
+ if (optional) return undefined;
388
+ throw new Error('required field is missing');
389
+ }
390
+ if ((field === 'when' || field === 'files') && (!source || source.startsWith('#')))
391
+ return undefined;
392
+ return parse(source);
227
393
  } catch (error) {
228
- throw new Error(
229
- `${filePath}: frontmatter field "${match[1]}" is not a valid quoted string: ${/** @type {Error} */ (error).message}`,
230
- );
394
+ const skipped = needed.has(field) && fallback === undefined;
395
+ invalid ||= skipped;
396
+ const value = source ?? 'missing';
397
+ const reason = error instanceof Error ? error.message : String(error);
398
+ warnings.push({
399
+ path,
400
+ field,
401
+ value,
402
+ message: `${path}: ${field} = ${JSON.stringify(value)}: ${reason}${skipped ? '; skipping item' : ''}.`,
403
+ });
404
+ return fallback;
231
405
  }
232
- }
233
- return fields;
406
+ };
407
+ const id = read(
408
+ 'id',
409
+ (source) => {
410
+ const value = readString(source);
411
+ if (!/^[A-Z]+-\d{3,}$/u.test(value))
412
+ throw new Error('expected an artifact ID such as BL-001');
413
+ return value;
414
+ },
415
+ false,
416
+ fallbacks.id,
417
+ );
418
+ const nonEmptyString = (/** @type {string} */ source) => {
419
+ const value = readString(source);
420
+ if (!value.length) throw new Error('expected a non-empty string');
421
+ return value;
422
+ };
423
+ const title = read(
424
+ 'title',
425
+ nonEmptyString,
426
+ false,
427
+ fallbacks.title.trim() ? fallbacks.title : undefined,
428
+ );
429
+ const status = read('status', nonEmptyString, false, 'open');
430
+ const when = read(
431
+ 'when',
432
+ (source) => {
433
+ const value = readString(source);
434
+ if (!value.trim()) return undefined;
435
+ parseWhen(value);
436
+ return value;
437
+ },
438
+ true,
439
+ );
440
+ const files = read('files', readFiles, true);
441
+ read('priority', (source) => {
442
+ const value = readString(source);
443
+ if (!PRIORITIES.includes(value)) throw new Error(`expected one of ${PRIORITIES.join(', ')}`);
444
+ return value;
445
+ });
446
+ read('tags', readStrings);
447
+ read('created', readDate);
448
+ read('updated', readDate);
449
+ read('epicId', readString, true);
450
+ read(
451
+ 'unverified',
452
+ (source) => {
453
+ if (!/^true(?:\s+#.*)?$/iu.test(source)) throw new Error('expected true');
454
+ return true;
455
+ },
456
+ true,
457
+ );
458
+ if (invalid || id === undefined || title === undefined || status === undefined) return undefined;
459
+ return { id, title, status, when, files };
234
460
  }
235
461
 
236
- /** @type {typeof import('./backlog-item.d.mts').listOpenBacklog} */
237
- export async function listOpenBacklog(projectDir) {
462
+ /** @param {string} body */
463
+ function evidenceFiles(body) {
464
+ const paths = [
465
+ ...body.matchAll(/(?:^|[\s`(['"])([^\s`()[\]<>"']+?):\d+(?::\d+|-\d+)?(?=$|[\s`),.;\]"'])/gu),
466
+ ]
467
+ .map((match) => normalizeFilePath(match[1]))
468
+ .filter((file) => FILE_PATTERN.test(file));
469
+ return [...new Set(paths)];
470
+ }
471
+
472
+ /** @type {typeof import('./backlog-item.d.mts').listBacklog} */
473
+ export async function listBacklog(projectDir, options = {}) {
474
+ const requestedFiles =
475
+ options.files === undefined ? undefined : normalizeBacklogFiles(options.files);
238
476
  const { backlogDir } = await projectBacklog(projectDir);
239
477
  let names;
240
478
  try {
241
479
  names = await readdir(backlogDir);
242
480
  } catch (error) {
243
- if (/** @type {NodeJS.ErrnoException} */ (error).code === 'ENOENT') return [];
481
+ if (/** @type {NodeJS.ErrnoException} */ (error).code === 'ENOENT')
482
+ return { items: [], warnings: [] };
244
483
  throw error;
245
484
  }
246
485
  const items = [];
486
+ /** @type {import('./backlog-item.d.mts').BacklogWarning[]} */
487
+ const warnings = [];
247
488
  for (const name of names.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }))) {
248
489
  const match = /^([A-Z]+-\d{3,})-.+\.md$/u.exec(name);
249
490
  if (!match) continue;
250
491
  const filePath = join(backlogDir, name);
251
- const fields = frontmatter(await readFile(filePath, 'utf8'), filePath);
252
- const status = fields.status ?? 'open';
253
- if (CLOSED_STATUSES.has(status)) continue;
492
+ const text = await readFile(filePath, 'utf8');
493
+ const path = relativePath(projectDir, filePath);
494
+ const fields = readBacklogFields(text, path, warnings, {
495
+ id: match[1],
496
+ title: name.slice(match[1].length + 1, -3).replace(/-/g, ' '),
497
+ });
498
+ if (!fields) continue;
499
+ const { id, title, status, when } = fields;
500
+ if (options.open && CLOSED_STATUSES.has(status)) continue;
501
+ if (options.noWhen && when !== undefined) continue;
502
+ const body = text.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u, '');
503
+ const files = fields.files === undefined ? evidenceFiles(body) : fields.files;
504
+ if (requestedFiles && !requestedFiles.some((file) => files.includes(file))) continue;
254
505
  items.push({
255
- id: fields.id ?? match[1],
256
- title: fields.title ?? name.slice(match[1].length + 1, -3).replace(/-/g, ' '),
506
+ id,
507
+ title,
257
508
  status,
258
- path: relativePath(projectDir, filePath),
509
+ path,
510
+ ...(when === undefined ? {} : { when }),
511
+ files,
259
512
  });
260
513
  }
261
- return items;
514
+ return { items, warnings };
515
+ }
516
+
517
+ /** @type {typeof import('./backlog-item.d.mts').listOpenBacklog} */
518
+ export function listOpenBacklog(projectDir, options = {}) {
519
+ return listBacklog(projectDir, { ...options, open: true });
262
520
  }
263
521
 
264
522
  /** @type {typeof import('./backlog-item.d.mts').takeCliHint} */
@@ -292,17 +550,25 @@ async function readInput(source) {
292
550
  }
293
551
 
294
552
  const USAGE =
295
- 'Usage: backlog-item.mjs add --project <dir> --data <file|->\n backlog-item.mjs list --project <dir> --open\n';
553
+ 'Usage: backlog-item.mjs add --project <dir> --data <file|->\n backlog-item.mjs list --project <dir> --open [--files <paths...>] [--no-when]\n';
296
554
 
297
555
  /** @param {string[]} argv */
298
556
  async function main(argv) {
299
557
  const [command, ...rest] = argv;
300
- /** @type {Record<string, string | boolean>} */
558
+ /** @type {Record<string, string | boolean | string[]>} */
301
559
  const options = {};
302
560
  for (let index = 0; index < rest.length; index += 1) {
303
561
  const flag = rest[index];
304
562
  if (flag === '--open') options.open = true;
305
- else if (flag === '--project' || flag === '--data')
563
+ else if (flag === '--no-when') options.noWhen = true;
564
+ else if (flag === '--files') {
565
+ const paths = [];
566
+ while (index + 1 < rest.length && !rest[index + 1].startsWith('--'))
567
+ paths.push(rest[++index]);
568
+ if (paths.length === 0)
569
+ throw new Error(`--files needs at least one repo-relative path.\n${USAGE}`);
570
+ options.files = paths;
571
+ } else if (flag === '--project' || flag === '--data')
306
572
  options[flag.slice(2)] = rest[++index] ?? '';
307
573
  else throw new Error(`Unknown argument: ${flag}\n${USAGE}`);
308
574
  }
@@ -325,7 +591,9 @@ async function main(argv) {
325
591
  return;
326
592
  }
327
593
  if (command === 'list' && options.open) {
328
- process.stdout.write(`${JSON.stringify({ items: await listOpenBacklog(projectDir) })}\n`);
594
+ process.stdout.write(
595
+ `${JSON.stringify(await listOpenBacklog(projectDir, { files: Array.isArray(options.files) ? options.files : undefined, noWhen: options.noWhen === true }))}\n`,
596
+ );
329
597
  return;
330
598
  }
331
599
  throw new Error(USAGE);
@@ -3,11 +3,11 @@
3
3
  "schemaVersion": "1.0.0",
4
4
  "protocolVersion": "1.8.0",
5
5
  "pipelineVersion": "0.63.0",
6
- "pluginVersion": "3.5.0",
6
+ "pluginVersion": "3.6.0",
7
7
  "adapters": [
8
8
  {
9
9
  "id": "claude-code",
10
- "version": "3.5.0",
10
+ "version": "3.6.0",
11
11
  "capabilityLevel": "product",
12
12
  "installScopes": [
13
13
  "user",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  {
21
21
  "id": "codex",
22
- "version": "3.5.0",
22
+ "version": "3.6.0",
23
23
  "capabilityLevel": "product",
24
24
  "installScopes": [
25
25
  "user",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  {
33
33
  "id": "cursor",
34
- "version": "3.5.0",
34
+ "version": "3.6.0",
35
35
  "capabilityLevel": "workflow",
36
36
  "installScopes": [
37
37
  "project"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "kind": "openplanr-capability-map",
3
3
  "schemaVersion": "1.0.0",
4
- "pluginVersion": "3.5.0",
4
+ "pluginVersion": "3.6.0",
5
5
  "families": [
6
6
  {
7
7
  "id": "planning",
@@ -5,14 +5,14 @@
5
5
  "name": "OpenPlanr"
6
6
  },
7
7
  "metadata": {
8
- "version": "3.5.0",
8
+ "version": "3.6.0",
9
9
  "description": "Generated local OpenPlanr package."
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "planr",
14
14
  "source": "./openplanr",
15
- "version": "3.5.0",
15
+ "version": "3.6.0",
16
16
  "description": "Close the loop from intent to delivery. Plans as files in your repo, checks before you merge, and a record of what shipped, in Claude Code, Codex and Cursor.",
17
17
  "strict": true
18
18
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "planr",
3
3
  "displayName": "OpenPlanr",
4
- "version": "3.5.0",
4
+ "version": "3.6.0",
5
5
  "description": "Close the loop from intent to delivery. Plans as files in your repo, checks before you merge, and a record of what shipped, in Claude Code, Codex and Cursor.",
6
6
  "author": {
7
7
  "name": "OpenPlanr",