@difflab/pi 0.3.0-rc.202609200047.ac3661a → 0.3.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 (61) hide show
  1. package/README.md +2 -22
  2. package/agents/diffpi-orchestrator.md +0 -10
  3. package/agents/diffpi-worker.md +1 -9
  4. package/dist/commands/index.d.ts +0 -1
  5. package/dist/commands/index.d.ts.map +1 -1
  6. package/dist/commands/review.d.ts.map +1 -1
  7. package/dist/environment.d.ts.map +1 -1
  8. package/dist/extensions/index.js +844 -2773
  9. package/dist/extensions/zedx.d.ts +0 -2
  10. package/dist/extensions/zedx.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -5
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +128 -1140
  14. package/dist/setup.d.ts.map +1 -1
  15. package/dist/store.d.ts +0 -1
  16. package/dist/store.d.ts.map +1 -1
  17. package/dist/tools/index.d.ts +0 -1
  18. package/dist/tools/index.d.ts.map +1 -1
  19. package/dist/tools/index.js +678 -2400
  20. package/package.json +1 -4
  21. package/agents/diffpi-planner.md +0 -30
  22. package/dist/cli/plan.d.ts +0 -7
  23. package/dist/cli/plan.d.ts.map +0 -1
  24. package/dist/cli.d.ts +0 -3
  25. package/dist/cli.d.ts.map +0 -1
  26. package/dist/cli.js +0 -729
  27. package/dist/commands/background.d.ts +0 -39
  28. package/dist/commands/background.d.ts.map +0 -1
  29. package/dist/commands/plan.d.ts +0 -21
  30. package/dist/commands/plan.d.ts.map +0 -1
  31. package/dist/plan/annotations.d.ts +0 -25
  32. package/dist/plan/annotations.d.ts.map +0 -1
  33. package/dist/plan/escalation.d.ts +0 -4
  34. package/dist/plan/escalation.d.ts.map +0 -1
  35. package/dist/plan/execution.d.ts +0 -19
  36. package/dist/plan/execution.d.ts.map +0 -1
  37. package/dist/plan/index.d.ts +0 -15
  38. package/dist/plan/index.d.ts.map +0 -1
  39. package/dist/plan/lock.d.ts +0 -7
  40. package/dist/plan/lock.d.ts.map +0 -1
  41. package/dist/plan/log.d.ts +0 -5
  42. package/dist/plan/log.d.ts.map +0 -1
  43. package/dist/plan/markdown.d.ts +0 -6
  44. package/dist/plan/markdown.d.ts.map +0 -1
  45. package/dist/plan/store.d.ts +0 -34
  46. package/dist/plan/store.d.ts.map +0 -1
  47. package/dist/plan/transitions.d.ts +0 -9
  48. package/dist/plan/transitions.d.ts.map +0 -1
  49. package/dist/plan/types.d.ts +0 -146
  50. package/dist/plan/types.d.ts.map +0 -1
  51. package/dist/tools/plan.d.ts +0 -6
  52. package/dist/tools/plan.d.ts.map +0 -1
  53. package/skills/plan/SKILL.md +0 -13
  54. package/skills/plan/references/workflows/annotate.md +0 -6
  55. package/skills/plan/references/workflows/finalize.md +0 -7
  56. package/skills/plan/references/workflows/go.md +0 -7
  57. package/skills/plan/references/workflows/help.md +0 -13
  58. package/skills/plan/references/workflows/init.md +0 -6
  59. package/skills/plan/references/workflows/new.md +0 -7
  60. package/skills/plan/references/workflows/update.md +0 -7
  61. package/templates/plan/PLAN.md +0 -38
package/dist/cli.js DELETED
@@ -1,729 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { pathToFileURL } from "node:url";
5
-
6
- // src/cli/plan.ts
7
- import { resolve as resolve4 } from "node:path";
8
-
9
- // src/plan/annotations.ts
10
- import { spawn as spawn2 } from "node:child_process";
11
- import { readFile as readFile2, rename, writeFile as writeFile2 } from "node:fs/promises";
12
- import { basename, dirname, join as join2 } from "node:path";
13
-
14
- // src/extensions/processx.ts
15
- import { spawn } from "node:child_process";
16
- function run(command, args, options = {}) {
17
- return new Promise((resolve, reject) => {
18
- const child = spawn(command, args, {
19
- cwd: options.cwd,
20
- env: options.env ?? process.env,
21
- stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "pipe"]
22
- });
23
- let stdout = "";
24
- let stderr = "";
25
- const stdoutChunks = [];
26
- const stderrChunks = [];
27
- const unbounded = options.capture === "unbounded";
28
- child.stdout?.on("data", (chunk) => {
29
- const text = chunk.toString();
30
- if (unbounded)
31
- stdoutChunks.push(text);
32
- else
33
- stdout = appendBounded(stdout, text);
34
- });
35
- child.stderr?.on("data", (chunk) => {
36
- const text = chunk.toString();
37
- if (unbounded)
38
- stderrChunks.push(text);
39
- else
40
- stderr = appendBounded(stderr, text);
41
- });
42
- child.on("error", reject);
43
- child.on("close", (code) => resolve({
44
- code: code ?? 1,
45
- stdout: unbounded ? stdoutChunks.join("") : stdout,
46
- stderr: unbounded ? stderrChunks.join("") : stderr
47
- }));
48
- if (options.input !== undefined && child.stdin)
49
- child.stdin.end(options.input);
50
- });
51
- }
52
- async function runChecked(command, args, options = {}) {
53
- const result = await run(command, args, options);
54
- if (result.code === 0)
55
- return result;
56
- const detail = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`;
57
- throw new Error(`${command} ${args.join(" ")} failed: ${detail}`);
58
- }
59
- var MAX_CAPTURED_OUTPUT_LENGTH = 65536;
60
- function appendBounded(current, next) {
61
- const combined = current + next;
62
- return combined.length <= MAX_CAPTURED_OUTPUT_LENGTH ? combined : combined.slice(-MAX_CAPTURED_OUTPUT_LENGTH);
63
- }
64
-
65
- // src/plan/lock.ts
66
- import { randomUUID } from "node:crypto";
67
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
68
- import { hostname } from "node:os";
69
- import { join } from "node:path";
70
- async function withPlanLock(planDir, operation, options = {}) {
71
- const lockDir = join(planDir, ".lock");
72
- const waitMs = options.waitMs ?? 5000;
73
- const pollMs = options.pollMs ?? 25;
74
- const owner = {
75
- token: randomUUID(),
76
- pid: process.pid,
77
- hostname: hostname(),
78
- operation: options.operation ?? "plan mutation",
79
- acquiredAt: new Date().toISOString()
80
- };
81
- const started = Date.now();
82
- while (true) {
83
- try {
84
- await mkdir(lockDir);
85
- await writeFile(join(lockDir, "owner.json"), `${JSON.stringify(owner)}
86
- `, { mode: 384 });
87
- break;
88
- } catch (error) {
89
- if (error.code !== "EEXIST")
90
- throw error;
91
- if (Date.now() - started >= waitMs) {
92
- let existing = "unknown owner";
93
- try {
94
- existing = (await readFile(join(lockDir, "owner.json"), "utf8")).trim();
95
- } catch {}
96
- throw new Error(`Timed out waiting for plan lock ${lockDir}; owner: ${existing}. Remove it only after confirming the owner is stale.`);
97
- }
98
- await new Promise((resolve) => setTimeout(resolve, pollMs));
99
- }
100
- }
101
- try {
102
- return await operation();
103
- } finally {
104
- let current;
105
- try {
106
- current = JSON.parse(await readFile(join(lockDir, "owner.json"), "utf8"));
107
- } catch {}
108
- if (current?.token === owner.token)
109
- await rm(lockDir, { recursive: true, force: true });
110
- }
111
- }
112
-
113
- // src/plan/annotations.ts
114
- async function annotatePlan(record, runtime = {}) {
115
- const execute = runtime.execute ?? executeProcess;
116
- const result = await execute("tuicr", ["--file", record.planPath], dirname(record.planPath), true);
117
- const sessionSlug = [...result.stderr.matchAll(/^tuicr-session:\s*(\S+)\s*$/gm)].at(-1)?.[1];
118
- if (result.code !== 0)
119
- throw new Error(result.stderr.trim() || `tuicr exited with status ${result.code}.`);
120
- if (!sessionSlug)
121
- throw new Error("tuicr did not report a tuicr-session marker.");
122
- const state = await withPlanLock(record.dir, async () => {
123
- const previous = await readAnnotationState(record).catch(() => {
124
- return;
125
- });
126
- const next = {
127
- schemaVersion: 1,
128
- sessionSlug,
129
- updatedAt: new Date().toISOString(),
130
- appliedCommentIds: previous?.appliedCommentIds ?? []
131
- };
132
- await atomicJson(annotationStatePath(record), next);
133
- return next;
134
- }, { operation: "save annotation session" });
135
- return { sessionSlug, code: result.code, state };
136
- }
137
- async function readPlanAnnotations(record, options = {}) {
138
- const state = await readAnnotationState(record).catch((error) => {
139
- if (error.code === "ENOENT")
140
- return;
141
- throw error;
142
- });
143
- if (!state)
144
- return { comments: [], pending: [] };
145
- const execute = options.runtime?.execute ?? executeProcess;
146
- const response = await execute("tuicr", ["review", "comments", "--session", state.sessionSlug], record.dir, false);
147
- if (response.code !== 0) {
148
- if (/not found|no session|deleted/i.test(response.stderr))
149
- return { state, comments: [], pending: [] };
150
- throw new Error(response.stderr.trim() || "Cannot read tuicr comments.");
151
- }
152
- let raw;
153
- try {
154
- raw = JSON.parse(response.stdout);
155
- } catch {
156
- throw new Error("tuicr returned malformed comment JSON.");
157
- }
158
- if (!Array.isArray(raw))
159
- throw new Error("tuicr comment output must be a JSON array.");
160
- const lines = record.source.split(`
161
- `);
162
- const applied = new Set(state.appliedCommentIds);
163
- const comments = raw.map((value, index) => normalizeComment(value, index, lines, applied));
164
- const pending = comments.filter((comment) => !comment.applied);
165
- return { state, comments: options.includeApplied ? comments : pending, pending };
166
- }
167
- function annotationStatePath(record) {
168
- return join2(record.dir, "annotations.json");
169
- }
170
- async function readAnnotationState(record) {
171
- const value = JSON.parse(await readFile2(annotationStatePath(record), "utf8"));
172
- if (value.schemaVersion !== 1 || typeof value.sessionSlug !== "string" || !Array.isArray(value.appliedCommentIds)) {
173
- throw new Error(`Malformed annotation state: ${annotationStatePath(record)}.`);
174
- }
175
- return value;
176
- }
177
- function normalizeComment(value, index, lines, applied) {
178
- if (!value || typeof value !== "object")
179
- throw new Error(`Malformed tuicr comment at index ${index}.`);
180
- const raw = value;
181
- if (typeof raw.id !== "string" || typeof raw.content !== "string")
182
- throw new Error(`Malformed tuicr comment at index ${index}.`);
183
- const line = integer(raw.start_line) ?? integer(raw.line);
184
- const endLine = integer(raw.end_line) ?? line;
185
- const targetPath = typeof raw.path === "string" ? raw.path : undefined;
186
- const appliesToPlan = !targetPath || basename(targetPath) === basename("PLAN.md");
187
- const validAnchor = appliesToPlan && line !== undefined && line > 0 && line <= lines.length;
188
- return {
189
- id: raw.id,
190
- body: raw.content,
191
- file: targetPath,
192
- line,
193
- endLine,
194
- context: validAnchor ? lines.slice(line - 1, Math.min(endLine ?? line, lines.length)).join(`
195
- `) : undefined,
196
- stale: line !== undefined && !validAnchor,
197
- applied: applied.has(raw.id)
198
- };
199
- }
200
- async function executeProcess(command, args, cwd, interactive) {
201
- if (!interactive) {
202
- const result = await run(command, args, { cwd, capture: "unbounded" });
203
- return { code: result.code, stdout: result.stdout, stderr: result.stderr };
204
- }
205
- return new Promise((resolve, reject) => {
206
- const child = spawn2(command, args, { cwd, stdio: ["inherit", "inherit", "pipe"] });
207
- let stderr = "";
208
- child.stderr.on("data", (chunk) => {
209
- const text = chunk.toString();
210
- stderr += text;
211
- process.stderr.write(text);
212
- });
213
- child.on("error", reject);
214
- child.on("close", (code) => resolve({ code: code ?? 1, stdout: "", stderr }));
215
- });
216
- }
217
- async function atomicJson(path, value) {
218
- const temp = join2(dirname(path), `.${basename(path)}.${crypto.randomUUID()}.tmp`);
219
- await writeFile2(temp, `${JSON.stringify(value, null, 2)}
220
- `, { mode: 384, flag: "wx" });
221
- await rename(temp, path);
222
- }
223
- function integer(value) {
224
- return typeof value === "number" && Number.isInteger(value) ? value : undefined;
225
- }
226
- // src/plan/escalation.ts
227
- import { z } from "zod";
228
- var escalationSchema = z.object({
229
- planId: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
230
- executionId: z.string().min(1),
231
- phaseId: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
232
- taskId: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).optional(),
233
- blocker: z.string().min(1),
234
- attempts: z.array(z.string()),
235
- evidence: z.array(z.string()),
236
- needsUserDecision: z.boolean()
237
- }).strict();
238
- // src/plan/transitions.ts
239
- var STABLE_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
240
- function isStableId(value) {
241
- return STABLE_ID.test(value) && value.length <= 80;
242
- }
243
- function assertStableId(value, label = "identifier") {
244
- if (!isStableId(value))
245
- throw new Error(`${label} must be a lowercase stable slug, not a path: ${value}`);
246
- }
247
- function assertUniqueIds(ids, label) {
248
- const seen = new Set;
249
- for (const id of ids) {
250
- assertStableId(id, label);
251
- if (seen.has(id))
252
- throw new Error(`Duplicate ${label}: ${id}.`);
253
- seen.add(id);
254
- }
255
- }
256
-
257
- // src/plan/markdown.ts
258
- var PLAN_MARKER = /<!-- diffpi-plan: (\{[^\n]+\}) -->/;
259
- var PHASE_MARKER = /<!-- diffpi-phase: (\{[^\n]+\}) -->/g;
260
- var TASK_MARKER = /<!-- diffpi-task: (\{[^\n]+\}) -->/g;
261
- var SUPPORTED_SCHEMA = 1;
262
- function countDesignWords(plan) {
263
- return [plan.design.bigIdeas, plan.design.keyApiUpdates, plan.design.consequences].join(" ").trim().split(/\s+/).filter(Boolean).length;
264
- }
265
- function validatePlanDocument(plan, options = {}) {
266
- const issues = [];
267
- const add = (code, severity, message, path) => issues.push({ code, severity, message, path });
268
- try {
269
- assertStableId(plan.id, "plan id");
270
- assertUniqueIds(plan.phases.map((phase) => phase.id), "phase id");
271
- assertUniqueIds(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)), "task id");
272
- } catch (error) {
273
- add("invalid-id", "error", error.message);
274
- }
275
- if (plan.schemaVersion !== SUPPORTED_SCHEMA)
276
- add("schema", "error", `Unsupported plan schema: ${plan.schemaVersion}.`);
277
- const phaseIds = new Set(plan.phases.map((phase) => phase.id));
278
- const taskIds = new Set(plan.phases.flatMap((phase) => phase.tasks.map((task) => task.id)));
279
- for (const phase of plan.phases) {
280
- for (const dependency of phase.dependencies) {
281
- if (!phaseIds.has(dependency))
282
- add("dangling-dependency", "error", `Phase ${phase.id} depends on missing ${dependency}.`);
283
- }
284
- for (const task of phase.tasks) {
285
- for (const dependency of task.dependencies) {
286
- if (!taskIds.has(dependency))
287
- add("dangling-dependency", "error", `Task ${task.id} depends on missing ${dependency}.`);
288
- }
289
- if (options.strict && task.acceptanceCriteria.length === 0)
290
- add("acceptance", "error", `Task ${task.id} has no acceptance criteria.`, task.id);
291
- }
292
- if (options.strict && phase.tasks.length === 0)
293
- add("empty-phase", "error", `Phase ${phase.id} has no tasks.`, phase.id);
294
- }
295
- for (const cycle of dependencyCycles(plan))
296
- add("dependency-cycle", "error", `Dependency cycle: ${cycle.join(" -> ")}.`);
297
- const designWords = countDesignWords(plan);
298
- if (designWords > 300)
299
- add("design-length", "warning", `Design is ${designWords} words; prefer 300 or fewer.`);
300
- if (options.strict && designWords > 800)
301
- add("design-too-long", "error", `Design is ${designWords} words; finalization allows at most 800.`);
302
- if (options.strict && plan.phases.length === 0)
303
- add("no-phases", "error", "Finalization requires at least one phase.");
304
- if (options.strict && (options.pendingAnnotations ?? 0) > 0)
305
- add("pending-annotations", "error", `${options.pendingAnnotations} annotations remain pending.`);
306
- return issues;
307
- }
308
- function parsePlanDocument(source, ref = "PLAN.md") {
309
- const planMatch = source.match(PLAN_MARKER);
310
- if (!planMatch)
311
- throw new Error(`${ref}: missing or malformed diffpi-plan marker.`);
312
- if ((source.match(new RegExp(PLAN_MARKER.source, "g")) ?? []).length !== 1)
313
- throw new Error(`${ref}: duplicate diffpi-plan marker.`);
314
- const marker = parseMarker(planMatch[1], `${ref} plan`);
315
- if (marker.schemaVersion !== SUPPORTED_SCHEMA)
316
- throw new Error(`${ref}: unsupported plan schema ${String(marker.schemaVersion)}.`);
317
- assertStableId(marker.id, "plan id");
318
- const title = source.match(/^# (.+)$/m)?.[1]?.trim();
319
- if (!title)
320
- throw new Error(`${ref}: missing plan title.`);
321
- const phases = parsePhases(section(source, "Implementation"), ref);
322
- const document = {
323
- ...marker,
324
- schemaVersion: 1,
325
- title,
326
- intent: cleanPlaceholder(section(source, "Intent")),
327
- requirements: parseBullets(section(source, "Requirements")),
328
- design: {
329
- bigIdeas: cleanPlaceholder(subsection(source, "Design", "Big Ideas")),
330
- keyApiUpdates: cleanPlaceholder(subsection(source, "Design", "Key API Addition/Updates")),
331
- consequences: cleanPlaceholder(subsection(source, "Design", "Consequences"))
332
- },
333
- phases,
334
- references: parseReferences(section(source, "References"))
335
- };
336
- const errors = validatePlanDocument(document).filter((issue) => issue.severity === "error");
337
- if (errors.length)
338
- throw new Error(`${ref}: ${errors.map((issue) => issue.message).join(" ")}`);
339
- return document;
340
- }
341
- function parsePhases(input, ref) {
342
- const starts = [...input.matchAll(PHASE_MARKER)];
343
- const phases = starts.map((match, index) => {
344
- const start = match.index;
345
- const end = input.indexOf("<!-- /diffpi-phase -->", start);
346
- if (end < 0)
347
- throw new Error(`${ref}: phase marker has no closing marker.`);
348
- const next = starts[index + 1]?.index;
349
- if (next !== undefined && next < end)
350
- throw new Error(`${ref}: nested or unclosed phase marker.`);
351
- const body = input.slice(start + match[0].length, end);
352
- const marker = parseMarker(match[1], `${ref} phase`);
353
- assertStableId(marker.id, "phase id");
354
- const title = body.match(/^### Phase: (.+)$/m)?.[1]?.trim();
355
- const objective = body.match(/^\*\*Objective:\*\*\s*(.*)$/m)?.[1]?.trim();
356
- if (!title || !objective)
357
- throw new Error(`${ref}: phase ${marker.id} is missing title or objective.`);
358
- return {
359
- ...marker,
360
- title,
361
- objective,
362
- dependencies: parseCsv(body.match(/^\*\*Dependencies:\*\*\s*(.*)$/m)?.[1]),
363
- tasks: parseTasks(body, ref)
364
- };
365
- });
366
- assertUniqueIds(phases.map((phase) => phase.id), "phase id");
367
- return phases;
368
- }
369
- function parseTasks(input, ref) {
370
- const starts = [...input.matchAll(TASK_MARKER)];
371
- const tasks = starts.map((match, index) => {
372
- const start = match.index;
373
- const end = input.indexOf("<!-- /diffpi-task -->", start);
374
- if (end < 0)
375
- throw new Error(`${ref}: task marker has no closing marker.`);
376
- const next = starts[index + 1]?.index;
377
- if (next !== undefined && next < end)
378
- throw new Error(`${ref}: nested or unclosed task marker.`);
379
- const body = input.slice(start + match[0].length, end);
380
- const marker = parseMarker(match[1], `${ref} task`);
381
- assertStableId(marker.id, "task id");
382
- const title = body.match(/^- \[[ xX]\] \*\*(.+)\*\*$/m)?.[1]?.trim();
383
- if (!title)
384
- throw new Error(`${ref}: task ${marker.id} is missing its checkbox title.`);
385
- return {
386
- ...marker,
387
- title,
388
- steps: parseListValue(body, "Steps"),
389
- dependencies: parseListValue(body, "Dependencies"),
390
- fileScopes: parseListValue(body, "File scopes"),
391
- acceptanceCriteria: parseListValue(body, "Acceptance criteria")
392
- };
393
- });
394
- assertUniqueIds(tasks.map((task) => task.id), "task id");
395
- return tasks;
396
- }
397
- function parseReferences(input) {
398
- return [...input.matchAll(/^- <!-- diffpi-reference: (\{[^\n]+\}) -->\s*(.*)$/gm)].map((match) => {
399
- const marker = parseMarker(match[1], "reference");
400
- assertStableId(marker.id, "reference id");
401
- return { id: marker.id, value: match[2].trim() || marker.value };
402
- });
403
- }
404
- function dependencyCycles(plan) {
405
- const graph = new Map;
406
- for (const phase of plan.phases)
407
- graph.set(phase.id, phase.dependencies);
408
- for (const task of plan.phases.flatMap((phase) => phase.tasks))
409
- graph.set(task.id, task.dependencies);
410
- const cycles = [];
411
- const visiting = new Set;
412
- const visited = new Set;
413
- const walk = (id, path) => {
414
- if (visiting.has(id)) {
415
- cycles.push([...path.slice(path.indexOf(id)), id]);
416
- return;
417
- }
418
- if (visited.has(id))
419
- return;
420
- visiting.add(id);
421
- for (const dependency of graph.get(id) ?? [])
422
- walk(dependency, [...path, id]);
423
- visiting.delete(id);
424
- visited.add(id);
425
- };
426
- for (const id of graph.keys())
427
- walk(id, []);
428
- return cycles;
429
- }
430
- function section(source, heading) {
431
- const match = source.match(new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m"));
432
- if (!match?.index) {
433
- if (match?.index === 0)
434
- return "";
435
- throw new Error(`Missing required heading: ${heading}.`);
436
- }
437
- const start = match.index + match[0].length;
438
- const rest = source.slice(start);
439
- const end = rest.search(/^## /m);
440
- return (end < 0 ? rest : rest.slice(0, end)).trim();
441
- }
442
- function subsection(source, parent, heading) {
443
- const body = section(source, parent);
444
- const match = body.match(new RegExp(`^### ${escapeRegExp(heading)}\\s*$`, "m"));
445
- if (match?.index === undefined)
446
- throw new Error(`Missing required heading: ${parent}/${heading}.`);
447
- const rest = body.slice(match.index + match[0].length);
448
- const end = rest.search(/^### /m);
449
- return (end < 0 ? rest : rest.slice(0, end)).trim();
450
- }
451
- function parseBullets(input) {
452
- return [...input.matchAll(/^- (?!<!--)(.+)$/gm)].map((match) => match[1].trim());
453
- }
454
- function parseListValue(body, label) {
455
- const value = body.match(new RegExp(`^ - ${escapeRegExp(label)}:\\s*(.*)$`, "m"))?.[1];
456
- return parseCsv(value);
457
- }
458
- function parseCsv(value) {
459
- if (!value || value.trim().toLowerCase() === "none")
460
- return [];
461
- return value.split(",").map((item) => item.trim()).filter(Boolean);
462
- }
463
- function cleanPlaceholder(value) {
464
- return value.replace(/<!--[^]*?-->/g, "").trim();
465
- }
466
- function parseMarker(value, label) {
467
- try {
468
- return JSON.parse(value);
469
- } catch {
470
- throw new Error(`Malformed ${label} marker JSON.`);
471
- }
472
- }
473
- function escapeRegExp(value) {
474
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
475
- }
476
- // src/plan/store.ts
477
- import { open, mkdir as mkdir3, readdir, readFile as readFile3, realpath as realpath3, rename as rename2, rm as rm2 } from "node:fs/promises";
478
- import { basename as basename3, dirname as dirname3, join as join5, resolve as resolve3, sep } from "node:path";
479
-
480
- // src/store.ts
481
- import { createHash } from "node:crypto";
482
- import { lstat, mkdir as mkdir2, readlink, realpath as realpath2, symlink, unlink } from "node:fs/promises";
483
- import { homedir } from "node:os";
484
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, resolve as resolve2 } from "node:path";
485
-
486
- // src/extensions/gitx.ts
487
- import { realpath } from "node:fs/promises";
488
- import { basename as basename2, isAbsolute, join as join3, resolve } from "node:path";
489
- async function gitToplevel(cwd) {
490
- const result = await run("git", ["-C", cwd, "rev-parse", "--show-toplevel"]);
491
- const top = result.stdout.trim();
492
- return result.code === 0 && top ? top : resolve(cwd);
493
- }
494
- async function inspectGitRepository(cwd) {
495
- const root = await gitToplevel(cwd);
496
- const remoteResult = await run("git", ["-C", root, "remote", "get-url", "origin"]);
497
- const remote = remoteResult.code === 0 ? remoteResult.stdout.trim() : "";
498
- const commonResult = await run("git", ["-C", root, "rev-parse", "--git-common-dir"]);
499
- const common = commonResult.stdout.trim();
500
- const commonPath = commonResult.code === 0 && common ? resolve(isAbsolute(common) ? common : join3(root, common)) : root;
501
- const commonDir = await canonicalPath(commonPath);
502
- return {
503
- root,
504
- commonDir,
505
- ...remote ? { remote } : {},
506
- name: remote ? repositoryName(remote) : basename2(resolve(commonDir, "..")) || basename2(root),
507
- identity: remote ? `remote:${normalizeRemote(remote)}` : `git-common-dir:${commonDir}`
508
- };
509
- }
510
- function normalizeRemote(remote) {
511
- return remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "").toLowerCase();
512
- }
513
- function repositoryName(remote) {
514
- const normalized = remote.trim().replace(/\.git\/?$/i, "").replace(/\/+$/, "");
515
- return normalized.split(/[/\\:]/).filter(Boolean).at(-1) ?? "";
516
- }
517
- async function canonicalPath(path) {
518
- try {
519
- return await realpath(path);
520
- } catch {
521
- return resolve(path);
522
- }
523
- }
524
-
525
- // src/store.ts
526
- var STORE_LINK = ".diffpi";
527
- var LEGACY_STORE_LINK = join4(".pi", "diffpi");
528
- function storeGlobalRoot(homeDir = homedir()) {
529
- return join4(homeDir, ".difflab", "diffpi", "projects");
530
- }
531
- async function ensureStore(cwd, homeDir = homedir()) {
532
- const repository = await inspectGitRepository(cwd);
533
- const root = repository.root;
534
- const slug = projectSlug(repository);
535
- const dest = join4(storeGlobalRoot(homeDir), slug);
536
- const link = join4(root, STORE_LINK);
537
- await mkdir2(dest, { recursive: true });
538
- try {
539
- await assertStoreLink(link, dest);
540
- } catch (error) {
541
- if (error.code !== "ENOENT")
542
- throw error;
543
- await symlink(dest, link);
544
- }
545
- await removeLegacyStoreLink(join4(root, LEGACY_STORE_LINK), dest);
546
- return { slug, root, dest, link, linked: true };
547
- }
548
- async function plansDir(cwd, homeDir = homedir()) {
549
- const store = await ensureStore(cwd, homeDir);
550
- const dir = join4(store.link, "plan");
551
- await mkdir2(dir, { recursive: true });
552
- return dir;
553
- }
554
- async function assertStoreLink(path, dest) {
555
- const entry = await lstat(path);
556
- if (!entry.isSymbolicLink())
557
- throw new Error(`${path} exists and is not a symlink.`);
558
- const target = await symlinkTarget(path);
559
- if (target !== await canonicalPath2(dest))
560
- throw new Error(`${path} points to ${target}, not ${dest}.`);
561
- }
562
- async function removeLegacyStoreLink(path, dest) {
563
- try {
564
- const entry = await lstat(path);
565
- if (!entry.isSymbolicLink())
566
- return;
567
- const target = await symlinkTarget(path);
568
- if (target === await canonicalPath2(dest))
569
- await unlink(path);
570
- } catch (error) {
571
- if (error.code !== "ENOENT")
572
- throw error;
573
- }
574
- }
575
- async function symlinkTarget(path) {
576
- const target = await readlink(path);
577
- return canonicalPath2(isAbsolute2(target) ? target : resolve2(dirname2(path), target));
578
- }
579
- async function canonicalPath2(path) {
580
- try {
581
- return await realpath2(path);
582
- } catch {
583
- return resolve2(path);
584
- }
585
- }
586
- function projectSlug(repository) {
587
- const readable = repository.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unnamed";
588
- const digest = createHash("sha256").update(repository.identity).digest("hex").slice(0, 12);
589
- return `${readable}-${digest}`;
590
- }
591
-
592
- // src/plan/store.ts
593
- async function resolvePlan(cwd, query, filters = {}, options = {}) {
594
- const root = await plansDir(cwd, options.homeDir);
595
- const entries = await readdir(root, { withFileTypes: true });
596
- const normalizedQuery = query ? normalizeQuery(query) : undefined;
597
- const records = [];
598
- for (const entry of entries) {
599
- if (!entry.isDirectory() || entry.name === ".tmp" || entry.name === ".lock")
600
- continue;
601
- if (normalizedQuery && entry.name !== normalizedQuery && stripDate(entry.name) !== normalizedQuery)
602
- continue;
603
- const dir = join5(root, entry.name);
604
- await assertContained(root, dir);
605
- try {
606
- const record = await readRecord(dir);
607
- if (filters.branch && record.document.branch !== filters.branch)
608
- continue;
609
- if (filters.statuses && !filters.statuses.includes(record.document.status))
610
- continue;
611
- records.push(record);
612
- } catch (error) {
613
- if (error.code !== "ENOENT")
614
- throw error;
615
- }
616
- }
617
- records.sort((left, right) => left.id.localeCompare(right.id));
618
- return { candidates: records, record: records.length === 1 ? records[0] : undefined, ambiguous: records.length > 1 };
619
- }
620
- async function readRecord(dir) {
621
- const planPath = join5(dir, "PLAN.md");
622
- const logPath = join5(dir, "logs.txt");
623
- const source = await readFile3(planPath, "utf8");
624
- const document = parsePlanDocument(source, planPath);
625
- return { id: basename3(dir), dir, planPath, logPath, document, source };
626
- }
627
- async function assertContained(root, candidate) {
628
- const canonicalRoot = await realpath3(root);
629
- let canonicalCandidate;
630
- try {
631
- canonicalCandidate = await realpath3(candidate);
632
- } catch {
633
- canonicalCandidate = resolve3(candidate);
634
- }
635
- if (canonicalCandidate !== canonicalRoot && !canonicalCandidate.startsWith(`${canonicalRoot}${sep}`)) {
636
- throw new Error(`Plan path escapes the shared store: ${candidate}.`);
637
- }
638
- }
639
- function normalizeQuery(value) {
640
- if (!value || value.includes("\x00") || value.includes("/") || value.includes("\\") || value.includes("..")) {
641
- throw new Error(`Invalid plan query: ${value}.`);
642
- }
643
- return value.toLowerCase();
644
- }
645
- function stripDate(value) {
646
- return value.replace(/^\d{6}-/, "");
647
- }
648
- // src/cli/plan.ts
649
- async function runPlanCli(args, io = process) {
650
- const verb = args.shift();
651
- if (verb === "--help" || verb === "-h" || !verb) {
652
- io.stdout.write(planCliHelp());
653
- return 0;
654
- }
655
- if (verb !== "annotate" && verb !== "annotations")
656
- throw new Error(`Unknown plan command: ${verb}.
657
- ${planCliHelp()}`);
658
- if (args.includes("--help") || args.includes("-h")) {
659
- io.stdout.write(`Usage: diffpi plan ${verb} [plan] [--cwd <path>]
660
- `);
661
- return 0;
662
- }
663
- let cwd = process.cwd();
664
- let query;
665
- while (args.length) {
666
- const token = args.shift();
667
- if (token === "--cwd") {
668
- const value = args.shift();
669
- if (!value || value.startsWith("--"))
670
- throw new Error("--cwd requires a path.");
671
- cwd = resolve4(value);
672
- } else if (token.startsWith("--"))
673
- throw new Error(`Unknown option: ${token}.`);
674
- else if (query)
675
- throw new Error(`Unexpected argument: ${token}.`);
676
- else
677
- query = token;
678
- }
679
- const record = await resolveCliPlan(cwd, query);
680
- if (verb === "annotate") {
681
- const result = await annotatePlan(record);
682
- io.stdout.write(`Annotated ${record.id} in tuicr session ${result.sessionSlug}.
683
- `);
684
- return result.code;
685
- }
686
- io.stdout.write(`${JSON.stringify(await readPlanAnnotations(record, { includeApplied: true }), null, 2)}
687
- `);
688
- return 0;
689
- }
690
- function planCliHelp() {
691
- return `Usage:
692
- diffpi plan annotate [plan] [--cwd <path>]
693
- diffpi plan annotations [plan] [--cwd <path>]
694
- `;
695
- }
696
- async function resolveCliPlan(cwd, query) {
697
- const branch = (await runChecked("git", ["-C", cwd, "branch", "--show-current"])).stdout.trim();
698
- const resolution = await resolvePlan(cwd, query, query ? {} : { branch, statuses: ["draft", "ready", "in_progress", "blocked"] });
699
- if (resolution.record)
700
- return resolution.record;
701
- if (resolution.ambiguous)
702
- throw new Error(`Plan selection is ambiguous: ${resolution.candidates.map((candidate) => candidate.id).join(", ")}.`);
703
- throw new Error(query ? `Plan "${query}" was not found.` : `No unfinished plan matches branch ${branch}.`);
704
- }
705
-
706
- // src/cli.ts
707
- async function main(argv = process.argv.slice(2)) {
708
- const [command, ...args] = argv;
709
- if (command === "--help" || command === "-h" || !command) {
710
- process.stdout.write(`Usage: diffpi plan <annotate|annotations> [options]
711
- `);
712
- return 0;
713
- }
714
- if (command !== "plan")
715
- throw new Error(`Unknown command: ${command}.`);
716
- return runPlanCli(args);
717
- }
718
- if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
719
- main().then((code) => {
720
- process.exitCode = code;
721
- }, (error) => {
722
- process.stderr.write(`diffpi: ${error instanceof Error ? error.message : String(error)}
723
- `);
724
- process.exitCode = 1;
725
- });
726
- }
727
- export {
728
- main
729
- };