@shiyuphan/follow-me 0.1.1 → 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 (150) hide show
  1. package/README.md +119 -69
  2. package/dist/src/cli.js +1069 -140
  3. package/dist/src/comments.js +361 -0
  4. package/dist/src/git.js +32 -3
  5. package/dist/src/host.js +106 -9
  6. package/dist/src/install.js +22 -1
  7. package/dist/src/naive.js +26 -0
  8. package/dist/src/plan.js +282 -26
  9. package/dist/src/prompt.js +711 -98
  10. package/dist/src/server.js +573 -140
  11. package/dist/src/session.js +106 -104
  12. package/dist/src/syntax.js +199 -0
  13. package/dist/src/types.js +3 -1
  14. package/dist/ui/dist/assets/abnfDiagram-WEYOIA3C-DcmzRkVZ.js +1 -0
  15. package/dist/ui/dist/assets/arc-T5QB0T1C.js +1 -0
  16. package/dist/ui/dist/assets/architecture-WOLXFQ4H-DUIkQTeE.js +1 -0
  17. package/dist/ui/dist/assets/architectureDiagram-ZHVYKT53-BUYi4-bA.js +36 -0
  18. package/dist/ui/dist/assets/array-BifhSqXX.js +1 -0
  19. package/dist/ui/dist/assets/blockDiagram-RJAE7QA3-dyJxtT9e.js +140 -0
  20. package/dist/ui/dist/assets/c4Diagram-XDPPPE54-CrAgiaZR.js +38 -0
  21. package/dist/ui/dist/assets/channel-wW7ebm0i.js +1 -0
  22. package/dist/ui/dist/assets/chunk-2E7WHVMD-C6fAf4Vj.js +88 -0
  23. package/dist/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  24. package/dist/ui/dist/assets/chunk-4EMNKR2T-BLZjwZ4Q.js +1 -0
  25. package/dist/ui/dist/assets/chunk-55XCU76K-DGLK2Q-L.js +1 -0
  26. package/dist/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  27. package/dist/ui/dist/assets/chunk-7S6XSTFO-BablszTX.js +2 -0
  28. package/dist/ui/dist/assets/chunk-AQDTSKZB-Cuk3B_vb.js +72 -0
  29. package/dist/ui/dist/assets/chunk-DSNLSHJZ-DiC78RqE.js +127 -0
  30. package/dist/ui/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  31. package/dist/ui/dist/assets/chunk-HAGFOTED-X_RbvixX.js +62 -0
  32. package/dist/ui/dist/assets/chunk-HNE4Q36P-C14_x5ck.js +1 -0
  33. package/dist/ui/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  34. package/dist/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  35. package/dist/ui/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  36. package/dist/ui/dist/assets/chunk-NQOKRVIF-Dxd0dtYR.js +213 -0
  37. package/dist/ui/dist/assets/chunk-UHW7PFBP-BIxePNUS.js +1 -0
  38. package/dist/ui/dist/assets/chunk-VQ5W62QI-CKSKLbj4.js +1 -0
  39. package/dist/ui/dist/assets/chunk-WQQL4CDK-CWNmZGlR.js +10 -0
  40. package/dist/ui/dist/assets/chunk-XXDRQBXY-D-I5QHgG.js +1 -0
  41. package/dist/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  42. package/dist/ui/dist/assets/chunk-YZO67HFG-DFP68CFd.js +1 -0
  43. package/dist/ui/dist/assets/chunk-ZAUX2LTX-DY90d4QG.js +1 -0
  44. package/dist/ui/dist/assets/chunk-ZCHMQSOR-Dlpr-5UC.js +2 -0
  45. package/dist/ui/dist/assets/classDiagram-v2-4MYWR3UA-CH9uhoLJ.js +217 -0
  46. package/dist/ui/dist/assets/cose-bilkent-JH36ORCC-DtYHrp8D.js +1 -0
  47. package/dist/ui/dist/assets/cynefin-EF2NZ3EQ-BtslbtrY.js +1 -0
  48. package/dist/ui/dist/assets/cynefinDiagram-WF24UDRP-DUU1Jcc7.js +62 -0
  49. package/dist/ui/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  50. package/dist/ui/dist/assets/dagre-NK4GIZVX-CDlP-dRW.js +4 -0
  51. package/dist/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  52. package/dist/ui/dist/assets/diagram-CBK3MWTC-DiV2fn-r.js +24 -0
  53. package/dist/ui/dist/assets/diagram-I5XIIV2D-Bg5RehGi.js +24 -0
  54. package/dist/ui/dist/assets/diagram-ODIL2UCW-CFmazz3f.js +30 -0
  55. package/dist/ui/dist/assets/diagram-PFBEJUXE-uw3ZBqDp.js +3 -0
  56. package/dist/ui/dist/assets/diagram-RJA3I4VV-uEOYYRYa.js +41 -0
  57. package/dist/ui/dist/assets/diagram-TYUNFSNR-BN58LF_3.js +200 -0
  58. package/dist/ui/dist/assets/dist-tWS_lXDS.js +1 -0
  59. package/dist/ui/dist/assets/ebnfDiagram-V4P3TTPT-B68Tz-w8.js +1 -0
  60. package/dist/ui/dist/assets/elk-QONTBCTQ-DZZohNea.js +27 -0
  61. package/dist/ui/dist/assets/erDiagram-KYQIGQAA-BH_m74pv.js +99 -0
  62. package/dist/ui/dist/assets/eventmodeling-K75KTNOO-CTp5FHos.js +1 -0
  63. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-CsCvexry.js +1 -0
  64. package/dist/ui/dist/assets/ganttDiagram-R7GPNTJY-BDau7_0f.js +292 -0
  65. package/dist/ui/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  66. package/dist/ui/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  67. package/dist/ui/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  68. package/dist/ui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  69. package/dist/ui/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  70. package/dist/ui/dist/assets/gitGraph-VSP46ZUC-Bv84IoCm.js +1 -0
  71. package/dist/ui/dist/assets/gitGraphDiagram-PDYL7XHY-DzXcMlm8.js +106 -0
  72. package/dist/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
  73. package/dist/ui/dist/assets/index-BTJoqVVN.js +133 -0
  74. package/dist/ui/dist/assets/index-D60U3smc.css +1 -0
  75. package/dist/ui/dist/assets/info-OHQRW6UA-222J0O_j.js +1 -0
  76. package/dist/ui/dist/assets/infoDiagram-MVDRKAX5-DE2sa3Vz.js +2 -0
  77. package/dist/ui/dist/assets/init-C-OQMol4.js +1 -0
  78. package/dist/ui/dist/assets/ishikawaDiagram-HPEHV2GE-BJDk0Zcj.js +70 -0
  79. package/dist/ui/dist/assets/journeyDiagram-BOSHW7O7-JSUXX5Hb.js +139 -0
  80. package/dist/ui/dist/assets/kanban-definition-FNSA4HC5-Bg79b49M.js +89 -0
  81. package/dist/ui/dist/assets/katex-B_JcIwqj.js +257 -0
  82. package/dist/ui/dist/assets/line-Bk5qQSIa.js +1 -0
  83. package/dist/ui/dist/assets/linear-PFnVMru5.js +1 -0
  84. package/dist/ui/dist/assets/mermaid-parser.core-Dcxlzc-S.js +7 -0
  85. package/dist/ui/dist/assets/mermaid.core-TeOHQJ00.js +44 -0
  86. package/dist/ui/dist/assets/mindmap-definition-6OY57UG3-IaNk8NnW.js +98 -0
  87. package/dist/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  88. package/dist/ui/dist/assets/packet-JDAUHWVQ-DMff5cVK.js +1 -0
  89. package/dist/ui/dist/assets/path-fybaL0A-.js +1 -0
  90. package/dist/ui/dist/assets/pegDiagram-ECIUJNBX-66Xu4YRc.js +1 -0
  91. package/dist/ui/dist/assets/pie-XZMESJXO-GEtzLCIM.js +1 -0
  92. package/dist/ui/dist/assets/pieDiagram-GNMPY74T-7jMBi-kU.js +39 -0
  93. package/dist/ui/dist/assets/prism-bash-D6zCJ74D.js +1 -0
  94. package/dist/ui/dist/assets/prism-c-04YixN25.js +1 -0
  95. package/dist/ui/dist/assets/prism-clike-DapgWyxx.js +1 -0
  96. package/dist/ui/dist/assets/prism-core-FDHlWQ43.js +1 -0
  97. package/dist/ui/dist/assets/prism-cpp-C-lJbC-6.js +1 -0
  98. package/dist/ui/dist/assets/prism-css-CPgOzxXp.js +1 -0
  99. package/dist/ui/dist/assets/prism-go-C_8qAH5n.js +1 -0
  100. package/dist/ui/dist/assets/prism-java-6wtusvIl.js +1 -0
  101. package/dist/ui/dist/assets/prism-javascript-BnO-swvr.js +1 -0
  102. package/dist/ui/dist/assets/prism-json-Dp_-W-Hv.js +1 -0
  103. package/dist/ui/dist/assets/prism-jsx-D-D0NWtC.js +1 -0
  104. package/dist/ui/dist/assets/prism-kotlin-BUlJgC-o.js +1 -0
  105. package/dist/ui/dist/assets/prism-markdown-Bc9-36OC.js +1 -0
  106. package/dist/ui/dist/assets/prism-markup-Cp04rIa1.js +1 -0
  107. package/dist/ui/dist/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
  108. package/dist/ui/dist/assets/prism-php-Co4fM40K.js +1 -0
  109. package/dist/ui/dist/assets/prism-python-CfiSVtM6.js +1 -0
  110. package/dist/ui/dist/assets/prism-ruby-DFCASkuD.js +1 -0
  111. package/dist/ui/dist/assets/prism-rust-Bu9N8X3N.js +1 -0
  112. package/dist/ui/dist/assets/prism-sql-CYhVDMEE.js +1 -0
  113. package/dist/ui/dist/assets/prism-swift-B0kWc-ne.js +1 -0
  114. package/dist/ui/dist/assets/prism-toml-DKb54GAT.js +1 -0
  115. package/dist/ui/dist/assets/prism-tsx-p6J2Kc_4.js +1 -0
  116. package/dist/ui/dist/assets/prism-typescript-BMqONzxf.js +1 -0
  117. package/dist/ui/dist/assets/prism-yaml-C0JHu8gr.js +1 -0
  118. package/dist/ui/dist/assets/quadrantDiagram-D57K4MC3-8hIWayC2.js +7 -0
  119. package/dist/ui/dist/assets/radar-ABXABTNO-BTdwUOOF.js +1 -0
  120. package/dist/ui/dist/assets/railroad-I3PHUGI6-DcMbDqBL.js +1 -0
  121. package/dist/ui/dist/assets/railroad-abnf-LNEFI6M7-gTzUTWeI.js +1 -0
  122. package/dist/ui/dist/assets/railroad-ebnf-DWYD2UWJ-CNtdUPtk.js +1 -0
  123. package/dist/ui/dist/assets/railroad-peg-JR7BVNR7-DOiExAtE.js +1 -0
  124. package/dist/ui/dist/assets/railroadDiagram-Y72NRBL7-2kE_ygvM.js +1 -0
  125. package/dist/ui/dist/assets/requirementDiagram-EQYSW5RA-CoIAHRq2.js +84 -0
  126. package/dist/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  127. package/dist/ui/dist/assets/sankeyDiagram-SY24JHO5-7p8D3wim.js +40 -0
  128. package/dist/ui/dist/assets/sequenceDiagram-BLEHNHQU-C6QFnMvn.js +169 -0
  129. package/dist/ui/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  130. package/dist/ui/dist/assets/src-FgijoBVs.js +1 -0
  131. package/dist/ui/dist/assets/stateDiagram-v2-A6IESKWZ-n9eAb_bT.js +291 -0
  132. package/dist/ui/dist/assets/swimlanes-B3ENRKFQ-U18mtUzX.js +1 -0
  133. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DX07QVy9.js +8 -0
  134. package/dist/ui/dist/assets/timeline-definition-JXYKGLGB-CD2Z8QGP.js +120 -0
  135. package/dist/ui/dist/assets/treeView-D4JQ5SDB-sqUjH5gB.js +1 -0
  136. package/dist/ui/dist/assets/treemap-SAJKECNS-DvZ38FP7.js +1 -0
  137. package/dist/ui/dist/assets/usecaseDiagram-WPOLBNNX-CqOGidaZ.js +328 -0
  138. package/dist/ui/dist/assets/vennDiagram-Q3Y3IDCW-D1SXSBQ5.js +34 -0
  139. package/dist/ui/dist/assets/wardley-7MLQ67FV-Bpwio-Dx.js +1 -0
  140. package/dist/ui/dist/assets/wardleyDiagram-IVAIDKWA-JSUXHMnk.js +78 -0
  141. package/dist/ui/dist/assets/xychartDiagram-4URNHDU4-B4IFqQTN.js +7 -0
  142. package/dist/ui/dist/index.html +15 -0
  143. package/dist/ui/dist/style.css +121 -0
  144. package/package.json +31 -8
  145. package/skills/follow-me/SKILL.md +15 -61
  146. package/dist/src/anthropic.js +0 -101
  147. package/dist/ui/app.js +0 -797
  148. package/dist/ui/index.html +0 -40
  149. package/dist/ui/playback.js +0 -175
  150. package/dist/ui/style.css +0 -466
@@ -1,9 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
3
  import { basename, dirname, join } from "node:path";
4
- import { isRepoPath, isSha, isUnitId } from "./diff.js";
5
- import { branchExists, currentBranch, defaultBaseRef, loadDiff, objectsDir, QUARANTINE_PREFIX, repoRoot, resolveCommit, } from "./git.js";
6
- import { isArray, isCommentId, isFields, isPlanVersion, parsePlanFile, planHash, serializePlanFile, } from "./plan.js";
4
+ import { parseDraft, parseReview } from "./comments.js";
5
+ import { isSha } from "./diff.js";
6
+ import { branchExists, currentBranch, defaultBaseRef, GitError, loadDiff, objectsDir, QUARANTINE_PREFIX, repoRoot, resolveCommit, } from "./git.js";
7
+ import { isFields, isPlanVersion, parsePlanFile, planHash, serializePlanFile, } from "./plan.js";
7
8
  export class BranchMovedError extends Error {
8
9
  name = "BranchMovedError";
9
10
  }
@@ -11,6 +12,21 @@ export class BranchMovedError extends Error {
11
12
  export class SessionFileError extends Error {
12
13
  name = "SessionFileError";
13
14
  }
15
+ /** draft.json has not been written yet. */
16
+ export class MissingDraftError extends SessionFileError {
17
+ path;
18
+ constructor(path) {
19
+ super(`${path} does not exist yet`);
20
+ this.path = path;
21
+ }
22
+ }
23
+ /** The command ran outside any git work tree. */
24
+ export class NotARepositoryError extends Error {
25
+ name = "NotARepositoryError";
26
+ constructor() {
27
+ super("not a git repository (run it inside the repo you want to split)");
28
+ }
29
+ }
14
30
  /** Another live follow-me process holds the session's lock. */
15
31
  export class SessionLockedError extends Error {
16
32
  name = "SessionLockedError";
@@ -118,12 +134,15 @@ function adoptLock(dir, from, command) {
118
134
  export function slugOf(branch) {
119
135
  return branch.replaceAll("/", "-");
120
136
  }
121
- function writeJson(path, value) {
137
+ function writeText(path, text) {
122
138
  mkdirSync(dirname(path), { recursive: true });
123
139
  const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
124
- writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`);
140
+ writeFileSync(temp, text);
125
141
  renameSync(temp, path);
126
142
  }
143
+ function writeJson(path, value) {
144
+ writeText(path, `${JSON.stringify(value, null, 2)}\n`);
145
+ }
127
146
  function readJson(path) {
128
147
  let text;
129
148
  try {
@@ -169,101 +188,25 @@ function parseAnchor(v) {
169
188
  function parseApplied(v) {
170
189
  if (!isFields(v))
171
190
  return null;
172
- const { schema, version, branch, head } = v;
191
+ const { schema, version, branch, head, hash = null, signed = null } = v;
173
192
  if (schema !== "follow-me/applied@1" || !isPlanVersion(version))
174
193
  return null;
175
194
  if (typeof branch !== "string" || typeof head !== "string" || !isSha(head))
176
195
  return null;
177
- return { schema, version, branch, head };
178
- }
179
- function isCommitIndex(v) {
180
- return typeof v === "number" && Number.isInteger(v) && v >= 0;
181
- }
182
- function parseCommentAnchor(v) {
183
- if (!isFields(v))
184
- return "anchor is not an object";
185
- if (v.kind === "plan")
186
- return { kind: "plan" };
187
- if (!isCommitIndex(v.commit))
188
- return "anchor.commit is not a commit index";
189
- const commit = v.commit;
190
- if (v.kind === "message" || v.kind === "position")
191
- return { kind: v.kind, commit };
192
- if (v.kind !== "hunk")
193
- return "anchor.kind is unknown";
194
- if (typeof v.unit !== "string" || !isUnitId(v.unit))
195
- return "anchor.unit is not a unit id";
196
- if (v.changes === null)
197
- return { kind: "hunk", unit: v.unit, commit, changes: null };
198
- if (!isArray(v.changes) || !v.changes.every(isCommitIndex)) {
199
- return "anchor.changes is not null or a list of change indices";
200
- }
201
- return { kind: "hunk", unit: v.unit, commit, changes: v.changes.filter(isCommitIndex) };
202
- }
203
- function parseComment(v) {
204
- if (!isFields(v))
205
- return "not an object";
206
- if (typeof v.id !== "string" || !isCommentId(v.id))
207
- return "id is not a comment id";
208
- const anchor = parseCommentAnchor(v.anchor);
209
- if (typeof anchor === "string")
210
- return anchor;
211
- const ctx = v.context;
212
- if (!isFields(ctx) || typeof ctx.commitMessage !== "string") {
213
- return "context.commitMessage is not a string";
214
- }
215
- const path = ctx.path;
216
- if (path !== null && (typeof path !== "string" || !isRepoPath(path))) {
217
- return "context.path is not null or a path";
218
- }
219
- if (typeof v.body !== "string" || typeof v.createdAt !== "string") {
220
- return "body and createdAt must be strings";
221
- }
222
- return {
223
- id: v.id,
224
- anchor,
225
- context: { commitMessage: ctx.commitMessage, path },
226
- body: v.body,
227
- createdAt: v.createdAt,
228
- };
229
- }
230
- /** Boundary parse of reviews/vN.json. Files written before walk mode existed have no mode. */
231
- export function parseReview(v) {
232
- if (!isFields(v)) {
233
- return "not an object";
234
- }
235
- if (v.schema !== "follow-me/review@1")
236
- return 'schema is not "follow-me/review@1"';
237
- const mode = v.mode ?? "split";
238
- if (mode !== "split" && mode !== "walk")
239
- return 'mode is not "split" or "walk"';
240
- if (!isPlanVersion(v.planVersion))
241
- return "planVersion is not a positive integer";
242
- if (typeof v.planHash !== "string" || typeof v.endedAt !== "string") {
243
- return "planHash and endedAt must be strings";
244
- }
245
- if (!isArray(v.comments))
246
- return "comments is not an array";
247
- const comments = [];
248
- for (const [i, raw] of v.comments.entries()) {
249
- const c = parseComment(raw);
250
- if (typeof c === "string")
251
- return `comments[${i}]: ${c}`;
252
- comments.push(c);
253
- }
254
- return {
255
- schema: v.schema,
256
- mode,
257
- planVersion: v.planVersion,
258
- planHash: v.planHash,
259
- endedAt: v.endedAt,
260
- comments,
261
- };
196
+ if (hash !== null && typeof hash !== "string")
197
+ return null;
198
+ if (signed !== null && typeof signed !== "boolean")
199
+ return null;
200
+ return { schema, version, branch, head, hash, signed };
262
201
  }
263
202
  function makeSession(root, dir, anchor, diff) {
264
203
  const plansDir = join(dir, "plans");
265
- const reviewsDir = join(dir, "reviews");
204
+ const splitReviews = join(dir, "reviews");
205
+ // Older builds wrote reviewer reviews to reviews/walk/; they stay readable, nothing new goes there.
206
+ const legacyWalkDir = join(splitReviews, "walk");
207
+ const reviewsDir = () => splitReviews;
266
208
  const draftPath = join(dir, "draft.json");
209
+ const promptPath = join(dir, "prompt.md");
267
210
  const appliedPath = join(dir, "applied.json");
268
211
  const plans = () => versions(plansDir).map(({ version, path }) => {
269
212
  const file = parsePlanFile(readJson(path));
@@ -274,30 +217,75 @@ function makeSession(root, dir, anchor, diff) {
274
217
  }
275
218
  return { version, file, hash: planHash(file) };
276
219
  });
277
- const reviews = () => versions(reviewsDir).map(({ version, path }) => {
220
+ const reviewsIn = (dir) => versions(dir).map(({ version, path }) => {
278
221
  const review = parseReview(readJson(path));
279
222
  if (typeof review === "string")
280
223
  throw new SessionFileError(`${path}: ${review}`);
281
224
  return { version, review };
282
225
  });
226
+ const reviews = () => reviewsIn(splitReviews);
227
+ const walkReviews = () => [...reviews().filter((r) => r.review.mode === "walk"), ...reviewsIn(legacyWalkDir)].sort((a, b) => a.version - b.version);
228
+ const draftVersions = () => {
229
+ const dir = reviewsDir();
230
+ if (!existsSync(dir))
231
+ return [];
232
+ return readdirSync(dir).flatMap((name) => {
233
+ const n = Number(/^v([1-9][0-9]*)\.draft\.json$/.exec(name)?.[1]);
234
+ return isPlanVersion(n) ? [n] : [];
235
+ });
236
+ };
237
+ const draftReview = (mode, version) => {
238
+ const path = join(reviewsDir(), `v${version}.draft.json`);
239
+ const draft = parseDraft(readJson(path));
240
+ if (typeof draft === "string")
241
+ throw new SessionFileError(`${path}: ${draft}`);
242
+ const { comments, concerns, resolved, excluded } = draft;
243
+ return {
244
+ schema: "follow-me/review@1",
245
+ mode,
246
+ planVersion: version,
247
+ planHash: plans().find((p) => p.version === version)?.hash ?? "",
248
+ endedAt: statSync(path).mtime.toISOString(),
249
+ comments,
250
+ ...(concerns.length === 0 ? {} : { concerns }),
251
+ ...(resolved.length === 0 ? {} : { resolved }),
252
+ ...(excluded.comments.length + excluded.concerns.length === 0 ? {} : { excluded }),
253
+ };
254
+ };
255
+ const priorReview = (mode, before) => {
256
+ const below = (v) => before === null || v < before;
257
+ const done = (mode === "walk" ? walkReviews() : reviews().filter((r) => r.review.mode === mode))
258
+ .filter((r) => below(r.version))
259
+ .at(-1);
260
+ const unfinished = draftVersions()
261
+ .filter((v) => below(v) && v > (done?.version ?? 0))
262
+ .sort((a, b) => a - b)
263
+ .at(-1);
264
+ return unfinished === undefined ? (done?.review ?? null) : draftReview(mode, unfinished);
265
+ };
283
266
  return {
284
267
  root,
285
268
  dir,
286
269
  anchor,
287
270
  diff,
288
271
  draftPath,
272
+ promptPath,
289
273
  plans,
274
+ reviewsDir,
290
275
  reviews,
291
- lastReview: () => reviews().findLast((r) => r.review.mode === "split")?.review ?? null,
276
+ walkReviews,
277
+ lastReview: () => priorReview("split", null),
278
+ priorReview,
292
279
  readDraft: () => {
293
280
  const draft = readJson(draftPath);
294
281
  if (draft === undefined)
295
- throw new SessionFileError(`${draftPath} does not exist yet`);
282
+ throw new MissingDraftError(draftPath);
296
283
  return draft;
297
284
  },
298
285
  writeDraft: (draft) => writeJson(draftPath, draft),
286
+ writePrompt: (text) => writeText(promptPath, text),
299
287
  writeReview: (review) => {
300
- const path = join(reviewsDir, `v${review.planVersion}.json`);
288
+ const path = join(reviewsDir(), `v${review.planVersion}.json`);
301
289
  if (existsSync(path))
302
290
  throw new SessionFileError(`${path} is already written`);
303
291
  writeJson(path, review);
@@ -324,16 +312,30 @@ function makeSession(root, dir, anchor, diff) {
324
312
  throw new SessionFileError(`${appliedPath} is not an applied record`);
325
313
  return applied;
326
314
  },
327
- recordApplied: (version, result) => writeJson(appliedPath, {
315
+ recordApplied: (plan, result) => writeJson(appliedPath, {
328
316
  schema: "follow-me/applied@1",
329
- version,
317
+ version: plan.version,
330
318
  branch: result.branch,
331
319
  head: result.head,
320
+ hash: plan.hash,
321
+ signed: result.signed,
332
322
  }),
333
323
  };
334
324
  }
325
+ /** The top level of the work tree holding cwd. */
326
+ async function rootOf(cwd) {
327
+ try {
328
+ return await repoRoot(cwd);
329
+ }
330
+ catch (error) {
331
+ if (error instanceof GitError && error.message.includes("not a git repository")) {
332
+ throw new NotARepositoryError();
333
+ }
334
+ throw error;
335
+ }
336
+ }
335
337
  async function locate(repo, branch) {
336
- const root = await repoRoot(repo);
338
+ const root = await rootOf(repo);
337
339
  const name = branch ?? (await currentBranch(root));
338
340
  if (name === null)
339
341
  throw new Error("HEAD is detached; pass --branch <name>");
@@ -421,8 +423,7 @@ function abandonedTemps(dir) {
421
423
  .map((name) => join(dir, name));
422
424
  }
423
425
  /** Review drafts for a version at or below the latest finished review; none can be resumed. */
424
- function deadReviewDrafts(dir) {
425
- const reviewsDir = join(dir, "reviews");
426
+ function deadReviewDrafts(reviewsDir) {
426
427
  const latest = versions(reviewsDir).at(-1)?.version ?? 0;
427
428
  if (!existsSync(reviewsDir))
428
429
  return [];
@@ -441,7 +442,7 @@ function deadReviewDrafts(dir) {
441
442
  * a live process are left alone, and all refuses while one exists.
442
443
  */
443
444
  export async function cleanSessions(repo, opts) {
444
- const root = await repoRoot(repo);
445
+ const root = await rootOf(repo);
445
446
  const top = join(root, ".follow-me");
446
447
  const sessions = existsSync(top)
447
448
  ? readdirSync(top, { withFileTypes: true })
@@ -466,7 +467,8 @@ export async function cleanSessions(repo, opts) {
466
467
  }
467
468
  if (lock.kind === "stale")
468
469
  found.push(join(dir, "lock"));
469
- found.push(...abandonedTemps(dir), ...deadReviewDrafts(dir));
470
+ const reviews = join(dir, "reviews");
471
+ found.push(...abandonedTemps(dir), ...deadReviewDrafts(reviews), ...deadReviewDrafts(join(reviews, "walk")));
470
472
  }
471
473
  if (opts.all && existsSync(top))
472
474
  found.push(top);
@@ -0,0 +1,199 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { extname, join } from "node:path";
5
+ const LIMIT = 4;
6
+ const TIMEOUT_MS = 30_000;
7
+ /**
8
+ * node --check does not strip types, so .ts goes through module.stripTypeScriptTypes, which
9
+ * parses the whole file and throws on a syntax error. An enum or namespace parses but cannot be
10
+ * stripped, which is node's limit and not the file's, so that counts as parsing.
11
+ */
12
+ const STRIP_TYPES = `
13
+ const { stripTypeScriptTypes } = require("node:module");
14
+ const { readFileSync } = require("node:fs");
15
+ try {
16
+ stripTypeScriptTypes(readFileSync(process.argv[1], "utf8"), { mode: "strip" });
17
+ } catch (error) {
18
+ if (error.code === "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX") process.exit(0);
19
+ console.error(error.message);
20
+ process.exit(1);
21
+ }`;
22
+ /** Runs argv with {file} replaced; a program that is not installed skips the file. */
23
+ function command(argv) {
24
+ return (file) => run(argv.map((a) => (a === "{file}" ? file : a)), file, argv[0] ?? "");
25
+ }
26
+ function run(argv, file, what) {
27
+ const [program, ...rest] = argv;
28
+ if (program === undefined)
29
+ return Promise.resolve({ kind: "parses" });
30
+ return new Promise((resolve) => {
31
+ execFile(program, rest, { timeout: TIMEOUT_MS, encoding: "utf8" }, (error, stdout, stderr) => {
32
+ if (error === null)
33
+ return resolve({ kind: "parses" });
34
+ if (error.code === "ENOENT") {
35
+ return resolve({ kind: "skipped", reason: `${what} is not installed` });
36
+ }
37
+ const output = stderr.trim() === "" ? stdout : stderr;
38
+ const message = error.killed === true
39
+ ? `checker timed out after ${TIMEOUT_MS / 1000}s`
40
+ : output.trim() === ""
41
+ ? `checker exited ${error.code}`
42
+ : complaint(output, file);
43
+ resolve({ kind: "fails", message });
44
+ });
45
+ });
46
+ }
47
+ /**
48
+ * The line of a parser's output that names the problem. node prints `file:LINE`, the source line,
49
+ * a caret and then `SyntaxError: ...`; python prints a traceback ending in `SyntaxError: ...`;
50
+ * sh and bash print one line. The temp path never leaks into the message.
51
+ */
52
+ export function complaint(output, file) {
53
+ const lines = output
54
+ .replaceAll(file, "file")
55
+ .split("\n")
56
+ .map((l) => l.trimEnd())
57
+ .filter((l) => l !== "");
58
+ const named = lines.find((l) => /^[A-Za-z]*Error\b/.test(l));
59
+ if (named === undefined)
60
+ return lines[0] ?? "does not parse";
61
+ const where = /^file:(\d+)$/.exec(lines[0] ?? "")?.[1];
62
+ return where === undefined ? named : `line ${where}: ${named}`;
63
+ }
64
+ const json = async (_file, text) => {
65
+ try {
66
+ JSON.parse(text);
67
+ return { kind: "parses" };
68
+ }
69
+ catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ return { kind: "fails", message: message.split("\n")[0] ?? message };
72
+ }
73
+ };
74
+ /** Bash scripts need bash -n; a plain .sh is held to POSIX unless its shebang names bash. */
75
+ const sh = command(["sh", "-n", "{file}"]);
76
+ const bash = command(["bash", "-n", "{file}"]);
77
+ const shell = (file, text) => /^#!.*\bbash\b/.test(text) || file.endsWith(".bash") ? bash(file, text) : sh(file, text);
78
+ const node = command(["node", "--check", "{file}"]);
79
+ const types = command([
80
+ "node",
81
+ "--disable-warning=ExperimentalWarning",
82
+ "-e",
83
+ STRIP_TYPES,
84
+ "{file}",
85
+ ]);
86
+ const python = command(["python3", "-m", "py_compile", "{file}"]);
87
+ /**
88
+ * Extension to checker. A missing extension is skipped, not guessed: .jsx and .tsx need a JSX
89
+ * parser node does not have, and yaml has none installed.
90
+ */
91
+ const CHECKERS = new Map([
92
+ [".js", node],
93
+ [".mjs", node],
94
+ [".cjs", node],
95
+ [".ts", types],
96
+ [".mts", types],
97
+ [".cts", types],
98
+ [".json", json],
99
+ [".sh", shell],
100
+ [".bash", shell],
101
+ [".py", python],
102
+ ]);
103
+ function quote(s) {
104
+ return `'${s.replaceAll("'", `'\\''`)}'`;
105
+ }
106
+ /** The --syntax override: one shell command for every text file, {file} replaced by its path. */
107
+ function overrideChecker(template) {
108
+ return (file) => run(["sh", "-c", template.replaceAll("{file}", quote(file))], file, "the --syntax command");
109
+ }
110
+ async function mapLimit(items, limit, fn) {
111
+ const out = new Array(items.length);
112
+ let next = 0;
113
+ const worker = async () => {
114
+ for (let i = next++; i < items.length; i = next++) {
115
+ const item = items[i];
116
+ if (item !== undefined)
117
+ out[i] = await fn(item);
118
+ }
119
+ };
120
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
121
+ return out;
122
+ }
123
+ function byPath(a, b) {
124
+ return a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
125
+ }
126
+ /**
127
+ * Checks every file each commit writes. The last commit equals the tip by construction, so a
128
+ * file that fails there is reported as the tip's own state and is not checked in earlier commits,
129
+ * where the same failure would be blamed on the split.
130
+ */
131
+ export async function checkSyntax(plan, opts) {
132
+ const last = plan.commits.length - 1;
133
+ const jobs = plan.commits.flatMap((c) => c.changes.flatMap((ch) => ch.op === "write" ? [{ commit: c.index, path: ch.path, bytes: ch.bytes }] : []));
134
+ // node prints the resolved path, so the dir is resolved too for the message to find it.
135
+ const dir = await realpath(await mkdtemp(join(tmpdir(), "follow-me-syntax-")));
136
+ try {
137
+ const cache = new Map();
138
+ let serial = 0;
139
+ const outcomeOf = (job) => {
140
+ const ext = extname(job.path).toLowerCase();
141
+ const checker = opts.override === undefined ? CHECKERS.get(ext) : overrideChecker(opts.override);
142
+ if (checker === undefined) {
143
+ return Promise.resolve({
144
+ kind: "skipped",
145
+ reason: `no checker for ${ext === "" ? "files without an extension" : ext}`,
146
+ });
147
+ }
148
+ const key = `${ext}\0${job.bytes}`;
149
+ const known = cache.get(key);
150
+ if (known !== undefined)
151
+ return known;
152
+ const file = join(dir, `f${serial++}${ext}`);
153
+ const fresh = writeFile(file, Buffer.from(job.bytes, "latin1")).then(() => checker(file, job.bytes));
154
+ cache.set(key, fresh);
155
+ return fresh;
156
+ };
157
+ const tipJobs = jobs.filter((j) => j.commit === last);
158
+ const tipOutcomes = await mapLimit(tipJobs, LIMIT, outcomeOf);
159
+ const tip = [];
160
+ const broken = new Set();
161
+ tipJobs.forEach((job, i) => {
162
+ const outcome = tipOutcomes[i];
163
+ if (outcome?.kind !== "fails")
164
+ return;
165
+ tip.push({ commit: job.commit, path: job.path, message: outcome.message });
166
+ broken.add(job.path);
167
+ });
168
+ const rest = jobs.filter((j) => j.commit !== last && !broken.has(j.path));
169
+ const outcomes = await mapLimit(rest, LIMIT, outcomeOf);
170
+ const issues = [];
171
+ const skipped = new Map();
172
+ let checked = 0;
173
+ const all = [
174
+ ...tipJobs.map((j, i) => [j, tipOutcomes[i]]),
175
+ ...rest.map((j, i) => [j, outcomes[i]]),
176
+ ];
177
+ for (const [job, outcome] of all) {
178
+ if (outcome === undefined)
179
+ continue;
180
+ if (outcome.kind === "skipped")
181
+ skipped.set(job.path, outcome.reason);
182
+ else
183
+ checked++;
184
+ if (outcome.kind === "fails" && job.commit !== last) {
185
+ issues.push({ commit: job.commit, path: job.path, message: outcome.message });
186
+ }
187
+ }
188
+ issues.sort((a, b) => a.commit - b.commit || byPath(a, b));
189
+ return {
190
+ issues,
191
+ tip,
192
+ skipped: [...skipped].map(([path, reason]) => ({ path, reason })).sort(byPath),
193
+ checked,
194
+ };
195
+ }
196
+ finally {
197
+ await rm(dir, { recursive: true, force: true });
198
+ }
199
+ }
package/dist/src/types.js CHANGED
@@ -8,7 +8,9 @@
8
8
  * project every commit, prove the last one equals the tip. Pure.
9
9
  * git.ts the only module that spawns git: load a Diff, write a branch from a ValidPlan.
10
10
  * session.ts the .follow-me/<slug>/ layout: session anchor, plans, reviews, draft.
11
- * prompt.ts the planner prompt for an agent, and the Anthropic API planner.
11
+ * comments.ts the wire formats of review state: comments, replies, concern decisions, the
12
+ * review file, the autosaved draft and the server's events. Pure parsers.
13
+ * prompt.ts the planner prompt for an agent, the feedback and the consolidated report.
12
14
  * server.ts the review server: a registry of hosted sessions, their JSON API, and ui/.
13
15
  * host.ts the one server per user: its record in the state dir, attach, start, stop.
14
16
  * cli.ts argv -> one call into the modules above.
@@ -0,0 +1 @@
1
+ import{m as e,t}from"./mermaid-parser.core-Dcxlzc-S.js";import{n}from"./chunk-Y2CYZVJY-DsF7k-Jl.js";import{m as r}from"./src-FgijoBVs.js";import"./chunk-DSNLSHJZ-DiC78RqE.js";import{n as i,r as a,t as o}from"./chunk-2E7WHVMD-C6fAf4Vj.js";import{t as s}from"./chunk-JWPE2WC7-DVXcaiue.js";import"./mermaid.core-TeOHQJ00.js";var c=e().RailroadAbnf.parser.LangiumParser,l=n(e=>{let t=e.alternatives.map(u);return t.length===1?t[0]:{type:`choice`,alternatives:t}},`transformAlternation`),u=n(e=>{let t=e.elements.map(f);return t.length===1?t[0]:{type:`sequence`,elements:t}},`transformConcatenation`),d=n(e=>{if(e.includes(`*`)){let[t,n]=e.split(`*`);return{min:t?parseInt(t,10):0,max:n?parseInt(n,10):1/0}}let t=parseInt(e,10);return{min:t,max:t}},`parseRepeat`),f=n(e=>{let t=p(e.primary);if(!e.repeat)return t;let{min:n,max:r}=d(e.repeat);return n===0&&r===1?{type:`optional`,element:t}:{type:`repetition`,element:t,min:n,max:r}},`transformElement`),p=n(e=>{switch(e.$type){case`AbnfStringLiteral`:return{type:`terminal`,value:e.value};case`AbnfNumVal`:return{type:`terminal`,value:e.value};case`AbnfRuleName`:return{type:`nonterminal`,name:e.name};case`AbnfGroup`:return l(e.element);case`AbnfOptionalGroup`:return{type:`optional`,element:l(e.element)};default:throw Error(`Unsupported ABNF primary node: ${e.$type}`)}},`transformPrimary`),m=n(e=>({name:e.name,definition:l(e.definition)}),`transformRule`),h=n(e=>{s(e,o),e.title&&o.setTitle(e.title),e.rules.map(e=>o.addRule(m(e)))},`populateDb`),g={parser:{parse:n(e=>{o.clear(),r.debug(`[ABNF Parser] Starting Langium parse`);let n=c.parse(e);if(n.lexerErrors.length>0||n.parserErrors.length>0)throw new t(n);let i=n.value;r.debug(`[ABNF Parser] Parsed rules:`,i.rules.length),h(i),r.debug(`[ABNF Parser] Parse complete`)},`parse`),parser:{yy:o}},db:o,renderer:a,styles:i};export{g as diagram};
@@ -0,0 +1 @@
1
+ import{n as e,t}from"./path-fybaL0A-.js";import{a as n,c as r,d as i,f as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,u as p}from"./dist-tWS_lXDS.js";function m(e){return e.innerRadius}function h(e){return e.outerRadius}function g(e){return e.startAngle}function _(e){return e.endAngle}function v(e){return e&&e.padAngle}function y(e,t,n,r,i,a,o,s){var c=n-e,l=r-t,u=o-i,d=s-a,f=d*c-u*l;if(!(f*f<1e-12))return f=(u*(t-a)-d*(e-i))/f,[e+f*c,t+f*l]}function b(e,t,n,r,i,a,o){var c=e-n,l=t-r,u=(o?a:-a)/d(c*c+l*l),f=u*l,p=-u*c,m=e+f,h=t+p,g=n+f,_=r+p,v=(m+g)/2,y=(h+_)/2,b=g-m,x=_-h,S=b*b+x*x,C=i-a,w=m*_-g*h,T=(x<0?-1:1)*d(s(0,C*C*S-w*w)),E=(w*x-b*T)/S,D=(-w*b-x*T)/S,O=(w*x+b*T)/S,k=(-w*b+x*T)/S,A=E-v,j=D-y,M=O-v,N=k-y;return A*A+j*j>M*M+N*N&&(E=O,D=k),{cx:E,cy:D,x01:-f,y01:-p,x11:E*(i/C-1),y11:D*(i/C-1)}}function x(){var s=m,x=h,S=e(0),C=null,w=g,T=_,E=v,D=null,O=t(k);function k(){var e,t,m=+s.apply(this,arguments),h=+x.apply(this,arguments),g=w.apply(this,arguments)-r,_=T.apply(this,arguments)-r,v=l(_-g),k=_>g;if(D||=e=O(),h<m&&(t=h,h=m,m=t),!(h>1e-12))D.moveTo(0,0);else if(v>c-1e-12)D.moveTo(h*u(g),h*a(g)),D.arc(0,0,h,g,_,!k),m>1e-12&&(D.moveTo(m*u(_),m*a(_)),D.arc(0,0,m,_,g,k));else{var A=g,j=_,M=g,N=_,P=v,F=v,I=E.apply(this,arguments)/2,L=I>1e-12&&(C?+C.apply(this,arguments):d(m*m+h*h)),R=p(l(h-m)/2,+S.apply(this,arguments)),z=R,B=R,V,H;if(L>1e-12){var U=o(L/m*a(I)),W=o(L/h*a(I));(P-=U*2)>1e-12?(U*=k?1:-1,M+=U,N-=U):(P=0,M=N=(g+_)/2),(F-=W*2)>1e-12?(W*=k?1:-1,A+=W,j-=W):(F=0,A=j=(g+_)/2)}var G=h*u(A),K=h*a(A),q=m*u(N),J=m*a(N);if(R>1e-12){var Y=h*u(j),X=h*a(j),Z=m*u(M),Q=m*a(M),$;if(v<i){if($=y(G,K,Z,Q,Y,X,q,J)){var ee=G-$[0],te=K-$[1],ne=Y-$[0],re=X-$[1],ie=1/a(f((ee*ne+te*re)/(d(ee*ee+te*te)*d(ne*ne+re*re)))/2),ae=d($[0]*$[0]+$[1]*$[1]);z=p(R,(m-ae)/(ie-1)),B=p(R,(h-ae)/(ie+1))}else z=B=0}}F>1e-12?B>1e-12?(V=b(Z,Q,G,K,h,B,k),H=b(Y,X,q,J,h,B,k),D.moveTo(V.cx+V.x01,V.cy+V.y01),B<R?D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,B,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,h,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),!k),D.arc(H.cx,H.cy,B,n(H.y11,H.x11),n(H.y01,H.x01),!k))):(D.moveTo(G,K),D.arc(0,0,h,A,j,!k)):D.moveTo(G,K),!(m>1e-12)||!(P>1e-12)?D.lineTo(q,J):z>1e-12?(V=b(q,J,Y,X,m,-z,k),H=b(G,K,Z,Q,m,-z,k),D.lineTo(V.cx+V.x01,V.cy+V.y01),z<R?D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(H.y01,H.x01),!k):(D.arc(V.cx,V.cy,z,n(V.y01,V.x01),n(V.y11,V.x11),!k),D.arc(0,0,m,n(V.cy+V.y11,V.cx+V.x11),n(H.cy+H.y11,H.cx+H.x11),k),D.arc(H.cx,H.cy,z,n(H.y11,H.x11),n(H.y01,H.x01),!k))):D.arc(0,0,m,N,M,k)}if(D.closePath(),e)return D=null,e+``||null}return k.centroid=function(){var e=(+s.apply(this,arguments)+ +x.apply(this,arguments))/2,t=(+w.apply(this,arguments)+ +T.apply(this,arguments))/2-i/2;return[u(t)*e,a(t)*e]},k.innerRadius=function(t){return arguments.length?(s=typeof t==`function`?t:e(+t),k):s},k.outerRadius=function(t){return arguments.length?(x=typeof t==`function`?t:e(+t),k):x},k.cornerRadius=function(t){return arguments.length?(S=typeof t==`function`?t:e(+t),k):S},k.padRadius=function(t){return arguments.length?(C=t==null?null:typeof t==`function`?t:e(+t),k):C},k.startAngle=function(t){return arguments.length?(w=typeof t==`function`?t:e(+t),k):w},k.endAngle=function(t){return arguments.length?(T=typeof t==`function`?t:e(+t),k):T},k.padAngle=function(t){return arguments.length?(E=typeof t==`function`?t:e(+t),k):E},k.context=function(e){return arguments.length?(D=e??null,k):D},k}export{x as t};
@@ -0,0 +1 @@
1
+ import"./chunk-NGNAAXSQ-C2Wy9Xmz.js";export{M as createArchitectureServices}from"./mermaid-parser.core-Dcxlzc-S.js";