@wirecat/cli-core 0.581.0 → 0.583.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 (37) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/cli/compact.js +46 -9
  3. package/dist/cli/index.d.ts +1 -0
  4. package/dist/cli/index.js +1 -0
  5. package/dist/cli/project-status-rows.js +1 -1
  6. package/dist/cli/project-status.d.ts +4 -12
  7. package/dist/cli/project-status.js +35 -16
  8. package/dist/entities/index.d.ts +2 -0
  9. package/dist/entities/mail-findings.d.ts +3 -0
  10. package/dist/entities/mail-findings.js +3 -0
  11. package/dist/errors/reasons.js +1 -0
  12. package/dist/mail/cli/marks.js +9 -3
  13. package/dist/mail/cli/review.js +2 -2
  14. package/dist/mail/findings.d.ts +56 -2
  15. package/dist/mail/findings.js +98 -81
  16. package/dist/mail/index.d.ts +1 -1
  17. package/dist/mail/index.js +1 -1
  18. package/dist/mail/marks.js +5 -6
  19. package/dist/meetings/findings-rules.d.ts +4 -1
  20. package/dist/meetings/findings-rules.js +36 -19
  21. package/dist/meetings/index.d.ts +1 -1
  22. package/dist/meetings/index.js +1 -1
  23. package/dist/meetings/timestamp.js +1 -1
  24. package/dist/services/index.d.ts +1 -1
  25. package/dist/services/index.js +1 -1
  26. package/dist/services/proposals/index.d.ts +1 -1
  27. package/dist/services/proposals/index.js +1 -1
  28. package/dist/services/proposals/kinds/organization.d.ts +7 -0
  29. package/dist/services/proposals/kinds/organization.js +22 -5
  30. package/dist/store/knowledge-query.js +7 -1
  31. package/dist/store/sqlite/finance.d.ts +2 -0
  32. package/dist/store/sqlite/finance.js +2 -2
  33. package/dist/store/sqlite/knowledge.d.ts +2 -2
  34. package/dist/store/sqlite/knowledge.js +2 -21
  35. package/dist/store/sqlite/organizations.d.ts +14 -0
  36. package/dist/store/sqlite/organizations.js +48 -0
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,60 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.583.0 — 05.11.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **`--compact` says a day once above its rows in a list in time order.** When a list's rows are sorted by time and
15
+ a day holds three rows on average, a `# YYYY-MM-DD` line opens each day and its rows print only `HH:MM`. A
16
+ 1,000-message `messages list` drops from 43 % to 41 % of JSON's tokens (53 % to 52 % of its bytes). A reader that
17
+ parsed `MM-DD HH:MM` from such a row reads the day from the line above it. Searches and other unsorted lists are
18
+ unchanged.
19
+ - **An item answer's compact sections share one span line.** The days and zone are printed once, as
20
+ `# <first>…<last> · <zone>` above the first section, not on every section's header (`projects status`,
21
+ `projects evidence`).
22
+ - **`projects status` leaves out what the answer already says.** `item.window` is gone (it is `meta.window`). A
23
+ listed item drops `chat` when its `ref` is a message of that chat, drops `basis` when it is `store`, and a promise
24
+ drops `data.source`, which equals its `basis`. `who.name` and a chat row's `role` are left out instead of `null`.
25
+ The promise row of `--compact` prints `basis`. On the E7 fixture JSON goes from 3,998 to 3,676 bytes and compact
26
+ from 1,511 to 1,387.
27
+ - **Every mail finding kind needs `reason`.** A fact, a memory and a new contact had none or an optional one; the
28
+ owner reads it beside each finding when he decides. A missing field is refused with its name, the finding and what
29
+ that kind needs (`finding.missing-field`, `details.fields`, `details.needs`). Batches staged before keep theirs.
30
+
31
+ ### Added
32
+
33
+ - **`invoices` takes `number`.** `finance.invoices({ number })` lists the invoices whose printed number is exactly
34
+ that, as `orders.list({ number })` does for orders.
35
+ - **`MAIL_FINDING_FIELDS` and `mailFindingFieldsHelp()`.** The fields each mail finding kind needs and takes, the
36
+ table the validator itself reads, so a host's `--help` lists them without drifting from the checks.
37
+
38
+ ### Fixed
39
+
40
+ - **A `messages reset` that leaves the mark where it is still counts.** It adds to `resets`, so the holder's next
41
+ findings key is new, and says "mark unchanged … counted as a reset"; the answer gives `resets`. Before, it wrote
42
+ nothing, and a run after it was refused with `mail-findings.key-used`.
43
+ - **`review --holder` gives no `mail-findings.add` step when it gives no emails.**
44
+ - **A task finding's `due_at` refusals say what is needed.** A date without a time and timezone, `due_at` without
45
+ `due_evidence`, and `due_evidence` given as evidence objects each name the field and show the right form, such as
46
+ `"due_at":"2026-11-09T09:00:00Z","due_evidence":["<reference>"]`.
47
+
48
+ ## 0.582.0 — 05.11.2026
49
+
50
+ ### Added
51
+
52
+ - **`knowledge.updateOrganization(organization, { name })` renames an organization**, given as `organization:<id>` or
53
+ by its id; its kind and scope stay. An agent's rename is a proposal for the owner: the `organization_rename`
54
+ proposal kind and `proposeOrganizationRename(store, { organization, name }, by, reason?)`. It shows in `review` as
55
+ an `organization` row (`rename organization:3 to ...`); confirming it renames, rejecting it changes nothing. No
56
+ schema change.
57
+
58
+ ### Fixed
59
+
60
+ - **`knowledge.organizations` answers a `name:` term.** `ORGANIZATIONS_QUERY` listed `name`, but the store had no
61
+ column for it, so `organizations list name:acme` failed. The match ignores case in any script, and `name:*acme*`
62
+ finds a name that holds a word.
63
+
10
64
  ## 0.581.0 — 05.11.2026
11
65
 
12
66
  ### Added
@@ -200,7 +200,9 @@ const cellOf = (item, column, clock) => {
200
200
  if (!parts)
201
201
  return { text: clean(iso) };
202
202
  const day = parts.date.slice(5);
203
- return { text: column.as === "day" ? day : `${day} ${parts.time}`, date: parts.date };
203
+ return column.as === "day"
204
+ ? { text: day, date: parts.date }
205
+ : { text: `${day} ${parts.time}`, date: parts.date, time: parts.time };
204
206
  }
205
207
  case "count":
206
208
  return typeof value === "number" && value !== 0
@@ -255,6 +257,22 @@ const sameEverywhere = (rows, index) => {
255
257
  const first = rows[0]?.cells[index]?.text;
256
258
  return first !== undefined && rows.every(({ cells }) => cells[index]?.text === first);
257
259
  };
260
+ const ordered = (dates) => dates.every((one, at) => at === 0 || one >= dates[at - 1]) ||
261
+ dates.every((one, at) => at === 0 || one <= dates[at - 1]);
262
+ /**
263
+ * The column whose date is said once on a `# YYYY-MM-DD` line above its rows: a list in time order, such as a chat's
264
+ * messages. Only when days hold three rows on average: with fewer, the day lines cost more than they save.
265
+ */
266
+ const dayColumn = (rows, shape, hoisted) => {
267
+ const index = shape.columns.findIndex((column, at) => column.as === "time" && !hoisted[at]);
268
+ if (index < 0)
269
+ return -1;
270
+ const dates = rows.map(({ cells }) => cells[index]?.date);
271
+ if (dates.some((one) => one === undefined))
272
+ return -1;
273
+ const days = dates;
274
+ return ordered(days) && new Set(days).size * 3 <= days.length ? index : -1;
275
+ };
258
276
  const rowsOfItems = (items, shape, clock, chars) => {
259
277
  const dates = [];
260
278
  const rows = items.map((item) => {
@@ -277,17 +295,30 @@ const rowsOfItems = (items, shape, clock, chars) => {
277
295
  return [];
278
296
  return [`${sourceOf(item, column) ?? fieldsOfColumn(column.field)[0]}: ${rows[0]?.cells[index]?.text}`];
279
297
  });
280
- const lines = rows.map(({ cells, text, item }) => {
298
+ const byDay = dayColumn(rows, shape, hoisted);
299
+ let shownDay;
300
+ const lines = rows.flatMap(({ cells, text, item }) => {
281
301
  const parts = cells.map((cell, index) => (hoisted[index] ? undefined : cell?.text));
302
+ const dayLine = [];
303
+ const cell = cells[byDay];
304
+ if (byDay >= 0 && cell?.date && cell.time) {
305
+ if (cell.date !== shownDay)
306
+ dayLine.push(`# ${cell.date}`);
307
+ shownDay = cell.date;
308
+ parts[byDay] = cell.time;
309
+ }
282
310
  const shortened = prefix ? clean(prefix) : undefined;
283
311
  if (shortened && parts[0]?.startsWith(shortened))
284
312
  parts[0] = `#${parts[0].slice(shortened.length)}`;
285
313
  const shown = [...parts, text].filter((part) => part !== undefined && part !== "");
286
- return guarded(shown.length ? shown.join(" · ") : item ? blockLine(item, chars) : "");
314
+ return [...dayLine, guarded(shown.length ? shown.join(" · ") : item ? blockLine(item, chars) : "")];
287
315
  });
316
+ const header = [...(prefix ? [`ref: ${clean(prefix)}<id>`] : []), ...named];
317
+ return { header, lines, dates };
318
+ };
319
+ const spanOf = (dates, clock) => {
288
320
  const when = span(dates);
289
- const header = [...(prefix ? [`ref: ${clean(prefix)}<id>`] : []), ...named, ...(when ? [when, clock.zone] : [])];
290
- return { header, lines };
321
+ return when ? [when, clock.zone] : [];
291
322
  };
292
323
  const blockLine = (fields, chars) => blockOf(fields, chars).join(" ");
293
324
  /** A record as `field: value` lines; a nested record is indented under its field, a list of records counted. */
@@ -352,20 +383,26 @@ export const compactText = (body, shape, options, rowsOf = rowsOfEntity()) => {
352
383
  const chars = options.textChars ?? TEXT_CHARS;
353
384
  let lines = [];
354
385
  if ("items" in body) {
355
- const { header, lines: rows } = rowsOfItems(body.items, shape, clock, chars);
356
- lines = (header.length ? [`# ${header.join(" · ")}`] : []).concat(rows.length ? rows : ["# nothing found"]);
386
+ const { header, lines: rows, dates } = rowsOfItems(body.items, shape, clock, chars);
387
+ const head = [...header, ...spanOf(dates, clock)];
388
+ lines = (head.length ? [`# ${head.join(" · ")}`] : []).concat(rows.length ? rows : ["# nothing found"]);
357
389
  }
358
390
  else if (isFields(body.item)) {
359
391
  const sections = shape.sections ?? {};
360
392
  const rest = Object.fromEntries(Object.entries(body.item).filter(([name]) => !Object.hasOwn(sections, name) && !shape.hidden?.includes(name)));
361
393
  lines = blockOf(rest, chars, "", shape.whole).map(guarded);
394
+ let dates = [];
395
+ let drawn = [];
362
396
  for (const [name, entity] of Object.entries(sections)) {
363
397
  const list = body.item[name];
364
398
  if (!Array.isArray(list))
365
399
  continue;
366
- const { header, lines: rows } = rowsOfItems(list, rowsOf(entity), clock, chars);
367
- lines = lines.concat([`# ${[`${clean(name)}: ${list.length}`, ...header].join(" · ")}`], rows);
400
+ const { header, lines: rows, dates: more } = rowsOfItems(list, rowsOf(entity), clock, chars);
401
+ dates = dates.concat(more);
402
+ drawn = drawn.concat([`# ${[`${clean(name)}: ${list.length}`, ...header].join(" · ")}`], rows);
368
403
  }
404
+ const when = spanOf(dates, clock);
405
+ lines = lines.concat(when.length ? [`# ${when.join(" · ")}`] : [], drawn);
369
406
  }
370
407
  else if (present(body.item))
371
408
  lines = [guarded(cut(clean(String(body.item)), chars))];
@@ -6,6 +6,7 @@ export { botTemplate } from "../entities/list-results.js";
6
6
  export { syncProgress } from "../entities/store-records.js";
7
7
  export { isCliFailure, isCommanderFailure } from "../errors/failures.js";
8
8
  export { type AppErrors, failureContext } from "../errors/reasons.js";
9
+ export { mailFindingFieldsHelp } from "../mail/findings.js";
9
10
  export { botInstructions } from "../mcp/bot/instructions.js";
10
11
  export type { BotServerOptions, RunBotCommand } from "../mcp/bot/server.js";
11
12
  export type { HttpOptions } from "../mcp/http/serve.js";
package/dist/cli/index.js CHANGED
@@ -5,6 +5,7 @@ export { botTemplate } from "../entities/list-results.js";
5
5
  export { syncProgress } from "../entities/store-records.js";
6
6
  export { isCliFailure, isCommanderFailure } from "../errors/failures.js";
7
7
  export { failureContext } from "../errors/reasons.js";
8
+ export { mailFindingFieldsHelp } from "../mail/findings.js";
8
9
  export { botInstructions } from "../mcp/bot/instructions.js";
9
10
  export { callMeetingPreparationTool, meetingPreparationTool } from "../mcp/meeting-preparation.js";
10
11
  export { callMeetingProjectCandidatesTool, meetingProjectCandidatesTool } from "../mcp/meeting-project-candidates.js";
@@ -15,7 +15,7 @@ export const STATUS_SIGNAL_ROWS = {
15
15
  columns: [
16
16
  ...where,
17
17
  { field: "at", as: "day" },
18
- { field: "data.source" },
18
+ { field: "basis" },
19
19
  { field: "data.side" },
20
20
  { field: "data.due", as: "day" },
21
21
  { field: "data.status" },
@@ -27,10 +27,6 @@ export declare const projectStatusSchema: v.LooseObjectSchema<{
27
27
  readonly project: v.StringSchema<undefined>;
28
28
  readonly key: v.NullableSchema<v.StringSchema<undefined>, undefined>;
29
29
  readonly name: v.StringSchema<undefined>;
30
- readonly window: v.StrictObjectSchema<{
31
- readonly since: v.StringSchema<undefined>;
32
- readonly until: v.StringSchema<undefined>;
33
- }, undefined>;
34
30
  /** Each signal's version, so a saved answer says which rules made it. */
35
31
  readonly signals: v.RecordSchema<v.StringSchema<undefined>, v.NumberSchema<undefined>, undefined>;
36
32
  readonly money: v.ArraySchema<v.StrictObjectSchema<{
@@ -43,7 +39,7 @@ export declare const projectStatusSchema: v.LooseObjectSchema<{
43
39
  readonly chat: v.StringSchema<undefined>;
44
40
  readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
45
41
  readonly kind: v.StringSchema<undefined>;
46
- readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
42
+ readonly role: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
47
43
  readonly purpose: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
48
44
  readonly project: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
49
45
  readonly coverage: v.StringSchema<undefined>;
@@ -54,7 +50,7 @@ export declare const projectStatusSchema: v.LooseObjectSchema<{
54
50
  readonly at: v.StringSchema<undefined>;
55
51
  readonly who: v.StrictObjectSchema<{
56
52
  readonly ref: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
57
- readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
53
+ readonly name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
58
54
  readonly owner: v.BooleanSchema<undefined>;
59
55
  }, undefined>;
60
56
  }, undefined>, undefined>;
@@ -100,10 +96,6 @@ export declare const projectStatusAnswer: (found: ProjectStatus, hints: ProjectS
100
96
  project: string;
101
97
  key: string | null;
102
98
  name: string;
103
- window: {
104
- since: string;
105
- until: string;
106
- };
107
99
  signals: {
108
100
  [x: string]: number;
109
101
  };
@@ -117,7 +109,7 @@ export declare const projectStatusAnswer: (found: ProjectStatus, hints: ProjectS
117
109
  chat: string;
118
110
  title: string | null;
119
111
  kind: string;
120
- role: string | null;
112
+ role?: string | undefined;
121
113
  purpose?: string | undefined;
122
114
  project?: string | undefined;
123
115
  coverage: string;
@@ -128,7 +120,7 @@ export declare const projectStatusAnswer: (found: ProjectStatus, hints: ProjectS
128
120
  at: string;
129
121
  who: {
130
122
  ref?: string | undefined;
131
- name: string | null;
123
+ name?: string | undefined;
132
124
  owner: boolean;
133
125
  };
134
126
  } | undefined;
@@ -13,21 +13,23 @@ const SECTIONS = {
13
13
  /** The `project_status` answer drawn by `--compact`: a row per chat, then each listed signal with its own row. */
14
14
  export const projectStatusRow = (signals) => ({
15
15
  columns: [],
16
- hidden: ["project", "window", "purposes", "signals"],
16
+ hidden: ["project", "purposes", "signals"],
17
17
  sections: {
18
18
  ...SECTIONS,
19
19
  ...Object.fromEntries(signals.flatMap(({ id }) => (ON_CHAT_ROW.has(id) ? [] : [[listKey(id), `status.${id}`]]))),
20
20
  },
21
21
  });
22
- const who = v.strictObject({ ref: v.optional(v.string()), name: v.nullable(v.string()), owner: v.boolean() });
22
+ const who = v.strictObject({ ref: v.optional(v.string()), name: v.optional(v.string()), owner: v.boolean() });
23
23
  const statusItem = v.looseObject({
24
24
  ref: v.nullable(v.string()),
25
- chat: v.nullable(v.string()),
25
+ /** Left out when `ref` is a message of that chat. */
26
+ chat: v.optional(v.nullable(v.string())),
26
27
  at: v.nullable(v.string()),
27
28
  who: v.optional(who),
28
29
  /** Every ref the item cites, when it cites more than `ref`. */
29
30
  evidence: v.optional(v.array(v.string())),
30
- basis: v.picklist(["store", "fact", "task", "pattern"]),
31
+ /** Left out for `store`, what most items are read from. */
32
+ basis: v.optional(v.picklist(["fact", "task", "pattern"])),
31
33
  data: v.record(v.string(), v.unknown()),
32
34
  });
33
35
  /** Where a project stands: per chat signals, open money, and what was left out. Empty lists are left out. */
@@ -35,7 +37,6 @@ export const projectStatusSchema = v.looseObject({
35
37
  project: v.string(),
36
38
  key: v.nullable(v.string()),
37
39
  name: v.string(),
38
- window: v.strictObject({ since: v.string(), until: v.string() }),
39
40
  /** Each signal's version, so a saved answer says which rules made it. */
40
41
  signals: v.record(v.string(), v.number()),
41
42
  money: v.array(v.strictObject({ direction: v.string(), currency: v.nullable(v.string()), amount: v.string(), rows: v.number() })),
@@ -43,7 +44,7 @@ export const projectStatusSchema = v.looseObject({
43
44
  chat: v.string(),
44
45
  title: v.nullable(v.string()),
45
46
  kind: v.string(),
46
- role: v.nullable(v.string()),
47
+ role: v.optional(v.string()),
47
48
  purpose: v.optional(v.string()),
48
49
  project: v.optional(v.string()),
49
50
  coverage: v.string(),
@@ -139,14 +140,33 @@ const missingOf = (found, hints) => {
139
140
  ...found.failed.map((one) => ({ field: `signal:${one.signal}`, why: one.why })),
140
141
  ];
141
142
  };
142
- const whoOf = (one) => one === null ? {} : { who: { ...(one.ref === null ? {} : { ref: one.ref }), name: one.name, owner: one.owner } };
143
- /** An item as listed: its first ref, every ref only when there are more. */
144
- const listed = ({ signal: _signal, who, evidence, ...one }) => ({
145
- ...one,
146
- ref: evidence[0]?.ref ?? one.chat,
147
- ...whoOf(who),
148
- ...(evidence.length > 1 ? { evidence: evidence.map((each) => each.ref) } : {}),
149
- });
143
+ const whoOf = (one) => one === null
144
+ ? {}
145
+ : {
146
+ who: {
147
+ ...(one.ref === null ? {} : { ref: one.ref }),
148
+ ...(one.name === null ? {} : { name: one.name }),
149
+ owner: one.owner,
150
+ },
151
+ };
152
+ const inChat = (ref, chat) => ref !== null && chat !== null && (ref === chat || ref.startsWith(`msg:${chat.slice("chat:".length)}/`));
153
+ /**
154
+ * An item as listed: its first ref, every ref only when there are more. What the rest of the item already says is
155
+ * left out: the chat its message ref names, the `store` basis, and a promise's `source`, which is its basis.
156
+ */
157
+ const listed = ({ signal: _signal, who, evidence, chat, basis, data, ...one }) => {
158
+ const ref = evidence[0]?.ref ?? chat;
159
+ const { source, ...rest } = data;
160
+ return {
161
+ ...(inChat(ref, chat) ? {} : { chat }),
162
+ ...one,
163
+ ...(basis === "store" ? {} : { basis }),
164
+ data: source === basis ? rest : data,
165
+ ref,
166
+ ...whoOf(who),
167
+ ...(evidence.length > 1 ? { evidence: evidence.map((each) => each.ref) } : {}),
168
+ };
169
+ };
150
170
  /**
151
171
  * Low signal items, counted on their chat's row instead of listed: questions others ask in a group with no role (a
152
172
  * leads group's requests), promises found by words that others wrote in a group, and past the newest one per chat.
@@ -194,7 +214,7 @@ const chatRow = (found, hints, chat, unlisted) => {
194
214
  chat: chat.ref,
195
215
  title: chat.title,
196
216
  kind: chat.kind,
197
- role: chat.role,
217
+ ...(chat.role === null ? {} : { role: chat.role }),
198
218
  ...(Array.isArray(found.purposes) && found.purposes.length === 1 ? {} : { purpose: chat.purpose }),
199
219
  ...(chat.project === found.project.ref ? {} : { project: chat.project }),
200
220
  coverage: chat.coverage.completeness.state,
@@ -225,7 +245,6 @@ export const projectStatusAnswer = (found, hints) => {
225
245
  project: found.project.ref,
226
246
  key: found.project.key,
227
247
  name: found.project.name,
228
- window: found.window,
229
248
  signals: Object.fromEntries(found.signals.map(({ id, version }) => [id, version])),
230
249
  money: found.money.open,
231
250
  chats: found.chats.map((chat) => chatRow(found, hints, chat, [...unlisted].filter((one) => one.chat === chat.ref))),
@@ -5936,6 +5936,7 @@ export declare const ENTITIES: {
5936
5936
  readonly subject: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
5937
5937
  readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
5938
5938
  readonly statement: v.StringSchema<undefined>;
5939
+ readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
5939
5940
  }, undefined>, v.StrictObjectSchema<{
5940
5941
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
5941
5942
  readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
@@ -5960,6 +5961,7 @@ export declare const ENTITIES: {
5960
5961
  readonly body: v.StringSchema<undefined>;
5961
5962
  readonly subject: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
5962
5963
  readonly project: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
5964
+ readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
5963
5965
  }, undefined>, v.StrictObjectSchema<{
5964
5966
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
5965
5967
  readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
@@ -41,6 +41,8 @@ export declare const mailFindings: v.StrictObjectSchema<{
41
41
  readonly subject: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
42
42
  readonly type: v.PicklistSchema<readonly ["fact", "statement", "question", "decision", "promise"], undefined>;
43
43
  readonly statement: v.StringSchema<undefined>;
44
+ /** Every new finding gives one; a batch staged before reason was needed on every kind has none. */
45
+ readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
44
46
  }, undefined>, v.StrictObjectSchema<{
45
47
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
46
48
  readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
@@ -65,6 +67,7 @@ export declare const mailFindings: v.StrictObjectSchema<{
65
67
  readonly body: v.StringSchema<undefined>;
66
68
  readonly subject: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
67
69
  readonly project: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
70
+ readonly reason: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
68
71
  }, undefined>, v.StrictObjectSchema<{
69
72
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
70
73
  readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
@@ -30,6 +30,8 @@ const finding = v.variant("kind", [
30
30
  subject: v.optional(reference),
31
31
  type: v.picklist(FACT_TYPES),
32
32
  statement: text,
33
+ /** Every new finding gives one; a batch staged before reason was needed on every kind has none. */
34
+ reason: v.optional(text),
33
35
  }),
34
36
  v.strictObject({
35
37
  ...base,
@@ -48,6 +50,7 @@ const finding = v.variant("kind", [
48
50
  body: text,
49
51
  subject: v.optional(reference),
50
52
  project: v.optional(reference),
53
+ reason: v.optional(text),
51
54
  }),
52
55
  v.strictObject({ ...base, kind: v.literal("link"), from: reference, to: reference, reason: text }),
53
56
  v.strictObject({ ...base, kind: v.literal("new_contact"), address: text, name: text, reason: v.optional(text) }),
@@ -65,6 +65,7 @@ const OTHERS = [
65
65
  validation("mail-findings.invalid", "a checked mail findings batch would be refused; details.problems names why, finding by finding"),
66
66
  validation("mail-findings.key-used", "the findings batch key was already used for different input"),
67
67
  validation("finding.unknown-field", "a finding has a field its kind does not take"),
68
+ validation("finding.missing-field", "a finding leaves out a field its kind needs"),
68
69
  validation("document.changed", "the document changed while it was read; nothing was saved"),
69
70
  ["document.not-imported", "not_found", "the document is not imported in this source account"],
70
71
  ["extraction.invalid-proposals", "invalid_response", "the model's memory proposals could not be used"],
@@ -78,6 +78,8 @@ const mailMarkReset = v.strictObject({
78
78
  bulk: v.number(),
79
79
  /** Emails between the two marks it skips again: drafts, envelopes, suspicious senders, partial mail. */
80
80
  skipped: v.number(),
81
+ /** How often the owner moved this mark back, this reset counted even when the mark stays: the findings key names it. */
82
+ resets: v.number(),
81
83
  /** Nothing moved: `--dry-run` only counts. */
82
84
  dry_run: v.boolean(),
83
85
  });
@@ -102,7 +104,8 @@ export const resetMarkCommand = (mail) => answerEntity(annotate(new Command("res
102
104
  .summary("move a holder's processing mark back, so it reads that mail again (owner only)")
103
105
  .description("the owner moves a holder's mark back past the mail received since --since-time, or past the last --last " +
104
106
  "emails it was given; its next reviews give that mail again. The answer says how many emails it reads " +
105
- "again. An agent cannot: a holder only moves its own mark forward, with `messages acknowledge`")
107
+ "again. Every reset counts, even one that leaves the mark where it is, so the holder's next findings key " +
108
+ "is new. An agent cannot: a holder only moves its own mark forward, with `messages acknowledge`")
106
109
  .requiredOption("--holder <name>", "whose mark: lower-case letters, digits and dashes, such as mail-triage")
107
110
  .option("--since-time <time>", "read again what was received since this ISO 8601 time, or 2d ago")
108
111
  .option("--last <n>", "read again the last n emails the holder was given", wholeNumber)
@@ -125,12 +128,15 @@ export const resetMarkCommand = (mail) => answerEntity(annotate(new Command("res
125
128
  again: reset.again,
126
129
  bulk: reset.bulk,
127
130
  skipped: reset.skipped,
131
+ resets: reset.mark.resets,
128
132
  dry_run: reset.dry,
129
133
  };
130
134
  renderAnswer(this, outputFor(this), itemOf(item), () => {
131
135
  const mark = reset.mark.throughId === reset.fromId
132
- ? `the mark stays at ${reset.fromId}`
133
- : `the mark ${reset.dry ? "would move" : "moved"} from ${reset.fromId} to ${reset.mark.throughId}`;
136
+ ? `mark unchanged at ${reset.fromId}, ${reset.dry ? "would be counted" : "counted"} as a reset ` +
137
+ `(resets ${reset.mark.resets}): the next run gets a new findings key`
138
+ : `the mark ${reset.dry ? "would move" : "moved"} from ${reset.fromId} to ${reset.mark.throughId} ` +
139
+ `(resets ${reset.mark.resets})`;
134
140
  return (`${holder} ${reset.dry ? "would read" : "reads"} ${reset.again} emails again, and ${reset.bulk} bulk ` +
135
141
  `ones as excerpts unless they are newsletters; ${reset.skipped} are skipped again; ${mark}\n`);
136
142
  });
@@ -208,7 +208,7 @@ const holderReview = async (command, mail, holder, query) => {
208
208
  const step = stepsFor(mail.mount, mailbox);
209
209
  const answer = reviewAnswer(found, mailbox, true, step);
210
210
  const skipped = Object.entries(read.skipped).filter(([, count]) => count > 0);
211
- const stage = read.findingsKey === undefined ? [] : step("mail-findings.add", { key: read.findingsKey });
211
+ const stage = read.findingsKey === undefined || emails.length === 0 ? [] : step("mail-findings.add", { key: read.findingsKey });
212
212
  answer.next = [
213
213
  ...stage,
214
214
  ...step("mail-messages.acknowledge", { holder, cursor: read.cursor }),
@@ -233,7 +233,7 @@ const holderReview = async (command, mail, holder, query) => {
233
233
  const output = outputFor(command);
234
234
  renderAnswer(command, output, answer, () => {
235
235
  output.renderer.note(read.mark
236
- ? `${holder}: past ${read.mark.throughId}; findings key ${read.findingsKey}; acknowledge with --cursor ${read.cursor}${read.more ? "; more after these" : ""}`
236
+ ? `${holder}: past ${read.mark.throughId}${emails.length ? `; findings key ${read.findingsKey}` : ""}; acknowledge with --cursor ${read.cursor}${read.more ? "; more after these" : ""}`
237
237
  : `${holder} has no mark yet: it starts from now — acknowledge with --cursor ${read.cursor}`);
238
238
  return emails.map((hit) => `${hitText(hit)}${markText(marks.get(hit.id), hit.excerptOnly)}\n`).join("");
239
239
  });
@@ -11,7 +11,10 @@ interface Cited {
11
11
  evidence: FindingEvidence[];
12
12
  }
13
13
  export type MailTaskFinding = TaskFinding;
14
- export type MailFactFinding = FactFinding;
14
+ /** The owner reads `reason` beside every finding when he decides it, so every kind gives one. */
15
+ export type MailFactFinding = FactFinding & {
16
+ reason: string;
17
+ };
15
18
  export interface MailTaskUpdateFinding extends Cited {
16
19
  kind: "task_update";
17
20
  task: string;
@@ -27,6 +30,7 @@ export interface MailMemoryFinding extends Cited {
27
30
  body: string;
28
31
  subject?: string;
29
32
  project?: string;
33
+ reason: string;
30
34
  }
31
35
  /** Address → person, person → organization, or thread → project. */
32
36
  export interface MailLinkFinding extends Cited {
@@ -39,7 +43,7 @@ export interface MailNewContactFinding extends Cited {
39
43
  kind: "new_contact";
40
44
  address: string;
41
45
  name: string;
42
- reason?: string;
46
+ reason: string;
43
47
  }
44
48
  /** An invoice the mail carries; accepting it records the invoice through `store.finance`, confirmed. */
45
49
  export interface MailInvoiceFinding extends Cited {
@@ -139,6 +143,56 @@ export interface MailFindingsInput {
139
143
  extractor: string;
140
144
  findings: MailFinding[];
141
145
  }
146
+ /**
147
+ * What each kind takes besides `id`, `kind` and `evidence`, in the order the validator reads them: `needs` it refuses
148
+ * without, `may` it takes. `--help`, `details.allowed` and `details.needs` all read this table, so they cannot drift
149
+ * from the checks. Some fields go together (due_at with due_evidence) or replace each other (counterparty or
150
+ * counterparty_name); the check of each kind says which.
151
+ */
152
+ export declare const MAIL_FINDING_FIELDS: {
153
+ readonly task: {
154
+ readonly needs: readonly ["type", "title", "reason", "commitment"];
155
+ readonly may: readonly ["project", "dueAt", "dueEvidence", "assignee", "assigneeEvidence", "externalId", "sourceUrl"];
156
+ };
157
+ readonly task_update: {
158
+ readonly needs: readonly ["task", "change", "reason"];
159
+ readonly may: readonly ["dueAt", "detail"];
160
+ };
161
+ readonly fact: {
162
+ readonly needs: readonly ["type", "statement", "reason"];
163
+ readonly may: readonly ["subject", "project"];
164
+ };
165
+ readonly memory: {
166
+ readonly needs: readonly ["memoryKind", "scope", "body", "reason"];
167
+ readonly may: readonly ["subject", "project"];
168
+ };
169
+ readonly link: {
170
+ readonly needs: readonly ["from", "to", "reason"];
171
+ readonly may: readonly [];
172
+ };
173
+ readonly new_contact: {
174
+ readonly needs: readonly ["address", "name", "reason"];
175
+ readonly may: readonly [];
176
+ };
177
+ readonly invoice: {
178
+ readonly needs: readonly ["direction", "currency", "lines", "reason"];
179
+ readonly may: readonly ["counterparty", "counterpartyName", "counterpartyEmail", "total", "number", "issueDate", "dueDate", "project"];
180
+ };
181
+ readonly payment: {
182
+ readonly needs: readonly ["direction", "amount", "currency", "date", "reason"];
183
+ readonly may: readonly ["payer", "payerName", "payerEmail", "payee", "payeeName", "payeeEmail", "reference", "allocations", "project"];
184
+ };
185
+ readonly order: {
186
+ readonly needs: readonly ["purpose", "supplierName", "reason"];
187
+ readonly may: readonly ["customer", "customerName", "customerEmail", "supplier", "number", "total", "currency", "status", "orderDate", "fulfilledDate", "invoice", "payment", "project", "sourceUrl"];
188
+ };
189
+ readonly order_update: {
190
+ readonly needs: readonly ["supplierName", "number", "status", "reason"];
191
+ readonly may: readonly ["date"];
192
+ };
193
+ };
194
+ /** One line per kind, `kind: needed fields; optional ones`, for `mail findings add --help`. */
195
+ export declare const mailFindingFieldsHelp: () => string;
142
196
  export declare const validateMailFindings: (input: MailFindingsInput) => MailFindingsInput;
143
197
  /** Why one finding, or the batch when `finding` is null, would be refused. */
144
198
  export interface MailFindingProblem {