@opvs-ai/cli 0.27.0 → 0.27.2

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 CHANGED
@@ -195,6 +195,31 @@ opvs scheduled-runs cancel <id> --yes # only a run that has not fired
195
195
  > admin only (403). `--run-at` must carry a zone (`Z` or `+02:00`); ids are full UUIDs. Cancel never disarms
196
196
  > a run that already fired — it returns the row unchanged, and the CLI says so.
197
197
 
198
+ ### Calendar
199
+
200
+ Write a wake-up for the current agent, inspect what is open, or report that a wake finished:
201
+
202
+ ```bash
203
+ opvs calendar wait --pr opvs-ai/OPVS#3470 --merged --title "pull request 3470 is merged"
204
+ opvs calendar wait --card HEA-39 --status done --title "calendar CLI card is done"
205
+ opvs calendar wait --mail --title "direct mail arrives"
206
+ opvs calendar wait --mail-reply <message-id> --title "the owner replies"
207
+ opvs calendar wait --box <environment-id> --version 0.2.140 --title "the box is upgraded"
208
+ opvs calendar at 2026-10-20T18:00+02:00 --title "send the report"
209
+ opvs calendar every 2h --title "check the queue" --ends 3x
210
+ opvs calendar preview --every 1d --title "daily review"
211
+ opvs calendar list [--owner me] [--status open]
212
+ opvs calendar cancel <entry-id> --yes
213
+ opvs calendar done --token <run-id>.<secret> [--no-work]
214
+ ```
215
+
216
+ Times must include `Z` or an offset such as `+02:00`. Add `--deadline <zoned-time>`, repeatable
217
+ `--remind who:how:anchor:minutes`, and `--note <words>` when registering an entry. For example,
218
+ `--remind owner:notify:deadline:+1440` tells the owner one day after the deadline. Repeating entries
219
+ are previewed before they are saved. Every calendar command accepts `--json`; `cancel` reads the
220
+ entry's current version before writing and needs `--yes` when no terminal is attached. A direct-mail
221
+ wait uses `OPVS_AGENT_ADDRESS`, then the address configured for `opvs mail`.
222
+
198
223
  ### Members
199
224
 
200
225
  ```bash
@@ -245,6 +270,30 @@ opvs tasks search -q OSB-78 # By ref, PR number (#1256), title, d
245
270
  opvs tasks search -q deploy --board OSB,ASP --status pending --column Review --offset 20
246
271
  ```
247
272
 
273
+ #### Task schedules
274
+
275
+ Tasks can carry a start time, repeat rule, and reminders alongside their due date:
276
+
277
+ ```bash
278
+ opvs tasks create --board OPS -t "Send the weekly report" \
279
+ --start 2026-10-20T09:00+02:00 --timezone Europe/Berlin \
280
+ --repeat 1w --ends 8x \
281
+ --remind assignee:wake:fire:-15 \
282
+ --remind owner:notify:deadline:+1440 \
283
+ --due 2026-10-20T17:00+02:00
284
+
285
+ opvs tasks update OPS-142 --no-schedule # Clear the whole schedule
286
+ opvs tasks get OPS-142 # Schedule shown in words
287
+ opvs tasks get OPS-142 --json # Raw API schedule object
288
+ ```
289
+
290
+ `--start` and a timed `--due` must be ISO 8601 times with an explicit offset or
291
+ `Z`. A bare due date such as `--due 2026-10-20` remains valid. `--timezone`
292
+ takes an IANA name and is used for repeats across clock changes; when it is
293
+ omitted, the CLI uses and prints the machine's timezone. Repeat intervals are
294
+ `<N>d`, `<N>w`, or `<N>m`; repeat ends are `never`, a date, or `<N>x`.
295
+ Each repeatable reminder is `who:how:anchor:minutes`.
296
+
248
297
  ### Comments
249
298
 
250
299
  ```bash
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "stamp_version": 1,
3
3
  "package": "@opvs-ai/cli",
4
- "version": "0.27.0",
5
- "source_digest": "9f4d7df59f3c6548b5e8b2cb2a17c1b476df0e9766f556ec2b91b1fa0cab5dac",
6
- "source_files": 56,
7
- "built_at": "2026-10-14T06:29:59.711Z",
4
+ "version": "0.27.2",
5
+ "source_digest": "b93b5a62130f3048bbfc2a17c8ecbaa8fa5ef352a1ab6859cb2fff42957fd7a0",
6
+ "source_files": 59,
7
+ "built_at": "2026-10-15T09:58:32.629Z",
8
8
  "node": "v22.23.3"
9
9
  }
@@ -0,0 +1,3 @@
1
+ /** opvs calendar — write and inspect the current agent's wake-up calendar. */
2
+ import { Command } from "commander";
3
+ export declare function registerCalendarCommands(program: Command): void;
@@ -0,0 +1,294 @@
1
+ /** opvs calendar — write and inspect the current agent's wake-up calendar. */
2
+ import { createClient, getEffectiveConfig, resolveAgentIdentity, } from "@opvs-ai/core";
3
+ import { confirmOrExit, YES_HELP } from "../lib/confirm.js";
4
+ import { CalendarSpecError, conditionFromFlags, describeReminder, describeRepeat, parseEnds, parseEvery, parseRemind, parseZonedTime, } from "../lib/calendar-spec.js";
5
+ import { printJson } from "../lib/format.js";
6
+ const BASE = "/api/v1/calendar";
7
+ function collect(value, previous) {
8
+ return [...previous, value];
9
+ }
10
+ function addEntryOptions(command) {
11
+ return command
12
+ .requiredOption("--title <words>", "What should wake the agent, in words")
13
+ .option("--deadline <time>", "Deadline as ISO 8601 with a zone")
14
+ .option("--ends <end>", "never, YYYY-MM-DD, or a count such as 3x")
15
+ .option("--remind <who:how:anchor:minutes>", "Reminder to add; repeat this flag for more than one", collect, [])
16
+ .option("--note <words>", "A note delivered with the wake")
17
+ .option("--json", "Print one JSON document");
18
+ }
19
+ function commonEntry(opts) {
20
+ const body = {
21
+ title: opts.title,
22
+ reminders: opts.remind.map(parseRemind),
23
+ };
24
+ if (opts.deadline)
25
+ body.deadline_at = parseZonedTime(opts.deadline);
26
+ if (opts.ends)
27
+ Object.assign(body, parseEnds(opts.ends));
28
+ if (opts.note)
29
+ body.registered_note = opts.note;
30
+ return body;
31
+ }
32
+ function errorPayload(error) {
33
+ if (!error || typeof error !== "object")
34
+ return undefined;
35
+ const candidate = error;
36
+ for (const value of [candidate.detail, candidate.data, candidate.body]) {
37
+ if (value && typeof value === "object" && !Array.isArray(value)) {
38
+ return value;
39
+ }
40
+ }
41
+ return candidate;
42
+ }
43
+ function fail(error, fallback) {
44
+ if (error instanceof CalendarSpecError) {
45
+ console.error(error.message);
46
+ process.exit(1);
47
+ }
48
+ const payload = errorPayload(error);
49
+ if (payload?.error === "calendar_bounds") {
50
+ console.error(`${String(payload.field)} has a limit of ${String(payload.limit)}; got ${String(payload.got)}.`);
51
+ process.exit(1);
52
+ }
53
+ if (payload?.error === "stale_version") {
54
+ console.error("someone changed it; run the command again");
55
+ process.exit(1);
56
+ }
57
+ const candidate = error;
58
+ const detail = typeof candidate?.detail === "string" ? candidate.detail : candidate?.message;
59
+ console.error(`Error: ${detail || fallback}`);
60
+ process.exit(1);
61
+ }
62
+ async function responseJson(response) {
63
+ return response.json();
64
+ }
65
+ function printTimes(times) {
66
+ if (!Array.isArray(times))
67
+ return;
68
+ console.log("Next 5 times:");
69
+ for (const time of times)
70
+ console.log(`- ${String(time)}`);
71
+ }
72
+ function printCreated(entry, title, reminders, repeat) {
73
+ const id = entry && typeof entry === "object" && "id" in entry
74
+ ? String(entry.id)
75
+ : "(unknown id)";
76
+ console.log(`Registered ${id}: ${title}`);
77
+ if (repeat)
78
+ console.log(describeRepeat(repeat));
79
+ for (const reminder of reminders)
80
+ console.log(describeReminder(reminder));
81
+ }
82
+ function directMailAddress(workspace) {
83
+ const fromEnvironment = process.env.OPVS_AGENT_ADDRESS?.trim();
84
+ const configured = resolveAgentIdentity(workspace)?.address?.trim();
85
+ const address = fromEnvironment || configured;
86
+ if (!address) {
87
+ throw new CalendarSpecError("mail conditions need an OPVS mail address (opvs mail whoami)");
88
+ }
89
+ return address;
90
+ }
91
+ async function createEntry(program, body) {
92
+ const client = createClient(program.opts().workspace);
93
+ return responseJson(await client.post(`${BASE}/commitments`, body));
94
+ }
95
+ export function registerCalendarCommands(program) {
96
+ const calendar = program
97
+ .command("calendar")
98
+ .description("Write and inspect this agent's wake-up calendar");
99
+ const wait = addEntryOptions(calendar
100
+ .command("wait")
101
+ .description("Wake when one registered condition becomes true")
102
+ .option("--pr <owner/repo#N>", "Pull request to watch")
103
+ .option("--merged", "Wake when --pr is merged")
104
+ .option("--closed", "Wake when --pr is closed")
105
+ .option("--green", "Wake when every required check on --pr is green")
106
+ .option("--card <REF>", "Card to watch")
107
+ .option("--status <word>", "Wake when --card reaches this status")
108
+ .option("--mail-reply <id>", "Wake when this message receives a reply")
109
+ .option("--mail", "Wake when direct mail arrives for this agent")
110
+ .option("--box <environment>", "Environment to watch")
111
+ .option("--version <version>", "Wake when --box reaches this version"));
112
+ wait.action(async (opts) => {
113
+ try {
114
+ const cfg = getEffectiveConfig(program.opts().workspace);
115
+ const condition = conditionFromFlags(opts);
116
+ if (condition.source === "mail" && condition.key === "direct") {
117
+ condition.value = {
118
+ exists: true,
119
+ owner_address: directMailAddress(cfg.workspace_slug),
120
+ };
121
+ }
122
+ const body = {
123
+ ...commonEntry(opts),
124
+ schedule_kind: "trigger",
125
+ condition,
126
+ };
127
+ const entry = await createEntry(program, body);
128
+ if (opts.json)
129
+ printJson(entry);
130
+ else
131
+ printCreated(entry, opts.title, opts.remind.map(parseRemind));
132
+ }
133
+ catch (error) {
134
+ fail(error, "Failed to register calendar wait");
135
+ }
136
+ });
137
+ const at = addEntryOptions(calendar
138
+ .command("at <time>")
139
+ .description("Wake once at an ISO 8601 time with a zone"));
140
+ at.action(async (time, opts) => {
141
+ try {
142
+ const body = {
143
+ ...commonEntry(opts),
144
+ schedule_kind: "once",
145
+ fire_at: parseZonedTime(time),
146
+ };
147
+ const entry = await createEntry(program, body);
148
+ if (opts.json)
149
+ printJson(entry);
150
+ else
151
+ printCreated(entry, opts.title, opts.remind.map(parseRemind));
152
+ }
153
+ catch (error) {
154
+ fail(error, "Failed to register calendar time");
155
+ }
156
+ });
157
+ const every = addEntryOptions(calendar
158
+ .command("every <repeat>")
159
+ .description("Wake every N minutes, hours or days, starting now"));
160
+ every.action(async (repeat, opts) => {
161
+ try {
162
+ const repeatFields = {
163
+ ...parseEvery(repeat),
164
+ ...(opts.ends ? parseEnds(opts.ends) : { repeat_ends: "never" }),
165
+ fire_at: new Date().toISOString(),
166
+ };
167
+ const body = {
168
+ ...commonEntry({ ...opts, ends: undefined }),
169
+ schedule_kind: "interval",
170
+ ...repeatFields,
171
+ };
172
+ const client = createClient(program.opts().workspace);
173
+ const times = await responseJson(await client.post(`${BASE}/commitments/preview`, body));
174
+ if (!opts.json)
175
+ printTimes(times);
176
+ const entry = await responseJson(await client.post(`${BASE}/commitments`, body));
177
+ if (opts.json)
178
+ printJson({ next_five: times, entry });
179
+ else {
180
+ printCreated(entry, opts.title, opts.remind.map(parseRemind), repeatFields);
181
+ }
182
+ }
183
+ catch (error) {
184
+ fail(error, "Failed to register repeating calendar entry");
185
+ }
186
+ });
187
+ calendar
188
+ .command("list")
189
+ .description("List calendar entries")
190
+ .option("--owner <owner>", "Owner to list; defaults to me")
191
+ .option("--status <status>", "Entry status; defaults to open")
192
+ .option("--json", "Print one JSON document")
193
+ .action(async (opts) => {
194
+ try {
195
+ const params = new URLSearchParams();
196
+ if (opts.owner)
197
+ params.set("owner", opts.owner);
198
+ if (opts.status)
199
+ params.set("status", opts.status);
200
+ const suffix = params.size ? `?${params.toString()}` : "";
201
+ const client = createClient(program.opts().workspace);
202
+ const entries = await responseJson(await client.get(`${BASE}/commitments${suffix}`));
203
+ printJson(entries);
204
+ }
205
+ catch (error) {
206
+ fail(error, "Failed to list calendar entries");
207
+ }
208
+ });
209
+ const preview = addEntryOptions(calendar
210
+ .command("preview")
211
+ .description("Show the next five times for an unsaved entry")
212
+ .option("--at <time>", "Preview a one-time entry")
213
+ .option("--every <repeat>", "Preview an interval entry"));
214
+ preview.action(async (opts) => {
215
+ try {
216
+ if (Boolean(opts.at) === Boolean(opts.every)) {
217
+ throw new CalendarSpecError("a preview needs exactly one time, for example --at 2026-10-20T18:00+02:00 or --every 2h.");
218
+ }
219
+ const body = {
220
+ ...commonEntry(opts),
221
+ schedule_kind: opts.at ? "once" : "interval",
222
+ };
223
+ if (opts.at)
224
+ body.fire_at = parseZonedTime(opts.at);
225
+ if (opts.every) {
226
+ Object.assign(body, parseEvery(opts.every));
227
+ body.fire_at = new Date().toISOString();
228
+ }
229
+ const client = createClient(program.opts().workspace);
230
+ const times = await responseJson(await client.post(`${BASE}/commitments/preview`, body));
231
+ if (opts.json)
232
+ printJson(times);
233
+ else
234
+ printTimes(times);
235
+ }
236
+ catch (error) {
237
+ fail(error, "Failed to preview calendar entry");
238
+ }
239
+ });
240
+ calendar
241
+ .command("cancel <id>")
242
+ .description("Cancel an open entry. Asks first; needs --yes off a terminal")
243
+ .option("-y, --yes", YES_HELP)
244
+ .option("--json", "Print one JSON document")
245
+ .action(async (id, opts) => {
246
+ await confirmOrExit(`Cancel calendar entry ${id}?`, opts.yes);
247
+ try {
248
+ const client = createClient(program.opts().workspace);
249
+ const current = (await responseJson(await client.get(`${BASE}/commitments/${id}`)));
250
+ if (!Number.isInteger(current.version)) {
251
+ throw new Error("calendar entry did not include its version");
252
+ }
253
+ const entry = await responseJson(await client.post(`${BASE}/commitments/${id}/cancel`, {
254
+ version: current.version,
255
+ }));
256
+ if (opts.json)
257
+ printJson(entry);
258
+ else
259
+ console.log(`Cancelled ${id}.`);
260
+ }
261
+ catch (error) {
262
+ fail(error, "Failed to cancel calendar entry");
263
+ }
264
+ });
265
+ calendar
266
+ .command("done")
267
+ .description("Report the outcome of a finished wake")
268
+ .requiredOption("--token <run.secret>", "One-use dispatch token")
269
+ .option("--no-work", "Report that the wake found no work")
270
+ .option("--json", "Print one JSON document")
271
+ .action(async (opts) => {
272
+ try {
273
+ const dot = opts.token.indexOf(".");
274
+ if (dot <= 0 || dot === opts.token.length - 1) {
275
+ throw new CalendarSpecError("a dispatch token needs its run id and secret, for example R.S.");
276
+ }
277
+ const runId = opts.token.slice(0, dot);
278
+ const client = createClient(program.opts().workspace);
279
+ const result = await responseJson(await client.post(`${BASE}/runs/${runId}/complete`, {
280
+ dispatch_token: opts.token,
281
+ outcome: opts.work === false ? "no_work" : "work_done",
282
+ }));
283
+ if (opts.json)
284
+ printJson(result);
285
+ else {
286
+ console.log(`Completed wake ${runId}: ${opts.work === false ? "no work" : "work done"}.`);
287
+ }
288
+ }
289
+ catch (error) {
290
+ fail(error, "Failed to complete calendar wake");
291
+ }
292
+ });
293
+ }
294
+ //# sourceMappingURL=calendar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.js","sourceRoot":"","sources":["../../src/commands/calendar.ts"],"names":[],"mappings":"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"}
@@ -29,7 +29,9 @@
29
29
  * `format=undefined` is a literal string on the URL. Each `if (opts.x) ...`
30
30
  * gate exists so the request the server sees is the request the user asked for.
31
31
  */
32
+ import { readFile } from "node:fs/promises";
32
33
  import { codeCallers, createClient, getEffectiveConfig, } from "@opvs-ai/core";
34
+ import { extractCardAnchors, } from "../lib/card-anchors.js";
33
35
  import { buildUrl, printResponse } from "../lib/format.js";
34
36
  export function registerCodeCommands(program) {
35
37
  const code = program
@@ -131,6 +133,80 @@ export function registerCodeCommands(program) {
131
133
  process.exit(1);
132
134
  }
133
135
  });
136
+ // ── check-card: bounded index facts for a planner's draft brief ──
137
+ code
138
+ .command("check-card <brief.md>")
139
+ .description("Check a draft card's paths and names against the code index")
140
+ .option("--project <id>", "Project id (or OPVS_CODE_PROJECT)")
141
+ .option("--similar <words>", "Append existing code similar to these words (repeatable, max 5)", (value, previous) => [...previous, value], [])
142
+ .option("--strict", "Exit 1 when an anchor is absent and not declared new")
143
+ .option("--json", "Print the same facts as JSON")
144
+ .action(async (briefPath, opts) => {
145
+ const project = opts.project || process.env.OPVS_CODE_PROJECT;
146
+ if (!project) {
147
+ reportCannotCheckCard("project required: pass --project <id> or set OPVS_CODE_PROJECT");
148
+ return;
149
+ }
150
+ const similarQueries = Array.isArray(opts.similar) ? opts.similar : [];
151
+ if (similarQueries.length > 5) {
152
+ reportCannotCheckCard("--similar may be repeated at most 5 times");
153
+ return;
154
+ }
155
+ let markdown;
156
+ try {
157
+ markdown = await readFile(briefPath, "utf8");
158
+ }
159
+ catch {
160
+ reportCannotCheckCard(`cannot read brief: ${briefPath}`);
161
+ return;
162
+ }
163
+ const extracted = extractCardAnchors(markdown);
164
+ const anchorNames = extracted.anchors.map((item) => item.anchor);
165
+ const wsSlug = program.opts().workspace;
166
+ try {
167
+ const client = createClient(wsSlug);
168
+ const resolvedResponse = await client.post(`/api/v1/code/${project}/nodes/resolve`, { anchors: anchorNames });
169
+ const resolutions = await resolvedResponse.json();
170
+ const resolutionByAnchor = new Map(resolutions.map((item) => [item.anchor, item]));
171
+ const live = resolutions.filter((item) => item.state === "live");
172
+ const nodeIds = live.map((item) => item.node_id);
173
+ const callersOptions = { callersLimit: 20, testsLimit: 10 };
174
+ const callersResponse = wsSlug
175
+ ? await codeCallers(project, nodeIds, callersOptions, wsSlug)
176
+ : await codeCallers(project, nodeIds, callersOptions);
177
+ if (callersResponse.degraded || !callersResponse.index) {
178
+ reportCannotCheckCard("code index unavailable");
179
+ return;
180
+ }
181
+ const callerByNode = new Map(callersResponse.results.map((item) => [item.node_id, item]));
182
+ const facts = extracted.anchors.map((item) => cardAnchorFact(item, resolutionByAnchor.get(item.anchor), callersResponse, callerByNode));
183
+ const similar = await loadSimilarFacts(client, project, similarQueries);
184
+ const checkedAt = new Date().toISOString();
185
+ const output = {
186
+ version: "index-facts-v1",
187
+ project_id: project,
188
+ checked_at: checkedAt,
189
+ index: callersResponse.index,
190
+ paths_checked: facts.length,
191
+ paths_left_out: extracted.leftOut,
192
+ anchors: facts,
193
+ similar,
194
+ };
195
+ if (opts.json) {
196
+ console.log(JSON.stringify(output, null, 2));
197
+ }
198
+ else {
199
+ console.log(boundIndexFactsMarkdown(indexFactsMarkdown(project, callersResponse, checkedAt, facts, extracted.leftOut, similar)));
200
+ }
201
+ if (opts.strict && facts.some((fact) => fact.in_index === "NO — not in the index and not declared new")) {
202
+ process.exitCode = 1;
203
+ }
204
+ }
205
+ catch (err) {
206
+ const e = err;
207
+ reportCannotCheckCard(e.detail || e.message || "code index unavailable");
208
+ }
209
+ });
134
210
  // ── summary: the orientation document as data ───────────────────
135
211
  code
136
212
  .command("summary")
@@ -368,15 +444,19 @@ function printCallersText(anchors, resolutions, response) {
368
444
  console.log(response.index
369
445
  ? `index: ${response.index.commit.slice(0, 7)}, ${ageInWords(response.index.age_seconds)}`
370
446
  : "index: unavailable");
371
- let resultIndex = 0;
447
+ const callerByNode = new Map(response.results.map((item) => [item.node_id, item]));
372
448
  for (let anchorIndex = 0; anchorIndex < anchors.length; anchorIndex += 1) {
373
449
  const resolution = resolutions[anchorIndex];
374
450
  if (!resolution || resolution.state !== "live") {
375
451
  printUnresolvedAnchor(resolution?.anchor ?? anchors[anchorIndex], resolution);
376
452
  continue;
377
453
  }
378
- const result = response.results[resultIndex];
379
- resultIndex += 1;
454
+ const result = callerByNode.get(resolution.node_id);
455
+ if (!result || result.state !== "found") {
456
+ console.log(`\n${resolution.anchor}\n` +
457
+ "UNKNOWN — the node was not found when callers were asked (index changed?)");
458
+ continue;
459
+ }
380
460
  console.log(`\n${nodeLabel(result, resolution.anchor)}`);
381
461
  if (!response.calls_indexed) {
382
462
  console.log("calls are not indexed for this repo: callers are UNKNOWN, not zero");
@@ -415,4 +495,150 @@ function printUnresolvedAnchor(anchor, resolution) {
415
495
  }
416
496
  console.log(`\n${anchor}\n${status}`);
417
497
  }
498
+ function reportCannotCheckCard(reason) {
499
+ console.error(`CANNOT: ${reason}`);
500
+ process.exitCode = 2;
501
+ }
502
+ function cardAnchorFact(cardAnchor, resolution, response, callerByNode) {
503
+ const state = resolution?.state ?? "unknown";
504
+ if (state === "moved") {
505
+ return {
506
+ anchor: cardAnchor.anchor,
507
+ declared_new: cardAnchor.declaredNew,
508
+ state,
509
+ moved_to_anchor: resolution?.moved_to_anchor ?? null,
510
+ in_index: resolution?.moved_to_anchor
511
+ ? `MOVED → ${resolution.moved_to_anchor}`
512
+ : "MOVED",
513
+ callers: "n/a",
514
+ test_hints: "n/a",
515
+ callers_truncated: false,
516
+ test_hints_truncated: false,
517
+ };
518
+ }
519
+ if (state !== "live" || !resolution) {
520
+ return {
521
+ anchor: cardAnchor.anchor,
522
+ declared_new: cardAnchor.declaredNew,
523
+ state,
524
+ moved_to_anchor: null,
525
+ in_index: cardAnchor.declaredNew
526
+ ? "no (declared new)"
527
+ : "NO — not in the index and not declared new",
528
+ callers: "n/a",
529
+ test_hints: "n/a",
530
+ callers_truncated: false,
531
+ test_hints_truncated: false,
532
+ };
533
+ }
534
+ const result = callerByNode.get(resolution.node_id);
535
+ if (!result || result.state !== "found") {
536
+ return {
537
+ anchor: cardAnchor.anchor,
538
+ declared_new: cardAnchor.declaredNew,
539
+ state: result?.state ?? "unknown",
540
+ moved_to_anchor: null,
541
+ in_index: "UNKNOWN — gone between resolve and callers",
542
+ callers: "n/a",
543
+ test_hints: "n/a",
544
+ callers_truncated: result?.callers_truncated ?? false,
545
+ test_hints_truncated: result?.nearby_tests_truncated ?? false,
546
+ };
547
+ }
548
+ let callers;
549
+ if (!response.calls_indexed) {
550
+ callers = "unknown (calls not indexed)";
551
+ }
552
+ else if (!result.callers_known) {
553
+ callers = "unknown (calls not indexed for this file type)";
554
+ }
555
+ else if (result.callers_truncated && result.callers_total === 0) {
556
+ callers = "unknown — answer cut (file has more than 500 symbols)";
557
+ }
558
+ else {
559
+ const shown = result.callers.slice(0, 5);
560
+ const exact = result.callers.filter((caller) => caller.resolution === "exact").length;
561
+ const heuristic = result.callers.filter((caller) => caller.resolution === "heuristic").length;
562
+ const labels = shown.map((caller) => caller.symbol ? `${caller.path}#${caller.symbol}` : caller.path);
563
+ const more = result.callers_total > shown.length ? ", …" : "";
564
+ const count = result.callers_truncated
565
+ ? `≥ ${result.callers_total} (page of ${result.callers.length}: ${exact} exact, ${heuristic} heuristic) — truncated`
566
+ : `${result.callers_total} (${exact} exact, ${heuristic} heuristic)`;
567
+ callers = count +
568
+ (labels.length > 0 ? `: ${labels.map(inlineCode).join(", ")}${more}` : "");
569
+ }
570
+ const testHints = (result.nearby_tests.length
571
+ ? result.nearby_tests.map((test) => inlineCode(test.path)).join(", ")
572
+ : "none") + (result.nearby_tests_truncated ? " — truncated" : "");
573
+ return {
574
+ anchor: cardAnchor.anchor,
575
+ declared_new: cardAnchor.declaredNew,
576
+ state,
577
+ moved_to_anchor: null,
578
+ in_index: "yes",
579
+ callers,
580
+ test_hints: testHints,
581
+ callers_truncated: result.callers_truncated,
582
+ test_hints_truncated: result.nearby_tests_truncated,
583
+ };
584
+ }
585
+ async function loadSimilarFacts(client, project, queries) {
586
+ const facts = [];
587
+ for (const query of queries) {
588
+ const response = await client.get(buildUrl(`/api/v1/code/${project}/context`, { query, limit: "5" }, "json"));
589
+ const body = await response.json();
590
+ for (const match of (body.matches ?? []).slice(0, 5)) {
591
+ const path = match.file_path;
592
+ const symbol = match.name;
593
+ const detail = firstLine(match.doc_first_line) || firstLine(match.signature) || "matching code";
594
+ facts.push({ query, anchor: symbol ? `${path}#${symbol}` : path, detail });
595
+ }
596
+ }
597
+ return facts;
598
+ }
599
+ function firstLine(value) {
600
+ return value?.split(/\r?\n/, 1)[0].trim() ?? "";
601
+ }
602
+ function indexFactsMarkdown(project, response, checkedAt, facts, leftOut, similar) {
603
+ const index = response.index;
604
+ const lines = [
605
+ "## Index facts",
606
+ `<!-- index-facts v1 commit=${index.commit} indexed_at=${index.indexed_at} checked_at=${checkedAt} -->`,
607
+ `index: ${index.commit.slice(0, 7)}, ${ageInWords(index.age_seconds)} · project ${project} · ${facts.length} paths checked`,
608
+ ];
609
+ if (leftOut > 0) {
610
+ lines.push(`_${leftOut} ${leftOut === 1 ? "path" : "paths"} left out after the 50-anchor limit._`);
611
+ }
612
+ lines.push("| Anchor | In the index | Callers | Test hints (not coverage) |", "| --- | --- | --- | --- |");
613
+ for (const fact of facts) {
614
+ lines.push(`| ${inlineCode(fact.anchor)} | ${markdownCell(fact.in_index)} | ${markdownCell(fact.callers)} | ${markdownCell(fact.test_hints)} |`);
615
+ }
616
+ for (const fact of similar) {
617
+ lines.push(`Already exists for "${markdownText(fact.query)}": ${inlineCode(fact.anchor)} — ${markdownText(fact.detail)}`);
618
+ }
619
+ return lines.join("\n");
620
+ }
621
+ const INDEX_FACTS_MAX_BYTES = 6_000;
622
+ const INDEX_FACTS_TRUNCATED = "_Truncated: output exceeded 6,000 bytes._";
623
+ function boundIndexFactsMarkdown(markdown) {
624
+ if (Buffer.byteLength(`${markdown}\n`, "utf8") <= INDEX_FACTS_MAX_BYTES)
625
+ return markdown;
626
+ const kept = [];
627
+ for (const line of markdown.split("\n")) {
628
+ const candidate = [...kept, line, INDEX_FACTS_TRUNCATED].join("\n");
629
+ if (Buffer.byteLength(`${candidate}\n`, "utf8") > INDEX_FACTS_MAX_BYTES)
630
+ break;
631
+ kept.push(line);
632
+ }
633
+ return [...kept, INDEX_FACTS_TRUNCATED].join("\n");
634
+ }
635
+ function inlineCode(value) {
636
+ return `\`${markdownText(value).replaceAll("`", "\\`")}\``;
637
+ }
638
+ function markdownCell(value) {
639
+ return value.replaceAll("|", "\\|").replaceAll("\n", " ");
640
+ }
641
+ function markdownText(value) {
642
+ return value.replaceAll("|", "\\|").replaceAll("\r", " ").replaceAll("\n", " ");
643
+ }
418
644
  //# sourceMappingURL=code.js.map