@link-assistant/hive-mind 1.52.1 → 1.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Centralized definitions for GitHub comments posted by solve.mjs itself
5
+ * (session bookkeeping, log uploads, auto-restart notices, etc.) — as
6
+ * opposed to comments posted by the AI agent via its own tool calls.
7
+ *
8
+ * Issue #1625: --auto-attach-solution-summary was broken because the tool's
9
+ * own "AI Work Session Started" / "Solution Draft Log" / "Ready to merge"
10
+ * comments counted as AI-authored comments, so the summary was always
11
+ * suppressed even when the AI session produced zero comments of its own.
12
+ *
13
+ * This module is the single source of truth for the marker strings embedded
14
+ * in those comments. Posting sites use these constants to *build* comment
15
+ * bodies; the summary filter uses the same constants to *detect* them. If a
16
+ * marker needs to change, changing it here updates both sides — no more
17
+ * duplicate literals drifting apart.
18
+ *
19
+ * It also provides in-memory tracking: any comment posted by solve.mjs can
20
+ * be registered by its numeric GitHub comment ID, and checkForAiCreatedComments
21
+ * uses that set as the *primary* filter (marker matching is the fallback for
22
+ * comments whose IDs were not captured, e.g. when `gh pr comment` didn't
23
+ * return JSON).
24
+ */
25
+
26
+ // ----------------------------------------------------------------------------
27
+ // Marker constants — single source of truth for comment header/keyphrase text.
28
+ // Each constant is the exact substring that both (a) appears in the posted
29
+ // comment body and (b) is searched for when filtering out tool-generated
30
+ // comments. Do NOT duplicate these literals elsewhere.
31
+ // ----------------------------------------------------------------------------
32
+
33
+ // solve.session.lib.mjs — startWorkSession() / endWorkSession()
34
+ export const AI_WORK_SESSION_STARTED_MARKER = 'AI Work Session Started';
35
+ export const AI_WORK_SESSION_COMPLETED_MARKER = 'AI Work Session Completed';
36
+ export const AI_WORK_SESSION_RESUMED_MARKER = 'AI Work Session Resumed';
37
+
38
+ // solve.session.lib.mjs — auto-resume / auto-restart on limit reset
39
+ export const AUTO_RESUME_ON_LIMIT_RESET_MARKER = 'Auto Resume (on limit reset)';
40
+ export const AUTO_RESTART_ON_LIMIT_RESET_MARKER = 'Auto Restart (on limit reset)';
41
+
42
+ // github.lib.mjs — attachLogToGitHub() success / resumed / truncated log comments
43
+ export const SOLUTION_DRAFT_LOG_MARKER = 'Solution Draft Log';
44
+
45
+ // solve.watch.lib.mjs / solve.auto-merge.lib.mjs — auto-restart notifications
46
+ export const AUTO_RESTART_MARKER = 'Auto-restart';
47
+ export const AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER = 'Auto-restart-until-mergeable Log';
48
+
49
+ // solve.auto-merge.lib.mjs — "ready to merge" status comments
50
+ export const READY_TO_MERGE_MARKER = 'Ready to merge';
51
+
52
+ // solve.auto-merge.lib.mjs — "auto-merged successfully" status comments
53
+ export const AUTO_MERGED_MARKER = 'Auto-merged';
54
+
55
+ // solve.auto-merge.lib.mjs — billing-limit notification (spending cap / free tier)
56
+ export const BILLING_LIMIT_MARKER = 'GitHub Actions Billing Limit';
57
+
58
+ // github.lib.mjs — fork contributor "Allow edits by maintainers" request
59
+ export const MAINTAINER_ACCESS_REQUEST_MARKER = 'Allow edits by maintainers';
60
+
61
+ // solve.progress-monitoring.lib.mjs — live-progress comment section markers.
62
+ // These are HTML comments so they don't render in the GitHub UI; they exist
63
+ // specifically to let the tool find its own comment later.
64
+ export const LIVE_PROGRESS_SECTION_START_MARKER = '<!-- LIVE-PROGRESS-START -->';
65
+ export const LIVE_PROGRESS_SECTION_END_MARKER = '<!-- LIVE-PROGRESS-END -->';
66
+
67
+ // claude.lib.mjs — "session force-killed due to stream timeout" notifications
68
+ export const SESSION_FORCE_KILLED_MARKER = 'Session Force-Killed';
69
+
70
+ // solve.repo-setup.lib.mjs / solve.repository.lib.mjs — issue comments posted
71
+ // when the target repository is empty / uninitialized so solving can't start.
72
+ export const REPOSITORY_INITIALIZATION_REQUIRED_MARKER = 'Repository Initialization Required';
73
+
74
+ // interactive-mode.lib.mjs — interactive mode session comments
75
+ export const INTERACTIVE_SESSION_STARTED_MARKER = 'Interactive session started';
76
+ export const INTERACTIVE_SESSION_ENDED_MARKER = 'Interactive session ended';
77
+
78
+ // github.lib.mjs — closing footer present in every log upload comment variant
79
+ export const NOW_WORKING_SESSION_IS_ENDED_MARKER = 'Now working session is ended';
80
+
81
+ // Failure-path markers (github.lib.mjs error paths)
82
+ export const SOLUTION_DRAFT_FAILED_MARKER = 'Solution Draft Failed';
83
+ export const SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER = 'Solution Draft Finished with Errors';
84
+ export const USAGE_LIMIT_REACHED_MARKER = 'Usage Limit Reached';
85
+
86
+ /**
87
+ * Every marker that identifies a tool-posted comment. Derived from the
88
+ * named constants above so that adding a new marker only requires adding
89
+ * the constant and appending it here.
90
+ */
91
+ export const TOOL_GENERATED_COMMENT_MARKERS = [AI_WORK_SESSION_STARTED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER, AI_WORK_SESSION_RESUMED_MARKER, AUTO_RESUME_ON_LIMIT_RESET_MARKER, AUTO_RESTART_ON_LIMIT_RESET_MARKER, SOLUTION_DRAFT_LOG_MARKER, AUTO_RESTART_MARKER, AUTO_RESTART_UNTIL_MERGEABLE_LOG_MARKER, READY_TO_MERGE_MARKER, AUTO_MERGED_MARKER, BILLING_LIMIT_MARKER, MAINTAINER_ACCESS_REQUEST_MARKER, LIVE_PROGRESS_SECTION_START_MARKER, SESSION_FORCE_KILLED_MARKER, REPOSITORY_INITIALIZATION_REQUIRED_MARKER, INTERACTIVE_SESSION_STARTED_MARKER, INTERACTIVE_SESSION_ENDED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER];
92
+
93
+ /**
94
+ * Markers that indicate the end of a working session. Used by
95
+ * solve.auto-merge-helpers.checkForExistingComment to scope the
96
+ * duplicate-search window to the current session only (Issue #1584).
97
+ */
98
+ export const SESSION_ENDING_MARKERS = [NOW_WORKING_SESSION_IS_ENDED_MARKER, AI_WORK_SESSION_COMPLETED_MARKER];
99
+
100
+ /**
101
+ * Determine whether a GitHub comment body matches any known tool-generated
102
+ * marker. Used as a fallback when a comment's ID was not captured by
103
+ * in-memory tracking (see below).
104
+ *
105
+ * @param {string} body - The comment body
106
+ * @returns {boolean} - True if the body contains a tool-generated marker
107
+ */
108
+ export const isToolGeneratedComment = body => {
109
+ if (!body || typeof body !== 'string') return false;
110
+ return TOOL_GENERATED_COMMENT_MARKERS.some(marker => body.includes(marker));
111
+ };
112
+
113
+ // ----------------------------------------------------------------------------
114
+ // In-memory tracking of comments posted by solve.mjs during this session.
115
+ //
116
+ // Every tool-initiated comment-post helper should register its comment ID
117
+ // via trackToolCommentId(). checkForAiCreatedComments() then uses the set
118
+ // as the primary filter, falling back to marker-based detection for any
119
+ // comment whose ID was not captured.
120
+ //
121
+ // IDs are GitHub numeric comment IDs (from issue/PR/review comment APIs),
122
+ // coerced to strings for consistent Set membership. Review (inline) comments
123
+ // and conversation comments share the same ID namespace at the API layer,
124
+ // but we never mix them since solve.mjs only posts to conversation + issue
125
+ // endpoints — review comments are AI-only.
126
+ // ----------------------------------------------------------------------------
127
+
128
+ const trackedToolCommentIds = new Set();
129
+
130
+ /**
131
+ * Register a comment ID as tool-generated. Safe to call with null/undefined
132
+ * (e.g., when comment posting failed or the ID couldn't be extracted).
133
+ * @param {string|number|null|undefined} commentId
134
+ */
135
+ export const trackToolCommentId = commentId => {
136
+ if (commentId === null || commentId === undefined) return;
137
+ trackedToolCommentIds.add(String(commentId));
138
+ };
139
+
140
+ /**
141
+ * Returns whether a given comment ID was posted by solve.mjs itself during
142
+ * this session.
143
+ * @param {string|number|null|undefined} commentId
144
+ * @returns {boolean}
145
+ */
146
+ export const isToolTrackedCommentId = commentId => {
147
+ if (commentId === null || commentId === undefined) return false;
148
+ return trackedToolCommentIds.has(String(commentId));
149
+ };
150
+
151
+ /**
152
+ * Returns the set of tracked comment IDs (read-only snapshot).
153
+ * Primarily for tests and diagnostics.
154
+ * @returns {Set<string>}
155
+ */
156
+ export const getTrackedToolCommentIds = () => new Set(trackedToolCommentIds);
157
+
158
+ /**
159
+ * Reset tracking state. Primarily for tests; solve.mjs does not need to
160
+ * call this between real sessions because each invocation is a fresh
161
+ * process.
162
+ */
163
+ export const resetTrackedToolCommentIds = () => {
164
+ trackedToolCommentIds.clear();
165
+ };
166
+
167
+ /**
168
+ * Post a GitHub comment on a PR or issue via `gh api` and return the
169
+ * numeric comment ID (as string). The ID is also automatically tracked in
170
+ * the in-memory set above.
171
+ *
172
+ * This is the preferred path for all tool-posted comments because `gh pr
173
+ * comment` / `gh issue comment` only print the comment URL to stdout, and
174
+ * extracting the numeric ID from a URL is brittle. `gh api POST` returns
175
+ * full JSON, from which the ID is trivial to extract.
176
+ *
177
+ * Falls back to best-effort URL parsing if JSON parsing fails, so a single
178
+ * API change cannot break the code path.
179
+ *
180
+ * @param {Object} options
181
+ * @param {Function} options.$ - command-stream tagged template (required — we
182
+ * accept it as a parameter so this module has no top-level dependency on
183
+ * `command-stream`, keeping it cheap to import from tests)
184
+ * @param {string} options.owner
185
+ * @param {string} options.repo
186
+ * @param {number|string} options.targetNumber - PR or issue number
187
+ * @param {string} options.body
188
+ * @returns {Promise<{ok: boolean, commentId: string|null, stderr?: string}>}
189
+ */
190
+ export const postTrackedComment = async ({ $, owner, repo, targetNumber, body }) => {
191
+ if (!$) {
192
+ throw new Error('postTrackedComment requires a command-stream $ helper');
193
+ }
194
+
195
+ // Use `gh api` with stdin to avoid shell-quoting problems on multi-line
196
+ // bodies and to get JSON back so we can extract the comment ID.
197
+ // We use the /issues/<n>/comments endpoint because it works identically
198
+ // for both PRs and issues (a PR is an issue at this endpoint).
199
+ const apiPath = `repos/${owner}/${repo}/issues/${targetNumber}/comments`;
200
+ const payload = JSON.stringify({ body });
201
+
202
+ // `gh api --input -` reads from stdin. command-stream supports .stdin(...)
203
+ // on the returned process handle (same API used in interactive-mode.lib.mjs
204
+ // via execFileAsync). We build the invocation through $ so callers can
205
+ // inject a mock.
206
+ let result;
207
+ try {
208
+ result = await $({ input: payload })`gh api ${apiPath} -X POST --input -`;
209
+ } catch (err) {
210
+ return { ok: false, commentId: null, stderr: err && err.message ? err.message : String(err) };
211
+ }
212
+
213
+ if (result.code !== 0) {
214
+ const stderr = result.stderr ? result.stderr.toString() : '';
215
+ return { ok: false, commentId: null, stderr };
216
+ }
217
+
218
+ const stdout = result.stdout ? result.stdout.toString() : '';
219
+ let commentId = null;
220
+ try {
221
+ const parsed = JSON.parse(stdout);
222
+ if (parsed && parsed.id !== undefined && parsed.id !== null) {
223
+ commentId = String(parsed.id);
224
+ }
225
+ } catch {
226
+ // Fallback: match numeric id in the JSON text, or the issuecomment-<n>
227
+ // fragment in the html_url, whichever shows up first.
228
+ const match = stdout.match(/"id"\s*:\s*(\d+)|issuecomment-(\d+)/);
229
+ if (match) commentId = match[1] || match[2] || null;
230
+ }
231
+
232
+ trackToolCommentId(commentId);
233
+
234
+ return { ok: true, commentId };
235
+ };
236
+
237
+ /**
238
+ * Post a GitHub comment whose body is already written to a file on disk.
239
+ * Used by attachLogToGitHub() where the comment body can be tens of KB
240
+ * (entire execution log embedded in a <details>) — too large for inline
241
+ * shell arguments and awkward to pipe as stdin JSON.
242
+ *
243
+ * Reads the file and posts via postTrackedComment() so the returned comment
244
+ * ID is tracked exactly like any other tool-posted comment. Kept separate
245
+ * from postTrackedComment so callers that already have a body string don't
246
+ * pay for a tempfile round-trip.
247
+ *
248
+ * @param {Object} options
249
+ * @param {Function} options.$ - command-stream tagged template
250
+ * @param {string} options.owner
251
+ * @param {string} options.repo
252
+ * @param {number|string} options.targetNumber
253
+ * @param {string} options.bodyFile - absolute path to the comment body file
254
+ * @returns {Promise<{ok: boolean, commentId: string|null, stderr?: string}>}
255
+ */
256
+ export const postTrackedCommentFromFile = async ({ $, owner, repo, targetNumber, bodyFile }) => {
257
+ if (!$) {
258
+ throw new Error('postTrackedCommentFromFile requires a command-stream $ helper');
259
+ }
260
+ if (typeof globalThis.use === 'undefined') {
261
+ globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
262
+ }
263
+ const fs = (await globalThis.use('fs')).promises;
264
+ let body;
265
+ try {
266
+ body = await fs.readFile(bodyFile, 'utf8');
267
+ } catch (err) {
268
+ return { ok: false, commentId: null, stderr: err && err.message ? err.message : String(err) };
269
+ }
270
+ return postTrackedComment({ $, owner, repo, targetNumber, body });
271
+ };