@shiyuphan/follow-me 0.2.0 → 0.4.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 (154) hide show
  1. package/README.md +44 -55
  2. package/dist/src/cli.js +740 -194
  3. package/dist/src/comments.js +109 -6
  4. package/dist/src/diff.js +11 -0
  5. package/dist/src/evolve.js +123 -0
  6. package/dist/src/git.js +12 -0
  7. package/dist/src/host.js +39 -8
  8. package/dist/src/install.js +21 -0
  9. package/dist/src/plan.js +1 -1
  10. package/dist/src/prompt.js +384 -51
  11. package/dist/src/server.js +195 -67
  12. package/dist/src/session.js +53 -12
  13. package/dist/ui/dist/assets/abnfDiagram-WEYOIA3C-byISxePP.js +1 -0
  14. package/dist/ui/dist/assets/arc-CBzsAsP2.js +1 -0
  15. package/dist/ui/dist/assets/architecture-WOLXFQ4H-0OSkIwdB.js +1 -0
  16. package/dist/ui/dist/assets/architectureDiagram-ZHVYKT53-ixHybNqO.js +36 -0
  17. package/dist/ui/dist/assets/array-BifhSqXX.js +1 -0
  18. package/dist/ui/dist/assets/blockDiagram-RJAE7QA3-1F8XL_Ou.js +140 -0
  19. package/dist/ui/dist/assets/c4Diagram-XDPPPE54-D9PFBXnc.js +38 -0
  20. package/dist/ui/dist/assets/channel-B-fwsHf1.js +1 -0
  21. package/dist/ui/dist/assets/chunk-2E7WHVMD-zZdNfMkj.js +88 -0
  22. package/dist/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  23. package/dist/ui/dist/assets/chunk-4EMNKR2T-CTKoxSSj.js +1 -0
  24. package/dist/ui/dist/assets/chunk-55XCU76K-ifFnDlcS.js +1 -0
  25. package/dist/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  26. package/dist/ui/dist/assets/chunk-7S6XSTFO-BCMVdi_1.js +2 -0
  27. package/dist/ui/dist/assets/chunk-AQDTSKZB-DC-5Whju.js +72 -0
  28. package/dist/ui/dist/assets/chunk-DSNLSHJZ-DASbe_gb.js +127 -0
  29. package/dist/ui/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  30. package/dist/ui/dist/assets/chunk-HAGFOTED-CArytDjj.js +62 -0
  31. package/dist/ui/dist/assets/chunk-HNE4Q36P-Dnl-Wtud.js +1 -0
  32. package/dist/ui/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  33. package/dist/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  34. package/dist/ui/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  35. package/dist/ui/dist/assets/chunk-NQOKRVIF-wvSLaeaK.js +213 -0
  36. package/dist/ui/dist/assets/chunk-UHW7PFBP-CckaOGiQ.js +1 -0
  37. package/dist/ui/dist/assets/chunk-VQ5W62QI-kQBMDJ7V.js +1 -0
  38. package/dist/ui/dist/assets/chunk-WQQL4CDK-CMg6bbLU.js +10 -0
  39. package/dist/ui/dist/assets/chunk-XXDRQBXY-CYlFr19K.js +1 -0
  40. package/dist/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  41. package/dist/ui/dist/assets/chunk-YZO67HFG-CqkG1vVk.js +1 -0
  42. package/dist/ui/dist/assets/chunk-ZAUX2LTX-BUNEdx-e.js +1 -0
  43. package/dist/ui/dist/assets/chunk-ZCHMQSOR-CFTbvcrm.js +2 -0
  44. package/dist/ui/dist/assets/classDiagram-v2-4MYWR3UA-t2OdJ0LA.js +217 -0
  45. package/dist/ui/dist/assets/cose-bilkent-JH36ORCC-FGm030gc.js +1 -0
  46. package/dist/ui/dist/assets/cynefin-EF2NZ3EQ-BtU33Lsu.js +1 -0
  47. package/dist/ui/dist/assets/cynefinDiagram-WF24UDRP-DUr-I7hS.js +62 -0
  48. package/dist/ui/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  49. package/dist/ui/dist/assets/dagre-NK4GIZVX-BWTHwJKt.js +4 -0
  50. package/dist/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  51. package/dist/ui/dist/assets/diagram-CBK3MWTC-PcHrsEBn.js +24 -0
  52. package/dist/ui/dist/assets/diagram-I5XIIV2D-CkGtKjYj.js +24 -0
  53. package/dist/ui/dist/assets/diagram-ODIL2UCW-B_HdBB1w.js +30 -0
  54. package/dist/ui/dist/assets/diagram-PFBEJUXE-DL9zayE6.js +3 -0
  55. package/dist/ui/dist/assets/diagram-RJA3I4VV-By5wl8nK.js +41 -0
  56. package/dist/ui/dist/assets/diagram-TYUNFSNR-ziWPVayL.js +200 -0
  57. package/dist/ui/dist/assets/dist-NDVqCM7k.js +1 -0
  58. package/dist/ui/dist/assets/ebnfDiagram-V4P3TTPT-DH4YGkPc.js +1 -0
  59. package/dist/ui/dist/assets/elk-QONTBCTQ-BL2RlCEv.js +27 -0
  60. package/dist/ui/dist/assets/erDiagram-KYQIGQAA-BKbTMaRp.js +99 -0
  61. package/dist/ui/dist/assets/eventmodeling-K75KTNOO-Dk5RCWc-.js +1 -0
  62. package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Dkjb3PCK.js +1 -0
  63. package/dist/ui/dist/assets/ganttDiagram-R7GPNTJY-3vM9gxeT.js +292 -0
  64. package/dist/ui/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  65. package/dist/ui/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  66. package/dist/ui/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  67. package/dist/ui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  68. package/dist/ui/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  69. package/dist/ui/dist/assets/gitGraph-VSP46ZUC-CXGFhz54.js +1 -0
  70. package/dist/ui/dist/assets/gitGraphDiagram-PDYL7XHY-Cl4fwUuA.js +106 -0
  71. package/dist/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
  72. package/dist/ui/dist/assets/index-B8TYcxF8.css +1 -0
  73. package/dist/ui/dist/assets/index-CBbx_pJk.js +134 -0
  74. package/dist/ui/dist/assets/info-OHQRW6UA-DfSsZ8ib.js +1 -0
  75. package/dist/ui/dist/assets/infoDiagram-MVDRKAX5-B_7tyrL5.js +2 -0
  76. package/dist/ui/dist/assets/init-C-OQMol4.js +1 -0
  77. package/dist/ui/dist/assets/ishikawaDiagram-HPEHV2GE-DUvHljpg.js +70 -0
  78. package/dist/ui/dist/assets/journeyDiagram-BOSHW7O7-BCgqzrb8.js +139 -0
  79. package/dist/ui/dist/assets/kanban-definition-FNSA4HC5-DOzZ3NmE.js +89 -0
  80. package/dist/ui/dist/assets/katex-B_JcIwqj.js +257 -0
  81. package/dist/ui/dist/assets/line-B-mPFVll.js +1 -0
  82. package/dist/ui/dist/assets/linear-k7o0jout.js +1 -0
  83. package/dist/ui/dist/assets/mermaid-parser.core-CAyLSQpH.js +7 -0
  84. package/dist/ui/dist/assets/mermaid.core-vfVMjgx2.js +44 -0
  85. package/dist/ui/dist/assets/mindmap-definition-6OY57UG3-B6y-B_r4.js +98 -0
  86. package/dist/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  87. package/dist/ui/dist/assets/packet-JDAUHWVQ-C_r4ed30.js +1 -0
  88. package/dist/ui/dist/assets/path-fybaL0A-.js +1 -0
  89. package/dist/ui/dist/assets/pegDiagram-ECIUJNBX-B8VUm3Bh.js +1 -0
  90. package/dist/ui/dist/assets/pie-XZMESJXO-DcwtbH9y.js +1 -0
  91. package/dist/ui/dist/assets/pieDiagram-GNMPY74T-CvUNBi2F.js +39 -0
  92. package/dist/ui/dist/assets/prism-bash-D6zCJ74D.js +1 -0
  93. package/dist/ui/dist/assets/prism-c-04YixN25.js +1 -0
  94. package/dist/ui/dist/assets/prism-clike-DapgWyxx.js +1 -0
  95. package/dist/ui/dist/assets/prism-core-_2ju85Ya.js +1 -0
  96. package/dist/ui/dist/assets/prism-cpp-C-lJbC-6.js +1 -0
  97. package/dist/ui/dist/assets/prism-css-CPgOzxXp.js +1 -0
  98. package/dist/ui/dist/assets/prism-go-C_8qAH5n.js +1 -0
  99. package/dist/ui/dist/assets/prism-java-6wtusvIl.js +1 -0
  100. package/dist/ui/dist/assets/prism-javascript-BnO-swvr.js +1 -0
  101. package/dist/ui/dist/assets/prism-json-Dp_-W-Hv.js +1 -0
  102. package/dist/ui/dist/assets/prism-jsx-D-D0NWtC.js +1 -0
  103. package/dist/ui/dist/assets/prism-kotlin-BUlJgC-o.js +1 -0
  104. package/dist/ui/dist/assets/prism-markdown-Bc9-36OC.js +1 -0
  105. package/dist/ui/dist/assets/prism-markup-Cp04rIa1.js +1 -0
  106. package/dist/ui/dist/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
  107. package/dist/ui/dist/assets/prism-php-Co4fM40K.js +1 -0
  108. package/dist/ui/dist/assets/prism-python-CfiSVtM6.js +1 -0
  109. package/dist/ui/dist/assets/prism-ruby-DFCASkuD.js +1 -0
  110. package/dist/ui/dist/assets/prism-rust-Bu9N8X3N.js +1 -0
  111. package/dist/ui/dist/assets/prism-sql-CYhVDMEE.js +1 -0
  112. package/dist/ui/dist/assets/prism-swift-B0kWc-ne.js +1 -0
  113. package/dist/ui/dist/assets/prism-toml-DKb54GAT.js +1 -0
  114. package/dist/ui/dist/assets/prism-tsx-p6J2Kc_4.js +1 -0
  115. package/dist/ui/dist/assets/prism-typescript-BMqONzxf.js +1 -0
  116. package/dist/ui/dist/assets/prism-yaml-C0JHu8gr.js +1 -0
  117. package/dist/ui/dist/assets/quadrantDiagram-D57K4MC3-Dg2eVS3j.js +7 -0
  118. package/dist/ui/dist/assets/radar-ABXABTNO-xCNTcZE9.js +1 -0
  119. package/dist/ui/dist/assets/railroad-I3PHUGI6-BI-sIL2Q.js +1 -0
  120. package/dist/ui/dist/assets/railroad-abnf-LNEFI6M7-Ddvk40Wx.js +1 -0
  121. package/dist/ui/dist/assets/railroad-ebnf-DWYD2UWJ-h35gE183.js +1 -0
  122. package/dist/ui/dist/assets/railroad-peg-JR7BVNR7-Zim9iPdp.js +1 -0
  123. package/dist/ui/dist/assets/railroadDiagram-Y72NRBL7-Cz6fdmOQ.js +1 -0
  124. package/dist/ui/dist/assets/requirementDiagram-EQYSW5RA-DTJkCrcD.js +84 -0
  125. package/dist/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  126. package/dist/ui/dist/assets/sankeyDiagram-SY24JHO5-Bg1iiVla.js +40 -0
  127. package/dist/ui/dist/assets/sequenceDiagram-BLEHNHQU-9XfetsRA.js +169 -0
  128. package/dist/ui/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  129. package/dist/ui/dist/assets/src-Dym5afQ5.js +1 -0
  130. package/dist/ui/dist/assets/stateDiagram-v2-A6IESKWZ-CMmcMV8z.js +291 -0
  131. package/dist/ui/dist/assets/swimlanes-B3ENRKFQ-B2PcshTY.js +1 -0
  132. package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DfB3XTTn.js +8 -0
  133. package/dist/ui/dist/assets/timeline-definition-JXYKGLGB-Cr1hCNtW.js +120 -0
  134. package/dist/ui/dist/assets/treeView-D4JQ5SDB-jH5ksKnh.js +1 -0
  135. package/dist/ui/dist/assets/treemap-SAJKECNS-BV90e8QL.js +1 -0
  136. package/dist/ui/dist/assets/usecaseDiagram-WPOLBNNX-BNwq5aJV.js +328 -0
  137. package/dist/ui/dist/assets/vennDiagram-Q3Y3IDCW-D-lLyZCL.js +34 -0
  138. package/dist/ui/dist/assets/wardley-7MLQ67FV-CWfHQUGt.js +1 -0
  139. package/dist/ui/dist/assets/wardleyDiagram-IVAIDKWA-BL_qNKVd.js +78 -0
  140. package/dist/ui/dist/assets/xychartDiagram-4URNHDU4-BHPK5Snp.js +7 -0
  141. package/dist/ui/dist/index.html +15 -0
  142. package/dist/ui/dist/style.css +121 -0
  143. package/package.json +31 -10
  144. package/skills/follow-me/SKILL.md +19 -89
  145. package/dist/src/anthropic.js +0 -133
  146. package/dist/ui/app.js +0 -2201
  147. package/dist/ui/concerns.js +0 -165
  148. package/dist/ui/highlight.js +0 -221
  149. package/dist/ui/index.html +0 -94
  150. package/dist/ui/live.js +0 -184
  151. package/dist/ui/playback.js +0 -579
  152. package/dist/ui/questions.js +0 -189
  153. package/dist/ui/style.css +0 -1331
  154. package/dist/ui/summary.js +0 -45
@@ -14,8 +14,9 @@ function isIndex(v) {
14
14
  return typeof v === "number" && Number.isInteger(v) && v >= 0;
15
15
  }
16
16
  function parseAnchor(v) {
17
- if (!isFields(v))
17
+ if (!isFields(v)) {
18
18
  return "anchor is not an object";
19
+ }
19
20
  if (v.kind === "plan")
20
21
  return { kind: "plan" };
21
22
  if (!isIndex(v.commit))
@@ -27,12 +28,25 @@ function parseAnchor(v) {
27
28
  return "anchor.kind is unknown";
28
29
  if (typeof v.unit !== "string" || !isUnitId(v.unit))
29
30
  return "anchor.unit is not a unit id";
30
- if (v.changes === null)
31
+ if (v.changes === null) {
32
+ if (v.offsets !== undefined)
33
+ return "anchor.offsets needs anchor.changes to be a list";
31
34
  return { kind: "hunk", unit: v.unit, commit, changes: null };
35
+ }
32
36
  if (!isArray(v.changes) || !v.changes.every(isIndex)) {
33
37
  return "anchor.changes is not null or a list of change indices";
34
38
  }
35
- return { kind: "hunk", unit: v.unit, commit, changes: v.changes.filter(isIndex) };
39
+ const changes = v.changes.filter(isIndex);
40
+ if (v.offsets === undefined)
41
+ return { kind: "hunk", unit: v.unit, commit, changes };
42
+ if (!isArray(v.offsets) || v.offsets.length === 0 || !v.offsets.every(isIndex)) {
43
+ return "anchor.offsets is not a non-empty list of row indices";
44
+ }
45
+ const offsets = v.offsets.filter(isIndex);
46
+ if (!offsets.every((o, i) => i === 0 || o > (offsets[i - 1] ?? 0))) {
47
+ return "anchor.offsets is not strictly ascending";
48
+ }
49
+ return { kind: "hunk", unit: v.unit, commit, changes, offsets };
36
50
  }
37
51
  /** A stored reply. Replies a client posts go through parseNewReply, which mints id and time. */
38
52
  export function parseReply(v) {
@@ -101,6 +115,17 @@ export function parseComment(v) {
101
115
  }
102
116
  if (v.kind !== undefined && v.kind !== "question")
103
117
  return 'kind is not "question"';
118
+ if (v.author !== undefined && v.author !== "agent")
119
+ return 'author is not "agent"';
120
+ if (v.diagram !== undefined) {
121
+ if (typeof v.diagram !== "string")
122
+ return "diagram is not a string";
123
+ if (v.author !== "agent")
124
+ return "diagram is only for an agent's comment";
125
+ const problem = diagramProblem(v.diagram);
126
+ if (problem !== null)
127
+ return `diagram: ${problem}`;
128
+ }
104
129
  const follows = v.follows;
105
130
  if (follows !== undefined && !(typeof follows === "string" && isCommentId(follows))) {
106
131
  return "follows is not a comment id";
@@ -125,6 +150,8 @@ export function parseComment(v) {
125
150
  ...(follows === undefined ? {} : { follows }),
126
151
  ...(severity === undefined ? {} : { severity }),
127
152
  ...(replies.length === 0 ? {} : { replies }),
153
+ ...(v.author === "agent" ? { author: "agent" } : {}),
154
+ ...(v.diagram === undefined ? {} : { diagram: v.diagram }),
128
155
  };
129
156
  }
130
157
  function withDecision(concern, v) {
@@ -135,11 +162,15 @@ function withDecision(concern, v) {
135
162
  return "severity is unknown";
136
163
  if (v.note !== undefined && typeof v.note !== "string")
137
164
  return "note is not a string";
165
+ if (v.decidedAt !== undefined && typeof v.decidedAt !== "string") {
166
+ return "decidedAt is not a string";
167
+ }
138
168
  return {
139
169
  concern,
140
170
  decision: v.decision,
141
171
  ...(v.severity === undefined ? {} : { severity: v.severity }),
142
172
  ...(v.note === undefined || v.note.trim() === "" ? {} : { note: v.note }),
173
+ ...(v.decidedAt === undefined ? {} : { decidedAt: v.decidedAt }),
143
174
  };
144
175
  }
145
176
  /** A stored decision. */
@@ -150,11 +181,11 @@ export function parseDecision(v) {
150
181
  return "concern is not a concern id";
151
182
  return withDecision(v.concern, v);
152
183
  }
153
- /** The body of POST .../concerns/<id>; the id comes from the URL. */
184
+ /** The body of POST .../concerns/<id>; the id comes from the URL and the time is the server's. */
154
185
  export function parseNewDecision(concern, v) {
155
186
  if (!isFields(v))
156
187
  return "the decision is not an object";
157
- return withDecision(concern, v);
188
+ return withDecision(concern, { ...v, decidedAt: undefined });
158
189
  }
159
190
  /** Absent reads as none. The first item that does not parse names its index and reason. */
160
191
  function parseList(raw, parse, what) {
@@ -171,12 +202,67 @@ function parseList(raw, parse, what) {
171
202
  }
172
203
  return out;
173
204
  }
205
+ function parseConcernId(item) {
206
+ return isConcernId(item) ? item : "not a concern id";
207
+ }
208
+ /** Absent reads as nothing excluded. */
209
+ export function parseExcluded(v) {
210
+ if (v === undefined)
211
+ return { comments: [], concerns: [] };
212
+ if (!isFields(v))
213
+ return "excluded is not an object";
214
+ const comments = parseList(v.comments, parseCommentId, "excluded.comments");
215
+ if (typeof comments === "string")
216
+ return comments;
217
+ const concerns = parseList(v.concerns, parseConcernId, "excluded.concerns");
218
+ return typeof concerns === "string" ? concerns : { comments, concerns };
219
+ }
174
220
  function asCommentId(item) {
175
221
  return typeof item === "string" && isCommentId(item) ? item : null;
176
222
  }
177
223
  function parseCommentId(item) {
178
224
  return asCommentId(item) ?? "not a comment id";
179
225
  }
226
+ function parseEarlierReplies(v) {
227
+ if (!isFields(v))
228
+ return "not an object";
229
+ const comment = asCommentId(v.comment);
230
+ if (comment === null)
231
+ return "comment is not a comment id";
232
+ const replies = parseList(v.replies, parseReply, "replies");
233
+ return typeof replies === "string" ? replies : { comment, replies };
234
+ }
235
+ function isLine(v) {
236
+ return typeof v === "number" && Number.isInteger(v) && v >= 1;
237
+ }
238
+ export function parseCarried(v) {
239
+ if (!isFields(v)) {
240
+ return "not an object";
241
+ }
242
+ const comment = parseComment(v.comment);
243
+ if (typeof comment === "string")
244
+ return comment;
245
+ const { unit, offsets, changed, path, newLine } = v;
246
+ if (unit !== null && !(typeof unit === "string" && isUnitId(unit)))
247
+ return "unit is not a unit id";
248
+ if (offsets !== null && !(isArray(offsets) && offsets.every(isIndex))) {
249
+ return "offsets is not null or a list of rows";
250
+ }
251
+ if (typeof changed !== "boolean")
252
+ return "changed is not a boolean";
253
+ if (path !== null && !(typeof path === "string" && isRepoPath(path)))
254
+ return "path is not a path";
255
+ if (newLine !== null && !isLine(newLine))
256
+ return "newLine is not null or a line";
257
+ return {
258
+ comment,
259
+ unit,
260
+ offsets: offsets === null ? null : offsets.filter(isIndex),
261
+ changed,
262
+ path,
263
+ newLine,
264
+ };
265
+ }
180
266
  /** Boundary parse of reviews/vN.json. Files written before walk mode existed have no mode. */
181
267
  export function parseReview(v) {
182
268
  if (!isFields(v)) {
@@ -203,6 +289,13 @@ export function parseReview(v) {
203
289
  const resolved = parseList(v.resolved, parseCommentId, "resolved");
204
290
  if (typeof resolved === "string")
205
291
  return resolved;
292
+ const excluded = parseExcluded(v.excluded);
293
+ if (typeof excluded === "string")
294
+ return excluded;
295
+ const anyExcluded = excluded.comments.length + excluded.concerns.length > 0;
296
+ const earlier = parseList(v.earlierReplies, parseEarlierReplies, "earlierReplies");
297
+ if (typeof earlier === "string")
298
+ return earlier;
206
299
  return {
207
300
  schema: v.schema,
208
301
  mode,
@@ -212,6 +305,8 @@ export function parseReview(v) {
212
305
  comments,
213
306
  ...(concerns.length === 0 ? {} : { concerns }),
214
307
  ...(resolved.length === 0 ? {} : { resolved }),
308
+ ...(anyExcluded ? { excluded } : {}),
309
+ ...(earlier.length === 0 ? {} : { earlierReplies: earlier }),
215
310
  };
216
311
  }
217
312
  function lenient(items, parse) {
@@ -230,10 +325,13 @@ export function parseDraft(v) {
230
325
  if (!isFields(v) || !isArray(v.comments)) {
231
326
  return "not a review draft";
232
327
  }
328
+ const excluded = parseExcluded(v.excluded);
233
329
  return {
234
330
  comments: lenient(v.comments, parseComment),
235
331
  concerns: lenient(v.concerns, parseDecision),
236
332
  resolved: lenient(v.resolved, parseCommentId),
333
+ excluded: typeof excluded === "string" ? { comments: [], concerns: [] } : excluded,
334
+ earlierReplies: lenient(v.earlierReplies, parseEarlierReplies),
237
335
  };
238
336
  }
239
337
  export function parseEvent(v) {
@@ -278,8 +376,13 @@ export function parseEvent(v) {
278
376
  case "version": {
279
377
  if (!isPlanVersion(v.version) || typeof v.hash !== "string")
280
378
  return "malformed version event";
281
- return { seq, type: v.type, version: v.version, hash: v.hash };
379
+ const carried = parseList(v.carried, parseCarried, "carried");
380
+ if (typeof carried === "string")
381
+ return carried;
382
+ return { seq, type: v.type, version: v.version, hash: v.hash, carried };
282
383
  }
384
+ case "submitted":
385
+ return { seq, type: v.type };
283
386
  case "ended":
284
387
  return typeof v.review === "string"
285
388
  ? { seq, type: v.type, review: v.review }
package/dist/src/diff.js CHANGED
@@ -357,3 +357,14 @@ export function parseUnitRef(s) {
357
357
  }
358
358
  return { unit, select: { kind: "lines", changes } };
359
359
  }
360
+ /** Where the hunk starts in the tip: its base position shifted by earlier hunks of the file. */
361
+ export function tipStartOf(hunk, units) {
362
+ let shift = 0;
363
+ for (const u of units.values()) {
364
+ if (u.kind !== "hunk" || u.path !== hunk.path || u.oldStart >= hunk.oldStart)
365
+ continue;
366
+ for (const line of u.body)
367
+ shift += line.op === "+" ? 1 : line.op === "-" ? -1 : 0;
368
+ }
369
+ return hunk.oldStart + shift + 1;
370
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * How a plan follows its branch when the tip moves. A unit id hashes its path and hunk body, so
3
+ * an id that survives names the same change and keeps its place in the plan; everything else is
4
+ * either gone or new, and the agent decides where the new units go.
5
+ */
6
+ import { tipStartOf } from "./diff.js";
7
+ /** Pure. Responses start empty: they answer the review of the old version, not this one. */
8
+ export function evolvePlan(oldPlan, oldDiff, newDiff) {
9
+ const gone = new Map();
10
+ const commits = [];
11
+ oldPlan.commits.forEach((commit, index) => {
12
+ const units = commit.units.filter((ref) => newDiff.units.has(ref.unit));
13
+ for (const ref of commit.units) {
14
+ const unit = oldDiff.units.get(ref.unit);
15
+ if (newDiff.units.has(ref.unit) || unit === undefined)
16
+ continue;
17
+ const seen = gone.get(ref.unit)?.commits ?? [];
18
+ gone.set(ref.unit, { unit, commits: [...seen, { index, message: commit.message }] });
19
+ }
20
+ if (units.length > 0)
21
+ commits.push({ ...commit, units });
22
+ });
23
+ const taken = new Set(oldPlan.commits.flatMap((c) => c.units.map((ref) => ref.unit)));
24
+ return {
25
+ draft: {
26
+ schema: oldPlan.schema,
27
+ base: newDiff.base,
28
+ tip: newDiff.tip,
29
+ commits,
30
+ responses: [],
31
+ },
32
+ vanished: [...gone.values()],
33
+ added: [...newDiff.units.values()].filter((unit) => !taken.has(unit.id)),
34
+ };
35
+ }
36
+ /** The tip line of each body row; a removed row gets the line that follows it. */
37
+ function tipLines(hunk, units) {
38
+ let line = tipStartOf(hunk, units);
39
+ return hunk.body.map((row) => {
40
+ const at = line;
41
+ if (row.op !== "-")
42
+ line += 1;
43
+ return at;
44
+ });
45
+ }
46
+ /** The body row a hunk comment points at first: its first picked row, else its first change. */
47
+ function firstRow(comment, hunk) {
48
+ const { anchor } = comment;
49
+ if (anchor.kind !== "hunk")
50
+ return 0;
51
+ const picked = anchor.offsets?.[0];
52
+ if (picked !== undefined)
53
+ return picked;
54
+ const change = anchor.changes?.[0];
55
+ const row = hunk.body.findIndex((r) => r.op !== " " && r.change === change);
56
+ return change === undefined || row === -1 ? 0 : row;
57
+ }
58
+ /**
59
+ * Where an old line lands in a replacement hunk: the nearest row with the same text the tip
60
+ * keeps, else the old line clamped into the hunk's tip lines.
61
+ */
62
+ function landing(hunk, units, oldLine, text) {
63
+ const lines = tipLines(hunk, units);
64
+ const same = hunk.body.flatMap((row, i) => row.op !== "-" && row.text === text ? [lines[i] ?? oldLine] : []);
65
+ if (same.length > 0) {
66
+ return same.reduce((a, b) => (Math.abs(b - oldLine) < Math.abs(a - oldLine) ? b : a));
67
+ }
68
+ const [from, to] = span(hunk, units);
69
+ return Math.min(Math.max(oldLine, from), to);
70
+ }
71
+ /** First and last tip line of a hunk; a pure removal spans the one line after it. */
72
+ function span(hunk, units) {
73
+ const from = tipStartOf(hunk, units);
74
+ const kept = hunk.body.filter((row) => row.op !== "-").length;
75
+ return [from, from + Math.max(kept, 1) - 1];
76
+ }
77
+ function distance(hunk, units, line) {
78
+ const [from, to] = span(hunk, units);
79
+ return line < from ? from - line : line > to ? line - to : 0;
80
+ }
81
+ /**
82
+ * Pure. Places each comment of the previous version on the new diff: on its own unit when that
83
+ * hunk is unchanged, else on the hunk of the same path nearest its old tip line.
84
+ */
85
+ export function carryComments(comments, oldUnits, newUnits) {
86
+ return comments.map((comment) => carry(comment, oldUnits, newUnits));
87
+ }
88
+ function carry(comment, oldUnits, newUnits) {
89
+ const { anchor } = comment;
90
+ const nowhere = { comment, unit: null, offsets: null, newLine: null };
91
+ if (anchor.kind !== "hunk")
92
+ return { ...nowhere, changed: false, path: comment.context.path };
93
+ const kept = newUnits.get(anchor.unit);
94
+ if (kept !== undefined) {
95
+ const newLine = kept.kind === "hunk" ? (tipLines(kept, newUnits)[firstRow(comment, kept)] ?? null) : null;
96
+ return {
97
+ comment,
98
+ unit: kept.id,
99
+ offsets: anchor.offsets ?? null,
100
+ changed: false,
101
+ path: kept.path,
102
+ newLine,
103
+ };
104
+ }
105
+ const old = oldUnits.get(anchor.unit);
106
+ const path = old?.path ?? comment.context.path;
107
+ const onPath = [...newUnits.values()].filter((u) => u.path === path);
108
+ const first = onPath[0];
109
+ if (first === undefined)
110
+ return { ...nowhere, changed: true, path };
111
+ const moved = { comment, offsets: null, changed: true, path };
112
+ const hunks = onPath.filter((u) => u.kind === "hunk");
113
+ if (old?.kind !== "hunk" || hunks.length === 0)
114
+ return { ...moved, unit: first.id, newLine: null };
115
+ const row = firstRow(comment, old);
116
+ const oldLine = tipLines(old, oldUnits)[row] ?? tipStartOf(old, oldUnits);
117
+ const nearest = hunks.reduce((a, b) => distance(b, newUnits, oldLine) < distance(a, newUnits, oldLine) ? b : a);
118
+ return {
119
+ ...moved,
120
+ unit: nearest.id,
121
+ newLine: landing(nearest, newUnits, oldLine, old.body[row]?.text),
122
+ };
123
+ }
package/dist/src/git.js CHANGED
@@ -134,6 +134,18 @@ export async function loadDiff(repo, baseRef, tipRef) {
134
134
  const blobs = await catBlobs(repo, neededBlobs(rawZ, patchText));
135
135
  return parseDiff({ base, tip, baseTree, tipTree, rawZ, patch: patchText, blobs });
136
136
  }
137
+ /**
138
+ * Whether the configured user wrote every commit of base..tip, which is how follow-me tells its
139
+ * author's own branch from one it is asked to review. False when no user.email is set.
140
+ */
141
+ export async function isOwnWork(repo, base, tip) {
142
+ const me = await gitText(repo, ["config", "--default=", "user.email"]);
143
+ if (me === "")
144
+ return false;
145
+ const authors = await gitText(repo, ["log", "--format=%ae", `${base}..${tip}`]);
146
+ const emails = authors.split("\n").filter((line) => line !== "");
147
+ return emails.length > 0 && emails.every((email) => email === me);
148
+ }
137
149
  export async function signing(repo, mode) {
138
150
  const configured = await gitText(repo, [
139
151
  "config",
package/dist/src/host.js CHANGED
@@ -3,10 +3,11 @@ import { randomUUID } from "node:crypto";
3
3
  import { closeSync, linkSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { parseEvent, parseEventBatch } from "./comments.js";
6
+ import { parseCarried, parseEvent, parseEventBatch } from "./comments.js";
7
+ import { carryComments } from "./evolve.js";
7
8
  import { isPlanVersion, serializePlanFile, validatePlan } from "./plan.js";
8
9
  import { HEALTH_APP, isSessionId, loadDraftComments, PortInUseError, RegistrationError, startServer, } from "./server.js";
9
- import { BranchMovedError, isAlive, openSessionAt, SessionFileError, SessionLockedError, } from "./session.js";
10
+ import { BranchMovedError, diffOfPlan, isAlive, openSessionAt, SessionFileError, SessionLockedError, } from "./session.js";
10
11
  const RECORD = "server.json";
11
12
  const DEFAULT_IDLE_MS = 30_000;
12
13
  const START_TIMEOUT_MS = 10_000;
@@ -125,7 +126,7 @@ async function loadHosted(registration, held) {
125
126
  if (!verdict.ok) {
126
127
  throw new RegistrationError(`plan v${version} no longer fits ${branch}; check it again`);
127
128
  }
128
- const reviewsDir = session.reviewsDir(mode);
129
+ const reviewsDir = session.reviewsDir();
129
130
  const comments = await loadDraftComments(reviewsDir, version);
130
131
  const prior = session.priorReview(mode, version);
131
132
  const previous = prior === null || prior.comments.length === 0
@@ -135,7 +136,12 @@ async function loadHosted(registration, held) {
135
136
  comments: prior.comments,
136
137
  responses: stored.file.responses,
137
138
  };
138
- const command = mode === "walk" ? "walk" : "review";
139
+ const priorPlan = session.plans().find((p) => p.version === previous?.version);
140
+ const before = priorPlan === undefined ? null : await diffOfPlan(session, priorPlan);
141
+ const carried = previous === null
142
+ ? []
143
+ : carryComments(previous.comments, before?.units ?? new Map(), session.diff.units);
144
+ const command = "review";
139
145
  // A version swap runs inside the server that already owns this session's lock.
140
146
  const release = held ??
141
147
  (lockToken === null ? session.lock(command) : session.adoptLock(lockToken, command)).release;
@@ -144,6 +150,7 @@ async function loadHosted(registration, held) {
144
150
  reviewsDir,
145
151
  release,
146
152
  previous,
153
+ carried,
147
154
  state: {
148
155
  mode,
149
156
  anchor: session.anchor,
@@ -250,14 +257,16 @@ async function call(port, method, path, body) {
250
257
  function parseSummary(raw) {
251
258
  if (!isFields(raw))
252
259
  return null;
253
- const { id, url, repo, branch, mode, version, comments } = raw;
260
+ const { id, url, repo, branch, mode, version, comments, openedAt } = raw;
254
261
  if (typeof url !== "string" || typeof repo !== "string")
255
262
  return null;
256
263
  if (typeof branch !== "string" || (mode !== "split" && mode !== "walk"))
257
264
  return null;
258
265
  if (!isSessionId(id) || !isPlanVersion(version) || typeof comments !== "number")
259
266
  return null;
260
- return { id, url, repo, branch, mode, version, comments };
267
+ if (typeof openedAt !== "string")
268
+ return null;
269
+ return { id, url, repo, branch, mode, version, comments, openedAt };
261
270
  }
262
271
  /** Registers a session with the server on this port; the same session yields the same URL. */
263
272
  export async function registerWith(port, registration) {
@@ -273,6 +282,19 @@ export async function swapVersion(port, summary, version, hash) {
273
282
  throw new RegistrationError("the server answered with a malformed session");
274
283
  return swapped;
275
284
  }
285
+ /** The previous version's comments as the open page places them on its plan. */
286
+ export async function fetchCarried(port, summary) {
287
+ const state = await call(port, "GET", `${summary.url}api/state`);
288
+ const raw = isFields(state) ? state["carried"] : undefined;
289
+ if (!Array.isArray(raw))
290
+ throw new RegistrationError("the server sent no carried comments");
291
+ return raw.flatMap((item) => {
292
+ const carried = parseCarried(item);
293
+ if (typeof carried === "string")
294
+ throw new RegistrationError(`carried: ${carried}`);
295
+ return [carried];
296
+ });
297
+ }
276
298
  /** One long poll of the session's event log: what happened after `since`, or nothing at timeout. */
277
299
  export async function pollEvents(port, summary, since, timeoutMs) {
278
300
  const path = `${summary.url}api/events?since=${since}&timeout=${timeoutMs}`;
@@ -317,9 +339,18 @@ export async function postReply(port, summary, commentId, reply) {
317
339
  throw new RegistrationError("the server answered without a reply id");
318
340
  return id;
319
341
  }
342
+ /** Posts a comment the agent starts and returns its id; the server mints id and time. */
343
+ export async function postComment(port, summary, comment) {
344
+ const body = await call(port, "POST", `${summary.url}api/comments`, comment);
345
+ const id = isFields(body) ? body["id"] : undefined;
346
+ if (typeof id !== "string") {
347
+ throw new RegistrationError("the server answered without a comment id");
348
+ }
349
+ return id;
350
+ }
320
351
  /** The consolidated report of the hosted session as it stands. */
321
- export async function fetchReport(port, summary) {
322
- const res = await fetch(`${sessionUrl(port, summary)}api/report`);
352
+ export async function fetchReport(port, summary, format = "markdown") {
353
+ const res = await fetch(`${sessionUrl(port, summary)}api/report?format=${format}`);
323
354
  if (!res.ok)
324
355
  throw new RegistrationError(`GET api/report failed with ${res.status}`);
325
356
  return res.text();
@@ -122,6 +122,27 @@ export function runInstall(options, home, repoRoot, say, skillDir = findSkillDir
122
122
  }
123
123
  });
124
124
  }
125
+ /**
126
+ * Keeps the skill current in every harness found at user scope: writes what is missing or differs
127
+ * from the shipped copy and says so, one line per file; says nothing when all is current. A host
128
+ * with no harness, or a package without its skill folder, is not an error here.
129
+ */
130
+ export function syncSkills(home, say, skillDir) {
131
+ try {
132
+ const dir = skillDir ?? findSkillDir();
133
+ const targets = { harness: undefined, all: false, project: false, dryRun: false };
134
+ for (const file of planSkillFiles(targets, home, home)) {
135
+ const line = writeSkill(dir, file, false);
136
+ if (line.startsWith("wrote "))
137
+ say(line);
138
+ }
139
+ }
140
+ catch (error) {
141
+ if (error instanceof InstallError)
142
+ return;
143
+ process.stderr.write(`follow-me: could not update the skill: ${error instanceof Error ? error.message : error}\n`);
144
+ }
145
+ }
125
146
  export function runUninstall(options, home, repoRoot, say) {
126
147
  return reportingErrors(() => {
127
148
  for (const file of planSkillFiles(options, home, repoRoot)) {
package/dist/src/plan.js CHANGED
@@ -645,7 +645,7 @@ export function validatePlan(diff, draft, opts = {}) {
645
645
  const warnings = review === null
646
646
  ? []
647
647
  : review.comments
648
- .filter((c) => !answered.has(c.id))
648
+ .filter((c) => c.author !== "agent" && !answered.has(c.id))
649
649
  .map((c) => ({ kind: "unanswered", comment: c.id, review: review.planVersion }));
650
650
  file.commits.forEach((c, commit) => {
651
651
  const length = (c.message.split("\n")[0] ?? "").length;