@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.
- package/README.md +44 -55
- package/dist/src/cli.js +740 -194
- package/dist/src/comments.js +109 -6
- package/dist/src/diff.js +11 -0
- package/dist/src/evolve.js +123 -0
- package/dist/src/git.js +12 -0
- package/dist/src/host.js +39 -8
- package/dist/src/install.js +21 -0
- package/dist/src/plan.js +1 -1
- package/dist/src/prompt.js +384 -51
- package/dist/src/server.js +195 -67
- package/dist/src/session.js +53 -12
- package/dist/ui/dist/assets/abnfDiagram-WEYOIA3C-byISxePP.js +1 -0
- package/dist/ui/dist/assets/arc-CBzsAsP2.js +1 -0
- package/dist/ui/dist/assets/architecture-WOLXFQ4H-0OSkIwdB.js +1 -0
- package/dist/ui/dist/assets/architectureDiagram-ZHVYKT53-ixHybNqO.js +36 -0
- package/dist/ui/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/ui/dist/assets/blockDiagram-RJAE7QA3-1F8XL_Ou.js +140 -0
- package/dist/ui/dist/assets/c4Diagram-XDPPPE54-D9PFBXnc.js +38 -0
- package/dist/ui/dist/assets/channel-B-fwsHf1.js +1 -0
- package/dist/ui/dist/assets/chunk-2E7WHVMD-zZdNfMkj.js +88 -0
- package/dist/ui/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/ui/dist/assets/chunk-4EMNKR2T-CTKoxSSj.js +1 -0
- package/dist/ui/dist/assets/chunk-55XCU76K-ifFnDlcS.js +1 -0
- package/dist/ui/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/ui/dist/assets/chunk-7S6XSTFO-BCMVdi_1.js +2 -0
- package/dist/ui/dist/assets/chunk-AQDTSKZB-DC-5Whju.js +72 -0
- package/dist/ui/dist/assets/chunk-DSNLSHJZ-DASbe_gb.js +127 -0
- package/dist/ui/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/dist/ui/dist/assets/chunk-HAGFOTED-CArytDjj.js +62 -0
- package/dist/ui/dist/assets/chunk-HNE4Q36P-Dnl-Wtud.js +1 -0
- package/dist/ui/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/dist/ui/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/ui/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/dist/ui/dist/assets/chunk-NQOKRVIF-wvSLaeaK.js +213 -0
- package/dist/ui/dist/assets/chunk-UHW7PFBP-CckaOGiQ.js +1 -0
- package/dist/ui/dist/assets/chunk-VQ5W62QI-kQBMDJ7V.js +1 -0
- package/dist/ui/dist/assets/chunk-WQQL4CDK-CMg6bbLU.js +10 -0
- package/dist/ui/dist/assets/chunk-XXDRQBXY-CYlFr19K.js +1 -0
- package/dist/ui/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/ui/dist/assets/chunk-YZO67HFG-CqkG1vVk.js +1 -0
- package/dist/ui/dist/assets/chunk-ZAUX2LTX-BUNEdx-e.js +1 -0
- package/dist/ui/dist/assets/chunk-ZCHMQSOR-CFTbvcrm.js +2 -0
- package/dist/ui/dist/assets/classDiagram-v2-4MYWR3UA-t2OdJ0LA.js +217 -0
- package/dist/ui/dist/assets/cose-bilkent-JH36ORCC-FGm030gc.js +1 -0
- package/dist/ui/dist/assets/cynefin-EF2NZ3EQ-BtU33Lsu.js +1 -0
- package/dist/ui/dist/assets/cynefinDiagram-WF24UDRP-DUr-I7hS.js +62 -0
- package/dist/ui/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/ui/dist/assets/dagre-NK4GIZVX-BWTHwJKt.js +4 -0
- package/dist/ui/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/ui/dist/assets/diagram-CBK3MWTC-PcHrsEBn.js +24 -0
- package/dist/ui/dist/assets/diagram-I5XIIV2D-CkGtKjYj.js +24 -0
- package/dist/ui/dist/assets/diagram-ODIL2UCW-B_HdBB1w.js +30 -0
- package/dist/ui/dist/assets/diagram-PFBEJUXE-DL9zayE6.js +3 -0
- package/dist/ui/dist/assets/diagram-RJA3I4VV-By5wl8nK.js +41 -0
- package/dist/ui/dist/assets/diagram-TYUNFSNR-ziWPVayL.js +200 -0
- package/dist/ui/dist/assets/dist-NDVqCM7k.js +1 -0
- package/dist/ui/dist/assets/ebnfDiagram-V4P3TTPT-DH4YGkPc.js +1 -0
- package/dist/ui/dist/assets/elk-QONTBCTQ-BL2RlCEv.js +27 -0
- package/dist/ui/dist/assets/erDiagram-KYQIGQAA-BKbTMaRp.js +99 -0
- package/dist/ui/dist/assets/eventmodeling-K75KTNOO-Dk5RCWc-.js +1 -0
- package/dist/ui/dist/assets/flowDiagram-IUGLLWBU-Dkjb3PCK.js +1 -0
- package/dist/ui/dist/assets/ganttDiagram-R7GPNTJY-3vM9gxeT.js +292 -0
- package/dist/ui/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/ui/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/ui/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/ui/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/ui/dist/assets/gitGraph-VSP46ZUC-CXGFhz54.js +1 -0
- package/dist/ui/dist/assets/gitGraphDiagram-PDYL7XHY-Cl4fwUuA.js +106 -0
- package/dist/ui/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/ui/dist/assets/index-B8TYcxF8.css +1 -0
- package/dist/ui/dist/assets/index-CBbx_pJk.js +134 -0
- package/dist/ui/dist/assets/info-OHQRW6UA-DfSsZ8ib.js +1 -0
- package/dist/ui/dist/assets/infoDiagram-MVDRKAX5-B_7tyrL5.js +2 -0
- package/dist/ui/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/ui/dist/assets/ishikawaDiagram-HPEHV2GE-DUvHljpg.js +70 -0
- package/dist/ui/dist/assets/journeyDiagram-BOSHW7O7-BCgqzrb8.js +139 -0
- package/dist/ui/dist/assets/kanban-definition-FNSA4HC5-DOzZ3NmE.js +89 -0
- package/dist/ui/dist/assets/katex-B_JcIwqj.js +257 -0
- package/dist/ui/dist/assets/line-B-mPFVll.js +1 -0
- package/dist/ui/dist/assets/linear-k7o0jout.js +1 -0
- package/dist/ui/dist/assets/mermaid-parser.core-CAyLSQpH.js +7 -0
- package/dist/ui/dist/assets/mermaid.core-vfVMjgx2.js +44 -0
- package/dist/ui/dist/assets/mindmap-definition-6OY57UG3-B6y-B_r4.js +98 -0
- package/dist/ui/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/ui/dist/assets/packet-JDAUHWVQ-C_r4ed30.js +1 -0
- package/dist/ui/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/ui/dist/assets/pegDiagram-ECIUJNBX-B8VUm3Bh.js +1 -0
- package/dist/ui/dist/assets/pie-XZMESJXO-DcwtbH9y.js +1 -0
- package/dist/ui/dist/assets/pieDiagram-GNMPY74T-CvUNBi2F.js +39 -0
- package/dist/ui/dist/assets/prism-bash-D6zCJ74D.js +1 -0
- package/dist/ui/dist/assets/prism-c-04YixN25.js +1 -0
- package/dist/ui/dist/assets/prism-clike-DapgWyxx.js +1 -0
- package/dist/ui/dist/assets/prism-core-_2ju85Ya.js +1 -0
- package/dist/ui/dist/assets/prism-cpp-C-lJbC-6.js +1 -0
- package/dist/ui/dist/assets/prism-css-CPgOzxXp.js +1 -0
- package/dist/ui/dist/assets/prism-go-C_8qAH5n.js +1 -0
- package/dist/ui/dist/assets/prism-java-6wtusvIl.js +1 -0
- package/dist/ui/dist/assets/prism-javascript-BnO-swvr.js +1 -0
- package/dist/ui/dist/assets/prism-json-Dp_-W-Hv.js +1 -0
- package/dist/ui/dist/assets/prism-jsx-D-D0NWtC.js +1 -0
- package/dist/ui/dist/assets/prism-kotlin-BUlJgC-o.js +1 -0
- package/dist/ui/dist/assets/prism-markdown-Bc9-36OC.js +1 -0
- package/dist/ui/dist/assets/prism-markup-Cp04rIa1.js +1 -0
- package/dist/ui/dist/assets/prism-markup-templating-KkSMBj6Y.js +1 -0
- package/dist/ui/dist/assets/prism-php-Co4fM40K.js +1 -0
- package/dist/ui/dist/assets/prism-python-CfiSVtM6.js +1 -0
- package/dist/ui/dist/assets/prism-ruby-DFCASkuD.js +1 -0
- package/dist/ui/dist/assets/prism-rust-Bu9N8X3N.js +1 -0
- package/dist/ui/dist/assets/prism-sql-CYhVDMEE.js +1 -0
- package/dist/ui/dist/assets/prism-swift-B0kWc-ne.js +1 -0
- package/dist/ui/dist/assets/prism-toml-DKb54GAT.js +1 -0
- package/dist/ui/dist/assets/prism-tsx-p6J2Kc_4.js +1 -0
- package/dist/ui/dist/assets/prism-typescript-BMqONzxf.js +1 -0
- package/dist/ui/dist/assets/prism-yaml-C0JHu8gr.js +1 -0
- package/dist/ui/dist/assets/quadrantDiagram-D57K4MC3-Dg2eVS3j.js +7 -0
- package/dist/ui/dist/assets/radar-ABXABTNO-xCNTcZE9.js +1 -0
- package/dist/ui/dist/assets/railroad-I3PHUGI6-BI-sIL2Q.js +1 -0
- package/dist/ui/dist/assets/railroad-abnf-LNEFI6M7-Ddvk40Wx.js +1 -0
- package/dist/ui/dist/assets/railroad-ebnf-DWYD2UWJ-h35gE183.js +1 -0
- package/dist/ui/dist/assets/railroad-peg-JR7BVNR7-Zim9iPdp.js +1 -0
- package/dist/ui/dist/assets/railroadDiagram-Y72NRBL7-Cz6fdmOQ.js +1 -0
- package/dist/ui/dist/assets/requirementDiagram-EQYSW5RA-DTJkCrcD.js +84 -0
- package/dist/ui/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/ui/dist/assets/sankeyDiagram-SY24JHO5-Bg1iiVla.js +40 -0
- package/dist/ui/dist/assets/sequenceDiagram-BLEHNHQU-9XfetsRA.js +169 -0
- package/dist/ui/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/ui/dist/assets/src-Dym5afQ5.js +1 -0
- package/dist/ui/dist/assets/stateDiagram-v2-A6IESKWZ-CMmcMV8z.js +291 -0
- package/dist/ui/dist/assets/swimlanes-B3ENRKFQ-B2PcshTY.js +1 -0
- package/dist/ui/dist/assets/swimlanesDiagram-AYQRV4AK-DfB3XTTn.js +8 -0
- package/dist/ui/dist/assets/timeline-definition-JXYKGLGB-Cr1hCNtW.js +120 -0
- package/dist/ui/dist/assets/treeView-D4JQ5SDB-jH5ksKnh.js +1 -0
- package/dist/ui/dist/assets/treemap-SAJKECNS-BV90e8QL.js +1 -0
- package/dist/ui/dist/assets/usecaseDiagram-WPOLBNNX-BNwq5aJV.js +328 -0
- package/dist/ui/dist/assets/vennDiagram-Q3Y3IDCW-D-lLyZCL.js +34 -0
- package/dist/ui/dist/assets/wardley-7MLQ67FV-CWfHQUGt.js +1 -0
- package/dist/ui/dist/assets/wardleyDiagram-IVAIDKWA-BL_qNKVd.js +78 -0
- package/dist/ui/dist/assets/xychartDiagram-4URNHDU4-BHPK5Snp.js +7 -0
- package/dist/ui/dist/index.html +15 -0
- package/dist/ui/dist/style.css +121 -0
- package/package.json +31 -10
- package/skills/follow-me/SKILL.md +19 -89
- package/dist/src/anthropic.js +0 -133
- package/dist/ui/app.js +0 -2201
- package/dist/ui/concerns.js +0 -165
- package/dist/ui/highlight.js +0 -221
- package/dist/ui/index.html +0 -94
- package/dist/ui/live.js +0 -184
- package/dist/ui/playback.js +0 -579
- package/dist/ui/questions.js +0 -189
- package/dist/ui/style.css +0 -1331
- package/dist/ui/summary.js +0 -45
package/dist/src/comments.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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();
|
package/dist/src/install.js
CHANGED
|
@@ -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;
|