plan-assistant 1.4.5 → 1.6.0-alpha.1
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/README.md +31 -12
- package/build/client/_app/immutable/assets/0.Bc8GgJCN.css +1 -0
- package/build/client/_app/immutable/chunks/{ABPQj_I6.js → 5KpFG7Y3.js} +1 -1
- package/build/client/_app/immutable/chunks/AMggw8F_.js +1 -0
- package/build/client/_app/immutable/chunks/{DCP7pAeu.js → B0Br67Jm.js} +22 -22
- package/build/client/_app/immutable/chunks/{D01yuNpr.js → BGuzEOEG.js} +1 -1
- package/build/client/_app/immutable/chunks/{x3JJhWj3.js → BPC30ZkC.js} +1 -1
- package/build/client/_app/immutable/chunks/{Dn9lSg-V.js → BT-rYp04.js} +1 -1
- package/build/client/_app/immutable/chunks/{B25Hd2fz.js → BXachUYg.js} +1 -1
- package/build/client/_app/immutable/chunks/{CKmKshyc.js → BdxNwExl.js} +1 -1
- package/build/client/_app/immutable/chunks/{BcKBKgcZ.js → Bdy6d4YN.js} +1 -1
- package/build/client/_app/immutable/chunks/{ClpyzqjJ.js → BkkBWYh4.js} +1 -1
- package/build/client/_app/immutable/chunks/{yS11QLaS.js → Bmsplg2Y.js} +1 -1
- package/build/client/_app/immutable/chunks/{hHxGQnZj.js → Bn0XLNse.js} +1 -1
- package/build/client/_app/immutable/chunks/{J7DoTV-B.js → BuxFeYZa.js} +1 -1
- package/build/client/_app/immutable/chunks/{DJ4AwCJ9.js → C2OLrLCJ.js} +1 -1
- package/build/client/_app/immutable/chunks/C88p_n1l.js +3 -0
- package/build/client/_app/immutable/chunks/{DVaxwt1f.js → CFomSPzW.js} +1 -1
- package/build/client/_app/immutable/chunks/{BRUI-1NR.js → CFx4VLDw.js} +1 -1
- package/build/client/_app/immutable/chunks/{DBY3mAk0.js → CRUUUIwS.js} +1 -1
- package/build/client/_app/immutable/chunks/{CfICpXHz.js → CRuCklg1.js} +1 -1
- package/build/client/_app/immutable/chunks/{DmuA5Qzn.js → CUHvMbx0.js} +1 -1
- package/build/client/_app/immutable/chunks/{CadzRJ6P.js → C_5efo6-.js} +1 -1
- package/build/client/_app/immutable/chunks/{BierXyQN.js → Cb177sm-.js} +1 -1
- package/build/client/_app/immutable/chunks/{CWe1fJjs.js → Cb9ekAV0.js} +1 -1
- package/build/client/_app/immutable/chunks/{DnZ1MoPi.js → CdiHcRRA.js} +1 -1
- package/build/client/_app/immutable/chunks/{BAG_vqoT.js → CgIOtWnS.js} +1 -1
- package/build/client/_app/immutable/chunks/{CbLSu4oh.js → CghPw8Xx.js} +1 -1
- package/build/client/_app/immutable/chunks/{Uy7RUP_Z.js → Cmym6IfH.js} +1 -1
- package/build/client/_app/immutable/chunks/{C7FtzXJF.js → Cvpo3167.js} +1 -1
- package/build/client/_app/immutable/chunks/{4dPdNqKL.js → Cy0JyQfD.js} +1 -1
- package/build/client/_app/immutable/chunks/{DXnvq78W.js → D-0mahX7.js} +1 -1
- package/build/client/_app/immutable/chunks/{gqH_bfvs.js → D8823oiN.js} +1 -1
- package/build/client/_app/immutable/chunks/{BCxCv_4N.js → DIjXbgfi.js} +1 -1
- package/build/client/_app/immutable/chunks/{CF3N_j1A.js → DUIJXtzj.js} +1 -1
- package/build/client/_app/immutable/chunks/{DbxsK-Gw.js → DXlvMCt6.js} +1 -1
- package/build/client/_app/immutable/chunks/{DA_DyJGG.js → Dffa6mVP.js} +1 -1
- package/build/client/_app/immutable/chunks/{D8aFfI6M.js → DgzenGSu.js} +1 -1
- package/build/client/_app/immutable/chunks/{1hoFVlwF.js → DqaGnr0n.js} +4 -4
- package/build/client/_app/immutable/chunks/{CE48ZXRi.js → Drf3oeac.js} +1 -1
- package/build/client/_app/immutable/chunks/{CmCoqHco.js → EBvoHjF1.js} +1 -1
- package/build/client/_app/immutable/chunks/{Cumsgxi9.js → Fqo6xqtt.js} +1 -1
- package/build/client/_app/immutable/chunks/QDQklEwM.js +1 -0
- package/build/client/_app/immutable/chunks/{zMHAx0Qk.js → Q_gMk3PN.js} +1 -1
- package/build/client/_app/immutable/chunks/{Wv7WdG7L.js → XHkT9caI.js} +1 -1
- package/build/client/_app/immutable/chunks/{B3CyqxdZ.js → Zxe_1j0j.js} +1 -1
- package/build/client/_app/immutable/chunks/{Bysr0gLv.js → czbfzgw3.js} +1 -1
- package/build/client/_app/immutable/chunks/{BdcZtdph.js → mlMCI16G.js} +1 -1
- package/build/client/_app/immutable/chunks/{BEobpKJ6.js → qFFXeAvY.js} +1 -1
- package/build/client/_app/immutable/entry/{app.BdutA_ce.js → app.C_pWXh8R.js} +2 -2
- package/build/client/_app/immutable/entry/start.DfbNX0kb.js +1 -0
- package/build/client/_app/immutable/nodes/{1.BBwvzKm5.js → 1.DN_p76XO.js} +1 -1
- package/build/client/_app/immutable/nodes/3.B0eShLlr.js +1 -0
- package/build/client/_app/version.json +1 -1
- package/build/server/chunks/{0-DpiwjOzQ.js → 0-rXmaDIAK.js} +3 -3
- package/build/server/chunks/{0-DpiwjOzQ.js.map → 0-rXmaDIAK.js.map} +1 -1
- package/build/server/chunks/{1-DLxoXeuD.js → 1-CEFNOpUG.js} +2 -2
- package/build/server/chunks/{1-DLxoXeuD.js.map → 1-CEFNOpUG.js.map} +1 -1
- package/build/server/chunks/{2-Cy9PX2zK.js → 2-qGWtN2-9.js} +3 -2
- package/build/server/chunks/{2-Cy9PX2zK.js.map → 2-qGWtN2-9.js.map} +1 -1
- package/build/server/chunks/{3-DIjT-1nt.js → 3-OUU46X3b.js} +6 -7
- package/build/server/chunks/3-OUU46X3b.js.map +1 -0
- package/build/server/chunks/{_page.svelte-XhzZOhTs.js → _page.svelte-DcqVlJwT.js} +13 -14
- package/build/server/chunks/_page.svelte-DcqVlJwT.js.map +1 -0
- package/build/server/chunks/_server.ts-BOWeAYnM.js +13 -0
- package/build/server/chunks/_server.ts-BOWeAYnM.js.map +1 -0
- package/build/server/chunks/{_server.ts-CEtvrNT1.js → _server.ts-BbMhMik1.js} +4 -3
- package/build/server/chunks/_server.ts-BbMhMik1.js.map +1 -0
- package/build/server/chunks/{_server.ts-b4ynvdd4.js → _server.ts-BhmTJZsP.js} +3 -4
- package/build/server/chunks/_server.ts-BhmTJZsP.js.map +1 -0
- package/build/server/chunks/{_server.ts-DCa-_qe7.js → _server.ts-BjSxqsHx.js} +5 -7
- package/build/server/chunks/_server.ts-BjSxqsHx.js.map +1 -0
- package/build/server/chunks/{_server.ts-sRxHDQa9.js → _server.ts-C7d7vtwn.js} +3 -2
- package/build/server/chunks/_server.ts-C7d7vtwn.js.map +1 -0
- package/build/server/chunks/{_server.ts-aWI5JVTi.js → _server.ts-D9nBKsq1.js} +2 -2
- package/build/server/chunks/{_server.ts-aWI5JVTi.js.map → _server.ts-D9nBKsq1.js.map} +1 -1
- package/build/server/chunks/_server.ts-DTGwAmWz.js +14 -0
- package/build/server/chunks/_server.ts-DTGwAmWz.js.map +1 -0
- package/build/server/chunks/{_server.ts-DVH0ejp0.js → _server.ts-DiMlmks0.js} +2 -2
- package/build/server/chunks/{_server.ts-DVH0ejp0.js.map → _server.ts-DiMlmks0.js.map} +1 -1
- package/build/server/chunks/{hooks.server-sBuk2932.js → hooks.server-DigZpFgW.js} +4 -3
- package/build/server/chunks/{hooks.server-sBuk2932.js.map → hooks.server-DigZpFgW.js.map} +1 -1
- package/build/server/chunks/{idle-timer-CouFy_WF.js → idle-timer-BshN5Svi.js} +3 -2
- package/build/server/chunks/idle-timer-BshN5Svi.js.map +1 -0
- package/build/server/chunks/{session-manager-Vdr0BxAo.js → session-manager-scAVAR7S.js} +39 -41
- package/build/server/chunks/session-manager-scAVAR7S.js.map +1 -0
- package/build/server/index.js +2 -2
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +13 -20
- package/build/server/manifest.js.map +1 -1
- package/dist/cli/commands/clean.js +33 -29
- package/dist/cli/commands/export.js +5 -8
- package/dist/cli/commands/feedback.js +5 -8
- package/dist/cli/commands/init.js +2 -1
- package/dist/cli/commands/review.js +18 -121
- package/dist/cli/commands/status.js +20 -23
- package/dist/cli/commands/stop.js +7 -15
- package/dist/cli/errors.js +23 -0
- package/dist/cli/export-html.js +1 -1
- package/dist/cli/index.js +21 -5
- package/dist/cli/markdown-parser.js +117 -132
- package/dist/cli/markdown-to-plan.js +1 -50
- package/dist/cli/review-session.js +130 -0
- package/dist/cli/server-client.js +20 -33
- package/dist/cli/session-reader.js +20 -47
- package/dist/cli/session-resolver.js +10 -0
- package/dist/lib/server/session-manager.js +115 -0
- package/dist/lib/utils/status.js +53 -0
- package/package.json +1 -1
- package/build/client/_app/immutable/assets/0.CltY7ilp.css +0 -1
- package/build/client/_app/immutable/chunks/CULHGyhn.js +0 -1
- package/build/client/_app/immutable/chunks/DPsQ0F5p.js +0 -3
- package/build/client/_app/immutable/chunks/UfQGGAux.js +0 -1
- package/build/client/_app/immutable/entry/start.CTfow7ge.js +0 -1
- package/build/client/_app/immutable/nodes/3.DHzG8WJV.js +0 -1
- package/build/server/chunks/3-DIjT-1nt.js.map +0 -1
- package/build/server/chunks/_page.svelte-XhzZOhTs.js.map +0 -1
- package/build/server/chunks/_server.ts-BkcqX0Gw.js +0 -14
- package/build/server/chunks/_server.ts-BkcqX0Gw.js.map +0 -1
- package/build/server/chunks/_server.ts-C9Rz5pqb.js +0 -15
- package/build/server/chunks/_server.ts-C9Rz5pqb.js.map +0 -1
- package/build/server/chunks/_server.ts-CEtvrNT1.js.map +0 -1
- package/build/server/chunks/_server.ts-DCa-_qe7.js.map +0 -1
- package/build/server/chunks/_server.ts-b4ynvdd4.js.map +0 -1
- package/build/server/chunks/_server.ts-r59HGb-h.js +0 -22
- package/build/server/chunks/_server.ts-r59HGb-h.js.map +0 -1
- package/build/server/chunks/_server.ts-sRxHDQa9.js.map +0 -1
- package/build/server/chunks/idle-timer-CouFy_WF.js.map +0 -1
- package/build/server/chunks/session-manager-Vdr0BxAo.js.map +0 -1
- /package/build/client/_app/immutable/nodes/{0.D7ZwKtS7.js → 0.CuhA9F69.js} +0 -0
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { stopServer, checkHealth, DEFAULT_BASE_PORT, MAX_PORT, } from "../server-client.js";
|
|
4
|
-
import { outputJson
|
|
2
|
+
import { requireSession } from "../session-resolver.js";
|
|
3
|
+
import { stopServer, checkHealth, fetchWithTimeout, clearLock, DEFAULT_BASE_PORT, MAX_PORT, } from "../server-client.js";
|
|
4
|
+
import { outputJson } from "../output.js";
|
|
5
5
|
export async function stop(args) {
|
|
6
6
|
const target = args.positional[0];
|
|
7
7
|
if (target) {
|
|
8
8
|
// Stop server for a specific session
|
|
9
|
-
const session =
|
|
10
|
-
if (!session) {
|
|
11
|
-
outputError(`Session not found: ${target}`, "NOT_FOUND");
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
9
|
+
const session = requireSession(target);
|
|
14
10
|
const parentDir = dirname(session.sessionDir);
|
|
15
11
|
const stopped = await stopServer(parentDir);
|
|
16
12
|
if (stopped) {
|
|
@@ -30,13 +26,8 @@ export async function stop(args) {
|
|
|
30
26
|
const health = await checkHealth(port);
|
|
31
27
|
if (health) {
|
|
32
28
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await fetch(`http://localhost:${port}/api/shutdown`, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
signal: controller.signal,
|
|
38
|
-
});
|
|
39
|
-
clearTimeout(timeout);
|
|
29
|
+
await fetchWithTimeout(`http://localhost:${port}/api/shutdown`, { method: "POST" }, 2000);
|
|
30
|
+
clearLock(health.sessionDir);
|
|
40
31
|
results.push({ port, sessionDir: health.sessionDir });
|
|
41
32
|
stopped++;
|
|
42
33
|
}
|
|
@@ -44,6 +35,7 @@ export async function stop(args) {
|
|
|
44
35
|
// Try SIGTERM
|
|
45
36
|
try {
|
|
46
37
|
process.kill(health.pid, "SIGTERM");
|
|
38
|
+
clearLock(health.sessionDir);
|
|
47
39
|
results.push({ port, sessionDir: health.sessionDir });
|
|
48
40
|
stopped++;
|
|
49
41
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by CLI commands to signal an error exit.
|
|
3
|
+
* Caught by main() which writes message to stderr and calls process.exit(exitCode).
|
|
4
|
+
*/
|
|
5
|
+
export class CliError extends Error {
|
|
6
|
+
exitCode;
|
|
7
|
+
constructor(message, exitCode = 1) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.exitCode = exitCode;
|
|
10
|
+
this.name = "CliError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Thrown by CLI commands to signal intentional non-error exit with a specific code.
|
|
15
|
+
* For example, `status` exits with code 3 for "needs-work" — not an error, just a signal.
|
|
16
|
+
* Caught by main() which calls process.exit(exitCode) without printing an error.
|
|
17
|
+
*/
|
|
18
|
+
export class CliExitCode {
|
|
19
|
+
exitCode;
|
|
20
|
+
constructor(exitCode) {
|
|
21
|
+
this.exitCode = exitCode;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/cli/export-html.js
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -130,8 +130,8 @@ Optional top-level sections (all H2):
|
|
|
130
130
|
## Testing Strategy
|
|
131
131
|
## References`);
|
|
132
132
|
}
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
import { CliError, CliExitCode } from "./errors.js";
|
|
134
|
+
async function dispatch(parsed) {
|
|
135
135
|
switch (parsed.command) {
|
|
136
136
|
case "review": {
|
|
137
137
|
const { review } = await import("./commands/review.js");
|
|
@@ -176,8 +176,24 @@ export async function main(args) {
|
|
|
176
176
|
}
|
|
177
177
|
return;
|
|
178
178
|
default:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
throw new CliError(`Unknown command: ${parsed.command}`, 1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export async function main(args) {
|
|
183
|
+
const parsed = parseArgs(args);
|
|
184
|
+
try {
|
|
185
|
+
await dispatch(parsed);
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (err instanceof CliExitCode) {
|
|
189
|
+
process.exit(err.exitCode);
|
|
190
|
+
}
|
|
191
|
+
if (err instanceof CliError) {
|
|
192
|
+
console.error(err.message);
|
|
193
|
+
process.exit(err.exitCode);
|
|
194
|
+
}
|
|
195
|
+
// Unexpected error
|
|
196
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
197
|
+
process.exit(1);
|
|
182
198
|
}
|
|
183
199
|
}
|
|
@@ -260,7 +260,7 @@ export function tryMatchPhaseHeading(heading, autoNumber) {
|
|
|
260
260
|
return null;
|
|
261
261
|
}
|
|
262
262
|
/** Known top-level section patterns that should NOT be treated as phases */
|
|
263
|
-
const KNOWN_SECTION_PATTERNS = [
|
|
263
|
+
export const KNOWN_SECTION_PATTERNS = [
|
|
264
264
|
/^Overview$/i,
|
|
265
265
|
/^Current\s+State/i,
|
|
266
266
|
/What\s+We.*NOT\s+Doing/i,
|
|
@@ -274,147 +274,132 @@ const KNOWN_SECTION_PATTERNS = [
|
|
|
274
274
|
];
|
|
275
275
|
const CHANGES_HEADING_PATTERN = /^(?:Changes\s+Required|Changes|File\s+Changes|Modifications):?$/i;
|
|
276
276
|
const SUCCESS_CRITERIA_HEADING_PATTERN = /^(?:Success\s+Criteria|Criteria|Verification):?$/i;
|
|
277
|
+
/**
|
|
278
|
+
* Detect whether a section is a phase heading.
|
|
279
|
+
* Handles canonical (## Phase N:), unnumbered H2s with phase-like content,
|
|
280
|
+
* and H3 phase headings (with warning).
|
|
281
|
+
* Returns null if the section is not a phase.
|
|
282
|
+
*/
|
|
283
|
+
function detectPhaseHeading(section, allSections, sectionIdx, autoNumber, ctx) {
|
|
284
|
+
if (section.level !== 2 && section.level !== 3)
|
|
285
|
+
return null;
|
|
286
|
+
const phaseMatch = tryMatchPhaseHeading(section.heading, autoNumber);
|
|
287
|
+
// Check for unnumbered H2s that look like phases
|
|
288
|
+
if (!phaseMatch && section.level === 2) {
|
|
289
|
+
if (KNOWN_SECTION_PATTERNS.some((p) => p.test(section.heading)))
|
|
290
|
+
return null;
|
|
291
|
+
const subSections = collectSectionsUntilLevel(allSections, sectionIdx, 2);
|
|
292
|
+
const hasChanges = subSections.some((s) => CHANGES_HEADING_PATTERN.test(s.heading));
|
|
293
|
+
const hasCriteria = subSections.some((s) => SUCCESS_CRITERIA_HEADING_PATTERN.test(s.heading) ||
|
|
294
|
+
/Automated\s+Verification/i.test(s.heading) ||
|
|
295
|
+
/Manual\s+Verification/i.test(s.heading));
|
|
296
|
+
const hasNumberedChanges = subSections.some((s) => (s.level === 4 || s.level === 3) && /^\d+\.\s/.test(s.heading));
|
|
297
|
+
if (hasChanges || hasCriteria || hasNumberedChanges) {
|
|
298
|
+
ctx.warn(`Unnumbered heading "${section.heading}" treated as Phase ${autoNumber}`);
|
|
299
|
+
return { number: autoNumber, name: section.heading, variant: "unnumbered" };
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
return phaseMatch;
|
|
304
|
+
}
|
|
305
|
+
// ── Phase content extraction ─────────────────────────────────────
|
|
306
|
+
function extractPhaseChanges(subSections, allSections, phaseNumber, ctx) {
|
|
307
|
+
const changesSection = subSections.find((s) => CHANGES_HEADING_PATTERN.test(s.heading));
|
|
308
|
+
if (!changesSection)
|
|
309
|
+
return [];
|
|
310
|
+
if (!/^Changes\s+Required/i.test(changesSection.heading)) {
|
|
311
|
+
ctx.warn(`Phase ${phaseNumber}: "${changesSection.heading}" used instead of "Changes Required" — accepted`);
|
|
312
|
+
}
|
|
313
|
+
const changesIdx = allSections.indexOf(changesSection);
|
|
314
|
+
if (changesIdx < 0)
|
|
315
|
+
return [];
|
|
316
|
+
// Strategy 1: Heading-based changes (#### N. Name)
|
|
317
|
+
const changeSubs = collectSectionsUntilLevel(allSections, changesIdx, changesSection.level);
|
|
318
|
+
const changes = parseChangesFromHeadings(changeSubs, ctx);
|
|
319
|
+
// Strategy 2: List-based changes if no heading-based changes found
|
|
320
|
+
if (changes.length === 0) {
|
|
321
|
+
return parseChangesFromList(changesSection.tokens, ctx);
|
|
322
|
+
}
|
|
323
|
+
return changes;
|
|
324
|
+
}
|
|
325
|
+
function extractPhaseCriteria(subSections, allSections) {
|
|
326
|
+
const criteriaSection = subSections.find((s) => SUCCESS_CRITERIA_HEADING_PATTERN.test(s.heading));
|
|
327
|
+
const criteriaIdx = criteriaSection
|
|
328
|
+
? allSections.indexOf(criteriaSection)
|
|
329
|
+
: -1;
|
|
330
|
+
const criteriaSubs = criteriaIdx >= 0 && criteriaSection
|
|
331
|
+
? collectSectionsUntilLevel(allSections, criteriaIdx, criteriaSection.level)
|
|
332
|
+
: [];
|
|
333
|
+
const automatedSection = criteriaSubs.find((s) => /Automated\s+Verification/i.test(s.heading)) ??
|
|
334
|
+
subSections.find((s) => /Automated\s+Verification/i.test(s.heading));
|
|
335
|
+
const manualSection = criteriaSubs.find((s) => /Manual\s+Verification/i.test(s.heading)) ??
|
|
336
|
+
subSections.find((s) => /Manual\s+Verification/i.test(s.heading));
|
|
337
|
+
const automated = automatedSection
|
|
338
|
+
? parseCriteria(automatedSection.tokens, "automated")
|
|
339
|
+
: [];
|
|
340
|
+
const manual = manualSection
|
|
341
|
+
? parseCriteria(manualSection.tokens, "manual")
|
|
342
|
+
: [];
|
|
343
|
+
// If criteria section exists but no automated/manual sub-sections,
|
|
344
|
+
// treat all list items as manual criteria
|
|
345
|
+
if (criteriaSection &&
|
|
346
|
+
automated.length === 0 &&
|
|
347
|
+
manual.length === 0 &&
|
|
348
|
+
criteriaSubs.length === 0) {
|
|
349
|
+
manual.push(...parseCriteria(criteriaSection.tokens, "manual"));
|
|
350
|
+
}
|
|
351
|
+
return { automated, manual };
|
|
352
|
+
}
|
|
353
|
+
function extractPhaseContentAndSubItems(phaseSection, subSections, phaseId, phaseNumber) {
|
|
354
|
+
const recognizedPattern = /^(Overview|Changes\s+Required|Changes|File\s+Changes|Modifications|Automated\s+Verification|Manual\s+Verification|Success\s+Criteria|Criteria|Verification):?$/i;
|
|
355
|
+
const subItemPattern = /^(\d+)([a-z])\.\s+(.+)/i;
|
|
356
|
+
const contentParts = [];
|
|
357
|
+
const subItems = [];
|
|
358
|
+
const directText = tokensToMarkdown(phaseSection.tokens);
|
|
359
|
+
if (directText)
|
|
360
|
+
contentParts.push(directText);
|
|
361
|
+
for (const sub of subSections) {
|
|
362
|
+
if (recognizedPattern.test(sub.heading))
|
|
363
|
+
continue;
|
|
364
|
+
const siMatch = sub.heading.match(subItemPattern);
|
|
365
|
+
if (siMatch && parseInt(siMatch[1], 10) === phaseNumber) {
|
|
366
|
+
const letter = siMatch[2].toLowerCase();
|
|
367
|
+
subItems.push({
|
|
368
|
+
id: `${phaseId}-${letter}`,
|
|
369
|
+
letter,
|
|
370
|
+
name: siMatch[3].trim(),
|
|
371
|
+
content: tokensToMarkdown(sub.tokens),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
const subBody = tokensToMarkdown(sub.tokens);
|
|
376
|
+
contentParts.push(`### ${sub.heading}\n\n${subBody}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const content = contentParts.join("\n\n").trim() || undefined;
|
|
380
|
+
return { content, subItems };
|
|
381
|
+
}
|
|
382
|
+
// ── Main phase parser ────────────────────────────────────────────
|
|
277
383
|
export function parsePhases(allSections, ctx) {
|
|
278
384
|
const phases = [];
|
|
279
385
|
let autoNumber = 1;
|
|
280
386
|
for (let i = 0; i < allSections.length; i++) {
|
|
281
387
|
const section = allSections[i];
|
|
282
|
-
|
|
283
|
-
if (
|
|
388
|
+
const detected = detectPhaseHeading(section, allSections, i, autoNumber, ctx);
|
|
389
|
+
if (!detected)
|
|
284
390
|
continue;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
let isUnnumberedPhase = false;
|
|
288
|
-
if (!phaseMatch && section.level === 2) {
|
|
289
|
-
// Skip known section patterns
|
|
290
|
-
if (KNOWN_SECTION_PATTERNS.some((p) => p.test(section.heading)))
|
|
291
|
-
continue;
|
|
292
|
-
// Check if this H2 has subsections that look like phase content
|
|
293
|
-
// (has Changes Required, or Success Criteria, or numbered sub-headings)
|
|
294
|
-
const subSections = collectSectionsUntilLevel(allSections, i, 2);
|
|
295
|
-
const hasChanges = subSections.some((s) => CHANGES_HEADING_PATTERN.test(s.heading));
|
|
296
|
-
const hasCriteria = subSections.some((s) => SUCCESS_CRITERIA_HEADING_PATTERN.test(s.heading) ||
|
|
297
|
-
/Automated\s+Verification/i.test(s.heading) ||
|
|
298
|
-
/Manual\s+Verification/i.test(s.heading));
|
|
299
|
-
const hasNumberedChanges = subSections.some((s) => (s.level === 4 || s.level === 3) && /^\d+\.\s/.test(s.heading));
|
|
300
|
-
if (hasChanges || hasCriteria || hasNumberedChanges) {
|
|
301
|
-
isUnnumberedPhase = true;
|
|
302
|
-
}
|
|
303
|
-
else {
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
391
|
+
if (detected.variant !== "canonical" && detected.variant !== "unnumbered") {
|
|
392
|
+
ctx.warn(`Phase "${section.heading}" uses non-canonical format (${detected.variant}) — accepted`);
|
|
306
393
|
}
|
|
307
|
-
|
|
308
|
-
// H3 with phase-like heading
|
|
309
|
-
if (section.level === 3) {
|
|
310
|
-
const h3Match = tryMatchPhaseHeading(section.heading, autoNumber);
|
|
311
|
-
if (h3Match) {
|
|
312
|
-
ctx.warn(`Phase "${section.heading}" uses h3 instead of h2 — accepted but non-canonical`);
|
|
313
|
-
// Fall through with h3Match
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
const match = phaseMatch ?? {
|
|
324
|
-
number: autoNumber,
|
|
325
|
-
name: section.heading,
|
|
326
|
-
variant: "unnumbered",
|
|
327
|
-
};
|
|
328
|
-
if (match.variant !== "canonical" && match.variant !== "unnumbered") {
|
|
329
|
-
ctx.warn(`Phase "${section.heading}" uses non-canonical format (${match.variant}) — accepted`);
|
|
330
|
-
}
|
|
331
|
-
if (match.variant === "unnumbered") {
|
|
332
|
-
ctx.warn(`Unnumbered heading "${section.heading}" treated as Phase ${match.number}`);
|
|
333
|
-
}
|
|
334
|
-
const number = match.number;
|
|
335
|
-
const name = match.name;
|
|
394
|
+
const { number, name } = detected;
|
|
336
395
|
const id = `phase-${number}`;
|
|
337
396
|
autoNumber = number + 1;
|
|
338
397
|
const subSections = collectSectionsUntilLevel(allSections, i, section.level);
|
|
339
398
|
const overviewSection = subSections.find((s) => /^Overview$/i.test(s.heading));
|
|
340
|
-
const overview = overviewSection
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const changesSection = subSections.find((s) => CHANGES_HEADING_PATTERN.test(s.heading));
|
|
345
|
-
if (changesSection &&
|
|
346
|
-
!/^Changes\s+Required/i.test(changesSection.heading)) {
|
|
347
|
-
ctx.warn(`Phase ${number}: "${changesSection.heading}" used instead of "Changes Required" — accepted`);
|
|
348
|
-
}
|
|
349
|
-
const changesIdx = changesSection
|
|
350
|
-
? allSections.indexOf(changesSection)
|
|
351
|
-
: -1;
|
|
352
|
-
let changes = [];
|
|
353
|
-
if (changesIdx >= 0 && changesSection) {
|
|
354
|
-
// Strategy 1: Heading-based changes (#### N. Name)
|
|
355
|
-
const changeSubs = collectSectionsUntilLevel(allSections, changesIdx, changesSection.level);
|
|
356
|
-
changes = parseChangesFromHeadings(changeSubs, ctx);
|
|
357
|
-
// Strategy 2: List-based changes if no heading-based changes found
|
|
358
|
-
if (changes.length === 0) {
|
|
359
|
-
changes = parseChangesFromList(changesSection.tokens, ctx);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
// Flexible success criteria heading
|
|
363
|
-
const criteriaSection = subSections.find((s) => SUCCESS_CRITERIA_HEADING_PATTERN.test(s.heading));
|
|
364
|
-
const criteriaIdx = criteriaSection
|
|
365
|
-
? allSections.indexOf(criteriaSection)
|
|
366
|
-
: -1;
|
|
367
|
-
const criteriaSubs = criteriaIdx >= 0 && criteriaSection
|
|
368
|
-
? collectSectionsUntilLevel(allSections, criteriaIdx, criteriaSection.level)
|
|
369
|
-
: [];
|
|
370
|
-
const automatedSection = criteriaSubs.find((s) => /Automated\s+Verification/i.test(s.heading)) ??
|
|
371
|
-
subSections.find((s) => /Automated\s+Verification/i.test(s.heading));
|
|
372
|
-
const manualSection = criteriaSubs.find((s) => /Manual\s+Verification/i.test(s.heading)) ??
|
|
373
|
-
subSections.find((s) => /Manual\s+Verification/i.test(s.heading));
|
|
374
|
-
const automated = automatedSection
|
|
375
|
-
? parseCriteria(automatedSection.tokens, "automated")
|
|
376
|
-
: [];
|
|
377
|
-
const manual = manualSection
|
|
378
|
-
? parseCriteria(manualSection.tokens, "manual")
|
|
379
|
-
: [];
|
|
380
|
-
// If criteria section exists but no automated/manual sub-sections,
|
|
381
|
-
// treat all list items as manual criteria
|
|
382
|
-
if (criteriaSection &&
|
|
383
|
-
automated.length === 0 &&
|
|
384
|
-
manual.length === 0 &&
|
|
385
|
-
criteriaSubs.length === 0) {
|
|
386
|
-
manual.push(...parseCriteria(criteriaSection.tokens, "manual"));
|
|
387
|
-
}
|
|
388
|
-
// Build content from direct tokens + unrecognized subsections
|
|
389
|
-
const recognizedPattern = /^(Overview|Changes\s+Required|Changes|File\s+Changes|Modifications|Automated\s+Verification|Manual\s+Verification|Success\s+Criteria|Criteria|Verification):?$/i;
|
|
390
|
-
const subItemPattern = /^(\d+)([a-z])\.\s+(.+)/i;
|
|
391
|
-
const contentParts = [];
|
|
392
|
-
const subItems = [];
|
|
393
|
-
// Direct tokens between phase heading and first sub-heading
|
|
394
|
-
const phaseSection = allSections[i];
|
|
395
|
-
const directText = tokensToMarkdown(phaseSection.tokens);
|
|
396
|
-
if (directText)
|
|
397
|
-
contentParts.push(directText);
|
|
398
|
-
// Unrecognized subsections — check for sub-items first
|
|
399
|
-
for (const sub of subSections) {
|
|
400
|
-
if (recognizedPattern.test(sub.heading))
|
|
401
|
-
continue;
|
|
402
|
-
const siMatch = sub.heading.match(subItemPattern);
|
|
403
|
-
if (siMatch && parseInt(siMatch[1], 10) === number) {
|
|
404
|
-
const letter = siMatch[2].toLowerCase();
|
|
405
|
-
subItems.push({
|
|
406
|
-
id: `${id}-${letter}`,
|
|
407
|
-
letter,
|
|
408
|
-
name: siMatch[3].trim(),
|
|
409
|
-
content: tokensToMarkdown(sub.tokens),
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
else {
|
|
413
|
-
const subBody = tokensToMarkdown(sub.tokens);
|
|
414
|
-
contentParts.push(`### ${sub.heading}\n\n${subBody}`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
const content = contentParts.join("\n\n").trim() || undefined;
|
|
399
|
+
const overview = overviewSection ? tokensToMarkdown(overviewSection.tokens) : "";
|
|
400
|
+
const changes = extractPhaseChanges(subSections, allSections, number, ctx);
|
|
401
|
+
const successCriteria = extractPhaseCriteria(subSections, allSections);
|
|
402
|
+
const { content, subItems } = extractPhaseContentAndSubItems(allSections[i], subSections, id, number);
|
|
418
403
|
phases.push({
|
|
419
404
|
id,
|
|
420
405
|
number,
|
|
@@ -423,7 +408,7 @@ export function parsePhases(allSections, ctx) {
|
|
|
423
408
|
content,
|
|
424
409
|
subItems,
|
|
425
410
|
changes,
|
|
426
|
-
successCriteria
|
|
411
|
+
successCriteria,
|
|
427
412
|
});
|
|
428
413
|
}
|
|
429
414
|
return phases;
|
|
@@ -2,7 +2,7 @@ import { Lexer } from "marked";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { generatePhaseFlowDiagram } from "./mermaid-gen.js";
|
|
5
|
-
import { createParseContext, splitIntoSections, tokensToMarkdown, findSection, collectSectionsUntilLevel, parseKeyDiscoveries, parseScopeExclusions, parsePhases, parseTestingStrategy, parseReferences,
|
|
5
|
+
import { createParseContext, splitIntoSections, tokensToMarkdown, findSection, collectSectionsUntilLevel, parseKeyDiscoveries, parseScopeExclusions, parsePhases, parseTestingStrategy, parseReferences, } from "./markdown-parser.js";
|
|
6
6
|
export function sessionIdFromPath(absolutePath) {
|
|
7
7
|
return createHash("sha256").update(absolutePath).digest("hex").slice(0, 8);
|
|
8
8
|
}
|
|
@@ -77,54 +77,6 @@ export function parseMarkdownToPlan(markdown, markdownPath, projectDir, version
|
|
|
77
77
|
// References
|
|
78
78
|
const refsSection = findSection(sections, /^References$/i, 2);
|
|
79
79
|
const references = refsSection ? parseReferences(refsSection.tokens) : [];
|
|
80
|
-
// Collect additional (unrecognized) H2 sections
|
|
81
|
-
const knownH2Indices = new Set();
|
|
82
|
-
const knownPatterns = [
|
|
83
|
-
/^Overview$/i,
|
|
84
|
-
/^Current\s+State/i,
|
|
85
|
-
/What\s+We.*NOT\s+Doing/i,
|
|
86
|
-
/Implementation\s+Approach/i,
|
|
87
|
-
/Testing\s+Strategy/i,
|
|
88
|
-
/^References$/i,
|
|
89
|
-
];
|
|
90
|
-
for (let i = 0; i < sections.length; i++) {
|
|
91
|
-
if (sections[i].level === 1) {
|
|
92
|
-
knownH2Indices.add(i);
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (sections[i].level !== 2)
|
|
96
|
-
continue;
|
|
97
|
-
// Check if it's a known section
|
|
98
|
-
if (knownPatterns.some((p) => p.test(sections[i].heading))) {
|
|
99
|
-
knownH2Indices.add(i);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
// Check if it was parsed as a phase
|
|
103
|
-
if (phases.some((p) => p.name === sections[i].heading ||
|
|
104
|
-
tryMatchPhaseHeading(sections[i].heading, 0))) {
|
|
105
|
-
knownH2Indices.add(i);
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const additionalSections = [];
|
|
110
|
-
for (let i = 0; i < sections.length; i++) {
|
|
111
|
-
if (sections[i].level !== 2 || knownH2Indices.has(i))
|
|
112
|
-
continue;
|
|
113
|
-
// Double-check it's not a parsed phase
|
|
114
|
-
const isPhase = phases.some((p) => p.name === sections[i].heading ||
|
|
115
|
-
sections[i].heading.match(new RegExp(`Phase\\s+${p.number}`, "i")));
|
|
116
|
-
if (isPhase)
|
|
117
|
-
continue;
|
|
118
|
-
const subs = collectSectionsUntilLevel(sections, i, 2);
|
|
119
|
-
let content = tokensToMarkdown(sections[i].tokens);
|
|
120
|
-
for (const sub of subs) {
|
|
121
|
-
content += `\n\n${"#".repeat(sub.level)} ${sub.heading}\n\n${tokensToMarkdown(sub.tokens)}`;
|
|
122
|
-
}
|
|
123
|
-
additionalSections.push({
|
|
124
|
-
heading: sections[i].heading,
|
|
125
|
-
content: content.trim(),
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
80
|
const now = new Date().toISOString();
|
|
129
81
|
const plan = {
|
|
130
82
|
schemaVersion: 1,
|
|
@@ -147,7 +99,6 @@ export function parseMarkdownToPlan(markdown, markdownPath, projectDir, version
|
|
|
147
99
|
diagrams,
|
|
148
100
|
testingStrategy,
|
|
149
101
|
references,
|
|
150
|
-
...(additionalSections.length > 0 ? { additionalSections } : {}),
|
|
151
102
|
};
|
|
152
103
|
return { plan, warnings: ctx.warnings };
|
|
153
104
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session setup for the review command.
|
|
3
|
+
* Extracted from review.ts to reduce god-function complexity.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, } from "node:fs";
|
|
6
|
+
import { resolve, dirname, join } from "node:path";
|
|
7
|
+
import { watch } from "chokidar";
|
|
8
|
+
import { parseMarkdownToPlan, sessionIdFromPath, } from "./markdown-to-plan.js";
|
|
9
|
+
import { CliError } from "./errors.js";
|
|
10
|
+
/**
|
|
11
|
+
* Parse a markdown file, create/update the session directory, and write session files.
|
|
12
|
+
* Returns all the context needed by the rest of the review command.
|
|
13
|
+
*/
|
|
14
|
+
export function prepareSession(markdownFile) {
|
|
15
|
+
const absolutePath = resolve(markdownFile);
|
|
16
|
+
if (!existsSync(absolutePath)) {
|
|
17
|
+
throw new CliError(`File not found: ${absolutePath}`);
|
|
18
|
+
}
|
|
19
|
+
const sessionDir = join(dirname(absolutePath), ".plan-sessions");
|
|
20
|
+
const sessionId = sessionIdFromPath(absolutePath);
|
|
21
|
+
const sessionPath = join(sessionDir, sessionId);
|
|
22
|
+
const projectDir = process.cwd();
|
|
23
|
+
// Determine version
|
|
24
|
+
let version = 1;
|
|
25
|
+
const existingPlanPath = join(sessionPath, "plan.json");
|
|
26
|
+
if (existsSync(existingPlanPath)) {
|
|
27
|
+
try {
|
|
28
|
+
const existing = JSON.parse(readFileSync(existingPlanPath, "utf-8"));
|
|
29
|
+
version = (existing.meta?.version ?? 0) + 1;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// ignore, start at 1
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Parse markdown
|
|
36
|
+
const markdown = readFileSync(absolutePath, "utf-8");
|
|
37
|
+
const { plan, warnings } = parseMarkdownToPlan(markdown, absolutePath, projectDir, version);
|
|
38
|
+
// Display parser warnings
|
|
39
|
+
for (const warning of warnings) {
|
|
40
|
+
console.error(`Warning: ${warning}`);
|
|
41
|
+
}
|
|
42
|
+
if (plan.phases.length === 0) {
|
|
43
|
+
console.error(`
|
|
44
|
+
⚠ No phases found — plan will appear empty in the browser.
|
|
45
|
+
|
|
46
|
+
Use the correct format for phases:
|
|
47
|
+
|
|
48
|
+
## Phase 1: Phase Name
|
|
49
|
+
|
|
50
|
+
### Changes Required:
|
|
51
|
+
|
|
52
|
+
#### 1. Component Name
|
|
53
|
+
**File**: \`path/to/file.ext\`
|
|
54
|
+
Description of what to change.
|
|
55
|
+
|
|
56
|
+
### Success Criteria:
|
|
57
|
+
- [ ] \`npm test\`
|
|
58
|
+
|
|
59
|
+
Accepted phase keywords: "Phase N:", "Step N:", "Task N:" (H2 headings)
|
|
60
|
+
Run \`npx plan-assistant init --output <file>\` to generate a correctly-formatted template.
|
|
61
|
+
`);
|
|
62
|
+
}
|
|
63
|
+
// Write session files
|
|
64
|
+
mkdirSync(sessionPath, { recursive: true });
|
|
65
|
+
mkdirSync(join(sessionPath, "versions"), { recursive: true });
|
|
66
|
+
// Clear stale feedback from previous review cycle
|
|
67
|
+
const oldFeedbackPath = join(sessionPath, "feedback.json");
|
|
68
|
+
if (existsSync(oldFeedbackPath)) {
|
|
69
|
+
try {
|
|
70
|
+
unlinkSync(oldFeedbackPath);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const meta = {
|
|
77
|
+
id: sessionId,
|
|
78
|
+
planTitle: plan.meta.title,
|
|
79
|
+
markdownPath: absolutePath,
|
|
80
|
+
projectDir,
|
|
81
|
+
status: "active",
|
|
82
|
+
planVersion: version,
|
|
83
|
+
createdAt: plan.meta.createdAt,
|
|
84
|
+
updatedAt: plan.meta.updatedAt,
|
|
85
|
+
};
|
|
86
|
+
writeFileSync(join(sessionPath, "meta.json"), JSON.stringify(meta, null, 2));
|
|
87
|
+
writeFileSync(join(sessionPath, "plan.json"), JSON.stringify(plan, null, 2));
|
|
88
|
+
writeFileSync(join(sessionPath, "versions", `v${version}.json`), JSON.stringify(plan, null, 2));
|
|
89
|
+
return {
|
|
90
|
+
sessionId,
|
|
91
|
+
sessionPath,
|
|
92
|
+
sessionDir,
|
|
93
|
+
plan,
|
|
94
|
+
version,
|
|
95
|
+
absolutePath,
|
|
96
|
+
projectDir,
|
|
97
|
+
existingPlanPath,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create a chokidar watcher on the markdown file that re-parses and
|
|
102
|
+
* updates session files on change.
|
|
103
|
+
*/
|
|
104
|
+
export function watchMarkdownFile(absolutePath, sessionPath, projectDir, existingPlanPath, ensureServer) {
|
|
105
|
+
const mdWatcher = watch(absolutePath, {
|
|
106
|
+
awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 },
|
|
107
|
+
});
|
|
108
|
+
mdWatcher.on("change", async () => {
|
|
109
|
+
try {
|
|
110
|
+
await ensureServer();
|
|
111
|
+
const updated = readFileSync(absolutePath, "utf-8");
|
|
112
|
+
const existingPlan = JSON.parse(readFileSync(existingPlanPath, "utf-8"));
|
|
113
|
+
const newVersion = (existingPlan.meta?.version ?? 0) + 1;
|
|
114
|
+
const { plan: newPlan, warnings: newWarnings } = parseMarkdownToPlan(updated, absolutePath, projectDir, newVersion);
|
|
115
|
+
for (const warning of newWarnings) {
|
|
116
|
+
console.error(`Warning: ${warning}`);
|
|
117
|
+
}
|
|
118
|
+
if (newPlan.phases.length === 0) {
|
|
119
|
+
console.error(`⚠ No phases found after reload — check format. Run \`npx plan-assistant init\` for a template.`);
|
|
120
|
+
}
|
|
121
|
+
writeFileSync(join(sessionPath, "plan.json"), JSON.stringify(newPlan, null, 2));
|
|
122
|
+
writeFileSync(join(sessionPath, "versions", `v${newVersion}.json`), JSON.stringify(newPlan, null, 2));
|
|
123
|
+
console.error(`[${new Date().toLocaleTimeString()}] Plan updated (v${newVersion})`);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
console.error(`Error re-parsing markdown: ${err}`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return mdWatcher;
|
|
130
|
+
}
|