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.
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/bin/commands/_helpers.js +53 -0
- package/bin/commands/_usage.js +67 -0
- package/bin/commands/capture.js +46 -0
- package/bin/commands/codebase-status.js +41 -0
- package/bin/commands/complete-milestone.js +57 -0
- package/bin/commands/config.js +70 -0
- package/bin/commands/doctor.js +139 -0
- package/bin/commands/gsd-import.js +90 -0
- package/bin/commands/inbox.js +81 -0
- package/bin/commands/index.js +33 -0
- package/bin/commands/init.js +87 -0
- package/bin/commands/install.js +43 -0
- package/bin/commands/scaffold-codebase.js +53 -0
- package/bin/commands/scaffold-milestone.js +58 -0
- package/bin/commands/scaffold-phase.js +65 -0
- package/bin/commands/status.js +42 -0
- package/bin/commands/statusline.js +108 -0
- package/bin/commands/tick.js +49 -0
- package/bin/commands/version.js +9 -0
- package/bin/commands/worktree.js +218 -0
- package/bin/commands/write-summary.js +54 -0
- package/bin/cp.cmd +2 -0
- package/bin/cp.js +54 -0
- package/commands/cp/capture.md +107 -0
- package/commands/cp/complete-milestone.md +166 -0
- package/commands/cp/execute-phase.md +220 -0
- package/commands/cp/map-codebase.md +211 -0
- package/commands/cp/new-milestone.md +136 -0
- package/commands/cp/new-project.md +132 -0
- package/commands/cp/plan-phase.md +195 -0
- package/commands/cp/progress.md +147 -0
- package/commands/cp/quick.md +104 -0
- package/commands/cp/resume.md +125 -0
- package/commands/cp/write-summary.md +33 -0
- package/docs/MIGRATION-v0.5.md +140 -0
- package/docs/architecture.md +189 -0
- package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-01-design-md-infrastructure.md +1064 -0
- package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-02-review-log-infrastructure.md +418 -0
- package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-03-key-decisions-hard-block.md +295 -0
- package/docs/superpowers/specs/2026-05-20-generic-provider-harness-detection-design.md +380 -0
- package/docs/superpowers/specs/2026-05-20-v0-7-design-capture-design.md +400 -0
- package/docs/writing-providers.md +76 -0
- package/install/aider.js +204 -0
- package/install/claude.js +116 -0
- package/install/common.js +65 -0
- package/install/copilot.js +86 -0
- package/install/cursor.js +120 -0
- package/install/echo-provider.js +50 -0
- package/lib/codebase-mapper.js +169 -0
- package/lib/detect.js +280 -0
- package/lib/frontmatter.js +72 -0
- package/lib/gsd-compat.js +165 -0
- package/lib/import.js +543 -0
- package/lib/inbox.js +226 -0
- package/lib/lifecycle.js +929 -0
- package/lib/merge.js +157 -0
- package/lib/milestone.js +595 -0
- package/lib/paths.js +191 -0
- package/lib/provider.js +168 -0
- package/lib/roadmap.js +134 -0
- package/lib/state.js +99 -0
- package/lib/worktree.js +253 -0
- package/package.json +45 -0
- package/templates/DESIGN.md +78 -0
- package/templates/INBOX.md +13 -0
- package/templates/MILESTONE-CONTEXT.md +40 -0
- package/templates/MILESTONES.md +29 -0
- package/templates/PLAN.md +84 -0
- package/templates/PROJECT.md +43 -0
- package/templates/REVIEW-LOG.md +38 -0
- package/templates/ROADMAP.md +34 -0
- package/templates/STATE.md +78 -0
- package/templates/SUMMARY.md +75 -0
- package/templates/codebase/ARCHITECTURE.md +30 -0
- package/templates/codebase/CONCERNS.md +30 -0
- package/templates/codebase/CONVENTIONS.md +30 -0
- package/templates/codebase/INTEGRATIONS.md +30 -0
- package/templates/codebase/STACK.md +26 -0
- package/templates/codebase/STRUCTURE.md +32 -0
- package/templates/codebase/TESTING.md +39 -0
- package/templates/config.json +173 -0
- package/templates/phase-PLAN.md +32 -0
- package/templates/quick-PLAN.md +24 -0
- 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
|
+
};
|