context-planning 0.7.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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -0
  3. package/bin/commands/_helpers.js +53 -0
  4. package/bin/commands/_usage.js +67 -0
  5. package/bin/commands/capture.js +46 -0
  6. package/bin/commands/codebase-status.js +41 -0
  7. package/bin/commands/complete-milestone.js +57 -0
  8. package/bin/commands/config.js +70 -0
  9. package/bin/commands/doctor.js +139 -0
  10. package/bin/commands/gsd-import.js +90 -0
  11. package/bin/commands/inbox.js +81 -0
  12. package/bin/commands/index.js +33 -0
  13. package/bin/commands/init.js +87 -0
  14. package/bin/commands/install.js +43 -0
  15. package/bin/commands/scaffold-codebase.js +53 -0
  16. package/bin/commands/scaffold-milestone.js +58 -0
  17. package/bin/commands/scaffold-phase.js +65 -0
  18. package/bin/commands/status.js +42 -0
  19. package/bin/commands/statusline.js +108 -0
  20. package/bin/commands/tick.js +49 -0
  21. package/bin/commands/version.js +9 -0
  22. package/bin/commands/worktree.js +218 -0
  23. package/bin/commands/write-summary.js +54 -0
  24. package/bin/cp.cmd +2 -0
  25. package/bin/cp.js +54 -0
  26. package/commands/cp/capture.md +107 -0
  27. package/commands/cp/complete-milestone.md +166 -0
  28. package/commands/cp/execute-phase.md +220 -0
  29. package/commands/cp/map-codebase.md +211 -0
  30. package/commands/cp/new-milestone.md +136 -0
  31. package/commands/cp/new-project.md +132 -0
  32. package/commands/cp/plan-phase.md +195 -0
  33. package/commands/cp/progress.md +147 -0
  34. package/commands/cp/quick.md +104 -0
  35. package/commands/cp/resume.md +125 -0
  36. package/commands/cp/write-summary.md +33 -0
  37. package/docs/MIGRATION-v0.5.md +140 -0
  38. package/docs/architecture.md +189 -0
  39. package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-01-design-md-infrastructure.md +1064 -0
  40. package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-02-review-log-infrastructure.md +418 -0
  41. package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-03-key-decisions-hard-block.md +295 -0
  42. package/docs/superpowers/specs/2026-05-20-generic-provider-harness-detection-design.md +380 -0
  43. package/docs/superpowers/specs/2026-05-20-v0-7-design-capture-design.md +400 -0
  44. package/docs/writing-providers.md +76 -0
  45. package/install/aider.js +204 -0
  46. package/install/claude.js +116 -0
  47. package/install/common.js +65 -0
  48. package/install/copilot.js +86 -0
  49. package/install/cursor.js +120 -0
  50. package/install/echo-provider.js +50 -0
  51. package/lib/codebase-mapper.js +169 -0
  52. package/lib/detect.js +280 -0
  53. package/lib/frontmatter.js +72 -0
  54. package/lib/gsd-compat.js +165 -0
  55. package/lib/import.js +543 -0
  56. package/lib/inbox.js +226 -0
  57. package/lib/lifecycle.js +929 -0
  58. package/lib/merge.js +157 -0
  59. package/lib/milestone.js +595 -0
  60. package/lib/paths.js +191 -0
  61. package/lib/provider.js +168 -0
  62. package/lib/roadmap.js +134 -0
  63. package/lib/state.js +99 -0
  64. package/lib/worktree.js +253 -0
  65. package/package.json +45 -0
  66. package/templates/DESIGN.md +78 -0
  67. package/templates/INBOX.md +13 -0
  68. package/templates/MILESTONE-CONTEXT.md +40 -0
  69. package/templates/MILESTONES.md +29 -0
  70. package/templates/PLAN.md +84 -0
  71. package/templates/PROJECT.md +43 -0
  72. package/templates/REVIEW-LOG.md +38 -0
  73. package/templates/ROADMAP.md +34 -0
  74. package/templates/STATE.md +78 -0
  75. package/templates/SUMMARY.md +75 -0
  76. package/templates/codebase/ARCHITECTURE.md +30 -0
  77. package/templates/codebase/CONCERNS.md +30 -0
  78. package/templates/codebase/CONVENTIONS.md +30 -0
  79. package/templates/codebase/INTEGRATIONS.md +30 -0
  80. package/templates/codebase/STACK.md +26 -0
  81. package/templates/codebase/STRUCTURE.md +32 -0
  82. package/templates/codebase/TESTING.md +39 -0
  83. package/templates/config.json +173 -0
  84. package/templates/phase-PLAN.md +32 -0
  85. package/templates/quick-PLAN.md +24 -0
  86. package/templates/quick-SUMMARY.md +25 -0
package/lib/inbox.js ADDED
@@ -0,0 +1,226 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * lib/inbox.js — pure file-IO module for `.planning/INBOX.md`.
5
+ *
6
+ * INBOX.md is a lightweight capture buffer. Each item is one line in either
7
+ * the `## Open` section (untriaged) or `## Triaged` section (handled).
8
+ *
9
+ * Line format:
10
+ * open: `- [ ] [YYYY-MM-DDTHH:mm] free-form text`
11
+ * triaged: `- [x] [YYYY-MM-DDTHH:mm] → destination-tag: original text`
12
+ *
13
+ * `destination-tag` is a short label the triage tool wrote (e.g. `quick`,
14
+ * `phase:02-mvp`, `seed:routing-redesign`, `discard`). cp does NOT enforce
15
+ * a closed vocabulary — the slash command picks whatever's useful.
16
+ *
17
+ * All exported functions are pure transforms (no fs writes). The CLI layer
18
+ * in `bin/cp.js` is responsible for turning returned `actions` into actual
19
+ * writes via `lifecycle.writeBatch` so they pick up atomic / commit-scoped
20
+ * behaviour automatically.
21
+ *
22
+ * v0.4.0 — initial implementation.
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const { planningDir } = require('./paths');
28
+
29
+ const INBOX_FILENAME = 'INBOX.md';
30
+
31
+ const HEADER = `# Inbox
32
+
33
+ Quick captures awaiting triage. Use \`cp capture "..."\` to add an item,
34
+ \`cp inbox\` to list, and the \`/cp-capture\` slash command to process them
35
+ interactively (route each to a quick task, phase, seed, or discard).
36
+
37
+ `;
38
+
39
+ const ITEM_RE_OPEN = /^- \[ \] \[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})\] (.+)$/;
40
+ const ITEM_RE_DONE = /^- \[x\] \[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2})\] (?:→\s*)?(?:(\S+):\s*)?(.+)$/i;
41
+
42
+ function inboxPath(root) {
43
+ return path.join(planningDir(root), INBOX_FILENAME);
44
+ }
45
+
46
+ /**
47
+ * Parse an INBOX.md body into `{ open: [...], triaged: [...] }` arrays of
48
+ * `{ idx, ts, text, destination? }` items. `idx` is the 1-based position
49
+ * within its own section — stable and human-friendly for `cp inbox --tick N`.
50
+ *
51
+ * Anything outside the `## Open` / `## Triaged` sections (e.g. the file
52
+ * header, in-section HTML comments, or stray prose) is preserved verbatim
53
+ * by `renderInbox`.
54
+ */
55
+ function parseInbox(content) {
56
+ const open = [];
57
+ const triaged = [];
58
+ if (typeof content !== 'string' || content.length === 0) return { open, triaged };
59
+
60
+ // Split into sections by `## Open` / `## Triaged` headings.
61
+ const sections = {};
62
+ let current = null;
63
+ for (const line of content.split(/\r?\n/)) {
64
+ const h = line.match(/^##\s+(Open|Triaged)\s*$/i);
65
+ if (h) {
66
+ current = h[1].toLowerCase();
67
+ sections[current] = sections[current] || [];
68
+ continue;
69
+ }
70
+ if (current) sections[current].push(line);
71
+ }
72
+
73
+ let i = 0;
74
+ for (const line of sections.open || []) {
75
+ const m = line.match(ITEM_RE_OPEN);
76
+ if (m) {
77
+ i++;
78
+ open.push({ idx: i, ts: m[1], text: m[2].trim() });
79
+ }
80
+ }
81
+ i = 0;
82
+ for (const line of sections.triaged || []) {
83
+ const m = line.match(ITEM_RE_DONE);
84
+ if (m) {
85
+ i++;
86
+ triaged.push({
87
+ idx: i,
88
+ ts: m[1],
89
+ destination: m[2] || null,
90
+ text: m[3].trim(),
91
+ });
92
+ }
93
+ }
94
+ return { open, triaged };
95
+ }
96
+
97
+ /**
98
+ * Re-render an `{ open, triaged }` shape back to INBOX.md content. Stable
99
+ * formatting: the file header is fixed, items are sorted by their original
100
+ * timestamp ascending, and each section keeps its HTML comment guidance.
101
+ */
102
+ function renderInbox({ open = [], triaged = [] } = {}) {
103
+ const openLines = open
104
+ .slice()
105
+ .sort((a, b) => a.ts.localeCompare(b.ts))
106
+ .map((it) => `- [ ] [${it.ts}] ${it.text}`);
107
+
108
+ const triagedLines = triaged
109
+ .slice()
110
+ .sort((a, b) => a.ts.localeCompare(b.ts))
111
+ .map((it) => {
112
+ const dest = it.destination ? `→ ${it.destination}: ` : '→ ';
113
+ return `- [x] [${it.ts}] ${dest}${it.text}`;
114
+ });
115
+
116
+ return [
117
+ HEADER.trimEnd(),
118
+ '',
119
+ '## Open',
120
+ '',
121
+ openLines.length
122
+ ? openLines.join('\n')
123
+ : '<!-- new items get appended here as: `- [ ] [YYYY-MM-DDTHH:mm] <text>` -->',
124
+ '',
125
+ '## Triaged',
126
+ '',
127
+ triagedLines.length
128
+ ? triagedLines.join('\n')
129
+ : '<!-- triaged items move here as: `- [x] [YYYY-MM-DDTHH:mm] → <destination>: <text>` -->',
130
+ '',
131
+ ].join('\n');
132
+ }
133
+
134
+ /**
135
+ * Produce a normalized timestamp string for inbox items: ISO-8601 minute
136
+ * precision in local time (e.g. "2026-05-20T14:32"). Pass a Date for tests.
137
+ */
138
+ function isoMinute(d = new Date()) {
139
+ const pad = (n) => String(n).padStart(2, '0');
140
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
141
+ }
142
+
143
+ /**
144
+ * Append a new open item to INBOX.md.
145
+ *
146
+ * options.now — optional Date for deterministic tests.
147
+ *
148
+ * Returns `{ actions, item, alreadyPresent }`:
149
+ * - actions[0] is a write action for INBOX.md (always — even if the file
150
+ * already existed, we re-render it for consistent formatting).
151
+ * - item.idx is the 1-based index of the new item within `Open` AFTER
152
+ * re-sort.
153
+ * - alreadyPresent is true if an identical (same text, same minute) item
154
+ * already exists in Open — cp does NOT dedupe, but the caller may want
155
+ * to warn.
156
+ */
157
+ function appendItem(root, text, options = {}) {
158
+ if (typeof text !== 'string' || text.trim().length === 0) {
159
+ throw new Error('appendItem: text must be a non-empty string');
160
+ }
161
+ const cleaned = text.trim();
162
+ const p = inboxPath(root);
163
+ const existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
164
+ const parsed = parseInbox(existing);
165
+ const ts = isoMinute(options.now);
166
+ const alreadyPresent = parsed.open.some((it) => it.ts === ts && it.text === cleaned);
167
+ parsed.open.push({ ts, text: cleaned });
168
+ const after = renderInbox(parsed);
169
+ // Recompute item idx after sort.
170
+ const newIdx = parseInbox(after).open.findIndex(
171
+ (it) => it.ts === ts && it.text === cleaned
172
+ ) + 1;
173
+ return {
174
+ actions: [{ kind: 'write', path: p, after, label: 'append-inbox-item' }],
175
+ item: { idx: newIdx, ts, text: cleaned },
176
+ alreadyPresent,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Move an open item to the Triaged section.
182
+ *
183
+ * Returns `{ actions, item }`. Throws if no open item has the given 1-based idx.
184
+ */
185
+ function markTriaged(root, openIdx, destination = null) {
186
+ const p = inboxPath(root);
187
+ if (!fs.existsSync(p)) throw new Error(`Inbox not found at ${p}. Run \`cp capture "..."\` first.`);
188
+ const parsed = parseInbox(fs.readFileSync(p, 'utf8'));
189
+ const target = parsed.open.find((it) => it.idx === Number(openIdx));
190
+ if (!target) {
191
+ throw new Error(`No open inbox item with index ${openIdx}. Run \`cp inbox\` to see indices.`);
192
+ }
193
+ parsed.open = parsed.open.filter((it) => it.idx !== target.idx);
194
+ parsed.triaged.push({
195
+ ts: target.ts,
196
+ text: target.text,
197
+ destination: destination ? String(destination).trim() : null,
198
+ });
199
+ const after = renderInbox(parsed);
200
+ return {
201
+ actions: [{ kind: 'write', path: p, after, label: 'mark-triaged' }],
202
+ item: { ts: target.ts, text: target.text, destination },
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Read the current inbox state. Returns `{ open: [...], triaged: [...] }`
208
+ * with `idx` fields populated. If INBOX.md doesn't exist, returns empty
209
+ * arrays (NOT an error — capturing for the first time should just work).
210
+ */
211
+ function listInbox(root) {
212
+ const p = inboxPath(root);
213
+ if (!fs.existsSync(p)) return { open: [], triaged: [], path: p, exists: false };
214
+ return { ...parseInbox(fs.readFileSync(p, 'utf8')), path: p, exists: true };
215
+ }
216
+
217
+ module.exports = {
218
+ INBOX_FILENAME,
219
+ inboxPath,
220
+ parseInbox,
221
+ renderInbox,
222
+ isoMinute,
223
+ appendItem,
224
+ markTriaged,
225
+ listInbox,
226
+ };