@agentorg/cli 0.0.1 → 0.0.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.
Files changed (90) hide show
  1. package/README.md +3 -3
  2. package/dist/src/bindings/mcp/encoding.js +1 -4
  3. package/dist/src/clients/cli/commands.js +1 -1
  4. package/dist/src/clients/cli/index.js +1 -1
  5. package/dist/src/clients/mcp/agent.d.ts +3 -5
  6. package/dist/src/clients/mcp/agent.js +10 -23
  7. package/dist/src/clients/mcp/command.d.ts +1 -1
  8. package/dist/src/clients/mcp/command.js +5 -11
  9. package/dist/src/clients/mcp/projects.d.ts +1 -2
  10. package/dist/src/clients/mcp/projects.js +5 -9
  11. package/dist/src/clients/mcp/sessions.d.ts +1 -5
  12. package/dist/src/clients/mcp/sessions.js +1 -10
  13. package/dist/src/clients/mcp/setup.js +4 -8
  14. package/dist/src/clients/mcp/waiter.d.ts +0 -2
  15. package/dist/src/clients/mcp/waiter.js +9 -20
  16. package/dist/src/domain/actors.js +3 -7
  17. package/dist/src/domain/attention.d.ts +8 -7
  18. package/dist/src/domain/attention.js +31 -47
  19. package/dist/src/domain/capabilities.js +0 -1
  20. package/dist/src/domain/claims.js +2 -2
  21. package/dist/src/domain/errors.d.ts +1 -1
  22. package/dist/src/domain/errors.js +1 -1
  23. package/dist/src/domain/index.d.ts +1 -15
  24. package/dist/src/domain/model/attention.d.ts +1 -5
  25. package/dist/src/domain/model/attention.js +5 -6
  26. package/dist/src/domain/model/builtins.d.ts +12 -179
  27. package/dist/src/domain/model/builtins.js +3 -6
  28. package/dist/src/domain/model/claims.d.ts +1 -3
  29. package/dist/src/domain/model/claims.js +1 -3
  30. package/dist/src/domain/model/index.d.ts +0 -7
  31. package/dist/src/domain/model/index.js +0 -3
  32. package/dist/src/domain/model/json-schema.d.ts +1 -4
  33. package/dist/src/domain/model/records.d.ts +0 -3
  34. package/dist/src/domain/model/records.js +3 -7
  35. package/dist/src/domain/model/system.d.ts +1 -5
  36. package/dist/src/domain/model/system.js +1 -5
  37. package/dist/src/domain/model/team.d.ts +4 -119
  38. package/dist/src/domain/model/team.js +8 -19
  39. package/dist/src/domain/model/workspace.d.ts +2 -86
  40. package/dist/src/domain/model/workspace.js +0 -1
  41. package/dist/src/domain/ports.d.ts +3 -4
  42. package/dist/src/domain/registry.d.ts +2 -16
  43. package/dist/src/domain/registry.js +22 -54
  44. package/dist/src/domain/system.js +1 -5
  45. package/dist/src/domain/team.d.ts +1 -1
  46. package/dist/src/domain/team.js +13 -33
  47. package/dist/src/domain/workspace.js +2 -10
  48. package/dist/src/home.d.ts +5 -5
  49. package/dist/src/home.js +6 -6
  50. package/dist/src/server/command.js +2 -2
  51. package/dist/src/server/index.d.ts +1 -1
  52. package/dist/src/server/index.js +2 -2
  53. package/dist/src/storage/sqlite/database.d.ts +5 -0
  54. package/dist/src/storage/sqlite/database.js +32 -1
  55. package/dist/src/storage/sqlite/index.js +1 -2
  56. package/dist/src/storage/sqlite/schema.d.ts +3 -4
  57. package/dist/src/storage/sqlite/schema.js +4 -4
  58. package/node_modules/@uniac/agentic-workspace/README.md +5 -5
  59. package/node_modules/@uniac/agentic-workspace/dist/examples/authority.d.ts +0 -1
  60. package/node_modules/@uniac/agentic-workspace/dist/examples/authority.js +3 -16
  61. package/node_modules/@uniac/agentic-workspace/dist/src/actors/index.d.ts +13 -13
  62. package/node_modules/@uniac/agentic-workspace/dist/src/actors/index.js +9 -25
  63. package/node_modules/@uniac/agentic-workspace/dist/src/contracts/common.d.ts +1 -9
  64. package/node_modules/@uniac/agentic-workspace/dist/src/contracts/common.js +2 -4
  65. package/node_modules/@uniac/agentic-workspace/dist/src/contracts/forms.d.ts +0 -13
  66. package/node_modules/@uniac/agentic-workspace/dist/src/contracts/forms.js +0 -3
  67. package/node_modules/@uniac/agentic-workspace/dist/src/contracts/messages.d.ts +0 -3
  68. package/node_modules/@uniac/agentic-workspace/dist/src/contracts/messages.js +3 -3
  69. package/node_modules/@uniac/agentic-workspace/dist/src/contracts/work.d.ts +2 -15
  70. package/node_modules/@uniac/agentic-workspace/dist/src/contracts/work.js +9 -14
  71. package/node_modules/@uniac/agentic-workspace/dist/src/mcp/index.js +1 -1
  72. package/node_modules/@uniac/agentic-workspace/dist/src/persistence/index.d.ts +1 -4
  73. package/node_modules/@uniac/agentic-workspace/dist/src/persistence/sqlite.js +2 -15
  74. package/node_modules/@uniac/agentic-workspace/dist/src/records.d.ts +0 -7
  75. package/node_modules/@uniac/agentic-workspace/dist/src/records.js +0 -14
  76. package/node_modules/@uniac/agentic-workspace/dist/src/work/asks.d.ts +20 -19
  77. package/node_modules/@uniac/agentic-workspace/dist/src/work/asks.js +39 -41
  78. package/node_modules/@uniac/agentic-workspace/dist/src/work/index.js +18 -24
  79. package/node_modules/@uniac/agentic-workspace/docs/contracts.md +23 -29
  80. package/node_modules/@uniac/agentic-workspace/docs/design.md +13 -13
  81. package/node_modules/@uniac/agentic-workspace/docs/hub-integration.md +25 -50
  82. package/node_modules/@uniac/agentic-workspace/package.json +1 -1
  83. package/package.json +3 -3
  84. package/skills/agentorg/SKILL.md +39 -38
  85. package/skills/agentorg/references/claude-code.md +3 -4
  86. package/vendor/uniac-agentic-workspace-0.0.2-8aebc44.tgz +0 -0
  87. package/vendor/workspace.json +6 -6
  88. package/dist/src/clients/mcp/levels.d.ts +0 -17
  89. package/dist/src/clients/mcp/levels.js +0 -7
  90. package/vendor/uniac-agentic-workspace-0.0.1-26fd0ee.tgz +0 -0
@@ -193,10 +193,6 @@ export declare const Work: z.ZodObject<{
193
193
  note: z.ZodOptional<z.ZodString>;
194
194
  at: z.ZodISODateTime;
195
195
  }, z.core.$strict>>;
196
- raises: z.ZodArray<z.ZodObject<{
197
- actorID: z.ZodString;
198
- at: z.ZodISODateTime;
199
- }, z.core.$strict>>;
200
196
  resolution: z.ZodOptional<z.ZodObject<{
201
197
  text: z.ZodString;
202
198
  reason: z.ZodOptional<z.ZodEnum<{
@@ -213,7 +209,6 @@ export type WaitsOn = z.infer<typeof WaitsOn>;
213
209
  export type Work = z.infer<typeof Work>;
214
210
  export declare const WorkContracts: {
215
211
  work_create: {
216
- level: "organization" | "report" | "team";
217
212
  description: string;
218
213
  input: z.ZodObject<{
219
214
  title: z.ZodString;
@@ -235,7 +230,6 @@ export declare const WorkContracts: {
235
230
  writes: boolean;
236
231
  };
237
232
  work_get: {
238
- level: "organization" | "report" | "team";
239
233
  description: string;
240
234
  input: z.ZodObject<{
241
235
  ticketID: z.ZodOptional<z.ZodString>;
@@ -274,14 +268,11 @@ export declare const WorkContracts: {
274
268
  writes: boolean;
275
269
  };
276
270
  work_list: {
277
- level: "organization" | "report" | "team";
278
271
  description: string;
279
272
  input: z.ZodObject<{
280
- ownerID: z.ZodOptional<z.ZodString>;
273
+ ownerID: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
281
274
  requesterID: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
282
275
  waitsOnActorID: z.ZodOptional<z.ZodString>;
283
- ownerWithin: z.ZodOptional<z.ZodString>;
284
- requesterWithin: z.ZodOptional<z.ZodString>;
285
276
  status: z.ZodOptional<z.ZodArray<z.ZodEnum<{
286
277
  accepted: "accepted";
287
278
  active: "active";
@@ -299,7 +290,6 @@ export declare const WorkContracts: {
299
290
  writes: boolean;
300
291
  };
301
292
  work_update: {
302
- level: "organization" | "report" | "team";
303
293
  description: string;
304
294
  input: z.ZodObject<{
305
295
  ticketID: z.ZodOptional<z.ZodString>;
@@ -357,7 +347,6 @@ export declare const WorkContracts: {
357
347
  writes: boolean;
358
348
  };
359
349
  work_submit: {
360
- level: "organization" | "report" | "team";
361
350
  description: string;
362
351
  input: z.ZodObject<{
363
352
  ticketID: z.ZodString;
@@ -369,14 +358,12 @@ export declare const WorkContracts: {
369
358
  writes: boolean;
370
359
  };
371
360
  work_answer: {
372
- level: "organization" | "report" | "team";
373
361
  description: string;
374
362
  input: z.ZodObject<{
375
363
  askID: z.ZodString;
376
364
  ticketID: z.ZodOptional<z.ZodString>;
377
- answers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
365
+ answers: z.ZodRecord<z.ZodString, z.ZodString>;
378
366
  note: z.ZodOptional<z.ZodString>;
379
- raise: z.ZodOptional<z.ZodLiteral<true>>;
380
367
  }, z.core.$strict>;
381
368
  output: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
382
369
  writes: boolean;
@@ -64,32 +64,30 @@ const ticketID = z.strictObject({ ticketID: ID });
64
64
  const mutation = ticketID.extend({ expectedRevision: Revision.optional() });
65
65
  /** Every supplied filter must match; the page limit applies after them. */
66
66
  const filter = z.strictObject({
67
- ownerID: ID.optional(),
67
+ ownerID: OneOrMany.optional(),
68
68
  requesterID: OneOrMany.optional(),
69
69
  waitsOnActorID: ID.optional(),
70
- ownerWithin: ID.optional(),
71
- requesterWithin: ID.optional(),
72
70
  status: z.array(WorkStatus).min(1).optional(),
73
71
  updatedSince: z.iso.datetime().optional(),
74
72
  order: z.literal("recent").optional(),
75
73
  });
76
74
  export const WorkContracts = {
77
- work_create: contract("report", "Create Work; the caller requests it and owns it unless ownerID names another Actor.", z.strictObject({
75
+ work_create: contract("Create Work; the caller requests it and owns it unless ownerID names another Actor.", z.strictObject({
78
76
  title: Title,
79
77
  goal: Text.optional(),
80
78
  ownerID: ID.optional(),
81
79
  links: Links.optional(),
82
80
  waitsOn: WaitsOn.optional(),
83
81
  }), Work, true),
84
- work_get: contract("report", "Read Work with all its asks, by ticketID, or by askID: the Work an ask (or the current submission) is on, or an older ask with no Work alone. History only when asked, for the Work and each ask.", z
82
+ work_get: contract("Read Work with all its asks, by ticketID, or by askID: the Work an ask (or the current submission) is on, or an older ask with no Work alone. History only when asked, for the Work and each ask.", z
85
83
  .strictObject({
86
84
  ticketID: ID.optional(),
87
85
  askID: ID.optional(),
88
86
  history: z.boolean().default(false),
89
87
  })
90
88
  .refine((v) => (v.ticketID === undefined) !== (v.askID === undefined), { error: "name exactly one of ticketID or askID" }), z.union([Work, Form])),
91
- work_list: contract("report", "List shared work matching every filter, by update order or most recent first; each item carries its open asks.", filter.extend({ after: Next.optional(), limit: Limit }), listOf(Work)),
92
- work_update: contract("report", "One of three forms, each in a call of its own. Edits: the owner edits title, progress, open/active status, links and waitsOn (null clears it); the requester edits goal or owner, or cancels, with expectedRevision. Progress is a paragraph, then optionally a Markdown task list of steps: `- [x]` done, `- [ ]` not done. {ticketID, ask}: the owner asks on its Work and gets the ask back; mark approval when the answer can't be undone. {close}: the asker resolves or cancels its open ask, recording why.", z
89
+ work_list: contract("List shared work matching every filter, by update order or most recent first; each item carries its open asks.", filter.extend({ after: Next.optional(), limit: Limit }), listOf(Work)),
90
+ work_update: contract("One of three forms, each in a call of its own. Edits: the owner edits title, progress, open/active status, links and waitsOn (null clears it); the requester edits goal or owner, or cancels, with expectedRevision. Progress is a paragraph, then optionally a Markdown task list of steps: `- [x]` done, `- [ ]` not done. {ticketID, ask}: the owner asks on its Work and gets the ask back; mark approval when the answer can't be undone. {close}: the asker resolves or cancels its open ask, recording why.", z
93
91
  .strictObject({
94
92
  ticketID: ID.optional(),
95
93
  expectedRevision: Revision.optional(),
@@ -117,14 +115,11 @@ export const WorkContracts = {
117
115
  }, {
118
116
  error: "use one form per call: edits with ticketID; {ticketID, ask} and nothing else; or {close} and nothing else",
119
117
  }), z.union([Work, Form]), true),
120
- work_submit: contract("report", "Owner submits the result; Work the owner requested itself completes, other Work waits for its requester's answer.", mutation.extend({ result: Text, evidence: z.array(Text).max(100).default([]) }), Work, true),
121
- work_answer: contract("team", "Answer an ask by its askID: answers for its fields and an optional note. Any Actor answers; only the asker's managers answer an approval; the first answer resolves the ask unless it collects. raise: true, by the Actor the ask is routed to, passes it one manager up. Or answer the Work's current submission by its ID: answers {verdict: accepted or changes_requested}, the note as the comment, which requesting changes needs; only its requester answers, or its nearest open manager once it is closed.", z
122
- .strictObject({
118
+ work_submit: contract("Owner submits the result; Work the owner requested itself completes, other Work waits for its requester's answer.", mutation.extend({ result: Text, evidence: z.array(Text).max(100).default([]) }), Work, true),
119
+ work_answer: contract("Answer an ask by its askID: answers for its fields and an optional note. Any Actor answers; the first answer resolves the ask unless it collects. An approval is answered only by the Actor it is routed to, never its asker: the Work's requester, or the person when the asker requested the Work itself or once the requester is closed. Or answer the Work's current submission by its ID: answers {verdict: accepted or changes_requested}, the note as the comment, which requesting changes needs; only its requester answers, or the person once it is closed.", z.strictObject({
123
120
  askID: ID,
124
121
  ticketID: ID.optional(),
125
- answers: z.record(z.string(), z.string().max(20000)).optional(),
122
+ answers: z.record(z.string(), z.string().max(20000)),
126
123
  note: Text.optional(),
127
- raise: z.literal(true).optional(),
128
- })
129
- .refine((v) => v.raise ? v.answers === undefined && v.note === undefined : v.answers !== undefined, { error: "give answers, or raise: true with no answers or note" }), z.union([Work, Form]), true),
124
+ }), z.union([Work, Form]), true),
130
125
  };
@@ -24,7 +24,7 @@ export function createMcpServer(clients, purpose) {
24
24
  work_submit: bind(WorkContracts.work_submit, (input) => work.submit(input)),
25
25
  work_answer: bind(WorkContracts.work_answer, (input) => work.answer(input)),
26
26
  };
27
- const server = new Server({ name: `agentic-workspace-${purpose}`, version: "0.0.1" }, { capabilities: { tools: {} } });
27
+ const server = new Server({ name: `agentic-workspace-${purpose}`, version: "0.0.2" }, { capabilities: { tools: {} } });
28
28
  server.setRequestHandler(ListToolsRequestSchema, async () => {
29
29
  try {
30
30
  actors.get();
@@ -14,8 +14,6 @@ export interface RecordTable<T, C extends string = never> {
14
14
  get(id: string): Snapshot<T> | undefined;
15
15
  /** Current snapshots in cursor order whose every filtered column holds one of its values. */
16
16
  where(filter: Filter<C>): Snapshot<T>[];
17
- /** The distinct values one indexed column holds among the rows a filter keeps. */
18
- values(column: C, filter: Filter<C>): string[];
19
17
  history(id: string): HistoryEntry[];
20
18
  head(): number;
21
19
  /** Called inside transaction; expectedRevision is absent only for insertion. */
@@ -25,8 +23,7 @@ export interface WorkspaceStore {
25
23
  messages: RecordTable<MessageState, "sender" | "recipient">;
26
24
  /** A stored `blocked` status is indexed as `active`; submission is the current submission's ID. */
27
25
  work: RecordTable<WorkState, "owner" | "requester" | "status" | "submission">;
28
- /** route is where an ask's route starts: its last raiser, else its asker. */
29
- forms: RecordTable<FormState, "requester" | "status" | "work" | "route">;
26
+ forms: RecordTable<FormState, "requester" | "status" | "work">;
30
27
  /** Callback is synchronous; state checks and writes share one transaction. */
31
28
  transaction<T>(callback: () => T): T;
32
29
  close(): void;
@@ -45,8 +45,8 @@ const UPGRADES = [
45
45
  }
46
46
  },
47
47
  // Version 4: indexed columns for the fields lists select by. A stored `blocked` is indexed as
48
- // `active`, the status Work reads it as, and an ask's route starts at its last raiser, else its
49
- // asker.
48
+ // `active`, the status Work reads it as. Asks' `route` is unread; its (status, route) index serves
49
+ // status selects.
50
50
  // A party's index leads with the party and then status, so a party's Work or asks in some
51
51
  // statuses is one index range whatever the planner knows of the data.
52
52
  (db) => db.exec(`
@@ -128,11 +128,6 @@ export function openSqlite(path) {
128
128
  throw new Error(`${name} has no indexed column ${column}`);
129
129
  return column;
130
130
  };
131
- /** The index each column leads, so a column's distinct values come from its own index. */
132
- const leading = new Map(db
133
- .prepare("SELECT (SELECT name FROM pragma_index_info(l.name) WHERE seqno=0) AS lead, l.name FROM pragma_index_list(?) l")
134
- .all(name)
135
- .map((row) => [String(row.lead), String(row.name)]));
136
131
  const statements = new Map();
137
132
  const prepared = (sql) => {
138
133
  const statement = statements.get(sql) ?? db.prepare(sql);
@@ -174,14 +169,6 @@ export function openSqlite(path) {
174
169
  .all(...values)
175
170
  .map(decode);
176
171
  },
177
- values(column, filter) {
178
- const { sql, values } = matching(filter);
179
- const selected = indexed(column);
180
- const index = leading.get(selected);
181
- return prepared(`SELECT DISTINCT m.${selected} AS value FROM ${name} m${index === undefined ? "" : ` INDEXED BY ${index}`} WHERE m.${selected} IS NOT NULL${sql}`)
182
- .all(...values)
183
- .map((row) => String(row.value));
184
- },
185
172
  save(id, data, event, expectedRevision) {
186
173
  const current = get(id);
187
174
  requireThat(current ? current.revision === expectedRevision : expectedRevision === undefined, "conflict", "record revision changed");
@@ -27,13 +27,6 @@ export declare function traverse<T, C extends string>(actors: ActorsClient, tabl
27
27
  limit: number;
28
28
  order?: "recent" | undefined;
29
29
  }, scope: unknown, indexed: () => Filter<C>, visible: (row: Snapshot<T>) => boolean, mentions: (row: T) => (string | undefined)[]): List<Published<T>>;
30
- /**
31
- * A filter whose subtree criteria are resolved to the Actors each party column holds among the
32
- * rows the filter keeps: one membership read per Actor rather than per row.
33
- */
34
- export declare function subtrees<C extends string>(actors: ActorsClient, table: RecordTable<unknown, C>, filter: Filter<C>, within: {
35
- [K in C]?: string | undefined;
36
- }): Filter<C>;
37
30
  /** Names of the Actors a page mentions; Actors the caller can't see are left out. */
38
31
  export declare function names(actors: ActorsClient, ids: Iterable<string | undefined>): {
39
32
  [k: string]: {
@@ -18,7 +18,6 @@ export function readThrough(table, read) {
18
18
  return row && read(row);
19
19
  },
20
20
  where: (filter) => table.where(filter).map(read),
21
- values: (column, filter) => table.values(column, filter),
22
21
  history: (id) => table.history(id),
23
22
  head: () => table.head(),
24
23
  save: (id, data, event, expectedRevision) => table.save(id, data, event, expectedRevision),
@@ -71,19 +70,6 @@ export function traverse(actors, table, query, scope, indexed, visible, mentions
71
70
  actors: names(actors, items.flatMap(mentions)),
72
71
  };
73
72
  }
74
- /**
75
- * A filter whose subtree criteria are resolved to the Actors each party column holds among the
76
- * rows the filter keeps: one membership read per Actor rather than per row.
77
- */
78
- export function subtrees(actors, table, filter, within) {
79
- const resolved = { ...filter };
80
- for (const [column, anchor] of Object.entries(within))
81
- if (anchor !== undefined)
82
- resolved[column] = table
83
- .values(column, filter)
84
- .filter((actorID) => actors.within(actorID, anchor));
85
- return resolved;
86
- }
87
73
  /** Names of the Actors a page mentions; Actors the caller can't see are left out. */
88
74
  export function names(actors, ids) {
89
75
  const found = [];
@@ -1,10 +1,11 @@
1
- import { type ActorsClient } from "../actors/index.js";
2
- import { type AskClose, type AskInput, type Field, type Form, type FormState, type Output } from "../contracts/index.js";
1
+ import { type ActorsClient, standIn } from "../actors/index.js";
2
+ import { type AskClose, type AskInput, type Field, type Form, type FormState, type Output, type WorkState } from "../contracts/index.js";
3
3
  import type { RecordTable, Snapshot } from "../persistence/index.js";
4
- type Asks = RecordTable<FormState, "requester" | "status" | "work" | "route">;
4
+ type Asks = RecordTable<FormState, "requester" | "status" | "work">;
5
+ type Requesters = Pick<RecordTable<WorkState>, "get">;
5
6
  /**
6
- * Asks as this package reads them: a `cause` stored before asks lived on Work is left unread, and
7
- * an ask stored then has no raises.
7
+ * Asks as this package reads them: a `cause` stored before asks lived on Work, and the `raises`
8
+ * earlier builds stored, are left unread.
8
9
  */
9
10
  export declare function asksAsRead(forms: Asks): Asks;
10
11
  export declare function checkFields(fields: Field[]): void;
@@ -54,10 +55,6 @@ export declare function asksOn(forms: Asks, ticketIDs: string[], options: {
54
55
  note?: string | undefined;
55
56
  at: string;
56
57
  }>;
57
- raises: {
58
- actorID: string;
59
- at: string;
60
- }[];
61
58
  resolution?: {
62
59
  text: string;
63
60
  reason?: "cancelled" | "reassigned" | "submitted" | undefined;
@@ -66,16 +63,20 @@ export declare function asksOn(forms: Asks, ticketIDs: string[], options: {
66
63
  }[]>;
67
64
  /** A new ask on a Work, by its owner, which the caller has checked may ask on it. */
68
65
  export declare function created(forms: Asks, caller: string, ticketID: string, input: Output<typeof AskInput>): Form;
69
- /** The asker, or its nearest open manager once it is closed, resolves or cancels its open ask. */
66
+ /** The asker, or the person once the asker is closed, resolves or cancels its open ask. */
70
67
  export declare function closed(actors: ActorsClient, forms: Asks, request: Output<typeof AskClose>, caller: string): Form;
71
- /** A complete answer set by one responder; only the asker's strict managers answer an approval. */
72
- export declare function responded(actors: ActorsClient, forms: Asks, row: Snapshot<FormState>, caller: string, answers: Record<string, string>, note: string | undefined): Form;
73
- /** The Actor an open ask is routed to passes it one manager up; a raise never resolves it. */
74
- export declare function raised(actors: ActorsClient, forms: Asks, row: Snapshot<FormState>, caller: string): Form;
75
- /** The Actor an open ask is routed to, from its last raiser or else its asker; null for none. */
76
- export declare function routeOf(actors: ActorsClient, row: FormState): string | null;
77
- /** The route origins of open asks, among those the filter keeps, that route to an Actor. */
78
- export declare function routedTo(actors: ActorsClient, forms: Asks, actorID: string, requester: string[] | undefined): string[];
68
+ /**
69
+ * A complete answer set by one responder; only the Actor an approval is routed to answers it, never
70
+ * its asker.
71
+ */
72
+ export declare function responded(actors: ActorsClient, forms: Asks, work: Requesters, row: Snapshot<FormState>, caller: string, answers: Record<string, string>, note: string | undefined): Form;
73
+ /**
74
+ * The Actor each open ask is routed to: its Work's requester, or the person when the asker requested
75
+ * the Work itself or the ask names no Work. A closed requester's asks go to the person. A router
76
+ * reads each Work's requester, each stand-in and the person once. Rights use the strict stand-in;
77
+ * only the hosts' routed read counts an Actor the caller can't see as closed.
78
+ */
79
+ export declare function router(actors: ActorsClient, work: Requesters, through?: typeof standIn): (row: FormState) => string;
79
80
  declare const CANCELLED: {
80
81
  readonly submitted: "Cancelled with the Work's submission.";
81
82
  readonly cancelled: "Cancelled with the Work's cancellation.";
@@ -83,6 +84,6 @@ declare const CANCELLED: {
83
84
  };
84
85
  /** Cancels the open asks on a Work whose asker no longer moves it. */
85
86
  export declare function cancelOpen(forms: Asks, ticketID: string, actorID: string, at: string, reason: keyof typeof CANCELLED): void;
86
- /** The Actors an ask mentions: its asker, responders and raisers. */
87
+ /** The Actors an ask mentions: its asker and responders. */
87
88
  export declare const mentioned: (row: FormState) => string[];
88
89
  export {};
@@ -1,19 +1,19 @@
1
1
  /**
2
- * An ask's rules and writes: its fields, answers, raises, route and closing. Asks are stored in
3
- * their own rows of the forms table; callers run each write inside the store's transaction.
2
+ * An ask's rules and writes: its fields, answers, route and closing. Asks are stored in their own
3
+ * rows of the forms table; callers run each write inside the store's transaction.
4
4
  */
5
5
  import { randomUUID } from "node:crypto";
6
- import { routedStandIn, standIn } from "../actors/index.js";
6
+ import { standIn } from "../actors/index.js";
7
7
  import { requireThat, } from "../contracts/index.js";
8
8
  import { now, published, readThrough, recorded, stateOf } from "../records.js";
9
9
  /**
10
- * Asks as this package reads them: a `cause` stored before asks lived on Work is left unread, and
11
- * an ask stored then has no raises.
10
+ * Asks as this package reads them: a `cause` stored before asks lived on Work, and the `raises`
11
+ * earlier builds stored, are left unread.
12
12
  */
13
13
  export function asksAsRead(forms) {
14
14
  return readThrough(forms, (row) => {
15
- const { cause: _cause, ...ask } = row;
16
- return { ...ask, raises: ask.raises ?? [] };
15
+ const { cause: _cause, raises: _raises, ...ask } = row;
16
+ return ask;
17
17
  });
18
18
  }
19
19
  export function checkFields(fields) {
@@ -55,10 +55,9 @@ export function created(forms, caller, ticketID, input) {
55
55
  status: "open",
56
56
  createdAt: at,
57
57
  responses: {},
58
- raises: [],
59
58
  }, { actorID: caller, at, action: "created", detail: { ...input, workID: ticketID } }));
60
59
  }
61
- /** The asker, or its nearest open manager once it is closed, resolves or cancels its open ask. */
60
+ /** The asker, or the person once the asker is closed, resolves or cancels its open ask. */
62
61
  export function closed(actors, forms, request, caller) {
63
62
  const row = recorded(forms, request.askID);
64
63
  requireThat(standIn(actors, row.requesterID) === caller, "forbidden", "only the asker closes its ask");
@@ -66,10 +65,13 @@ export function closed(actors, forms, request, caller) {
66
65
  const resolution = { text: request.resolution, at: now() };
67
66
  return published(forms.save(row.formID, { ...stateOf(row), status: request.status, resolution }, { actorID: caller, at: resolution.at, action: request.status, detail: resolution }, row.revision));
68
67
  }
69
- /** A complete answer set by one responder; only the asker's strict managers answer an approval. */
70
- export function responded(actors, forms, row, caller, answers, note) {
68
+ /**
69
+ * A complete answer set by one responder; only the Actor an approval is routed to answers it, never
70
+ * its asker.
71
+ */
72
+ export function responded(actors, forms, work, row, caller, answers, note) {
71
73
  requireThat(row.status === "open", "conflict", "the ask is closed");
72
- requireThat(!row.approval || (caller !== row.requesterID && actors.within(row.requesterID, caller)), "forbidden", "only the asker's managers answer an approval");
74
+ requireThat(!row.approval || (caller !== row.requesterID && router(actors, work)(row) === caller), "forbidden", "only the Actor an approval is routed to answers it, never its asker");
73
75
  for (const name of Object.keys(answers))
74
76
  requireThat(row.fields.some((field) => field.name === name), "invalid_input", "answer names an unknown field");
75
77
  for (const field of row.fields) {
@@ -87,32 +89,32 @@ export function responded(actors, forms, row, caller, answers, note) {
87
89
  responses: { ...row.responses, [caller]: { ...answer, at } },
88
90
  }, { actorID: caller, at, action: "responded", detail: answer }, row.revision));
89
91
  }
90
- /** The Actor an open ask is routed to passes it one manager up; a raise never resolves it. */
91
- export function raised(actors, forms, row, caller) {
92
- requireThat(row.status === "open", "conflict", "the ask is closed");
93
- requireThat(routeOf(actors, row) === caller && above(actors, caller) !== null, "forbidden", "only the Actor the ask is routed to raises it, to an open manager above it");
94
- const raise = { actorID: caller, at: now() };
95
- return published(forms.save(row.formID, { ...stateOf(published(row)), raises: [...row.raises, raise] }, { actorID: caller, at: raise.at, action: "raised", detail: raise }, row.revision));
96
- }
97
92
  /**
98
- * Where an Actor's asks route: its manager, or that manager's nearest open manager. Raising walks it
99
- * strictly; only the routed read walks past Actors the caller can't see.
93
+ * The Actor each open ask is routed to: its Work's requester, or the person when the asker requested
94
+ * the Work itself or the ask names no Work. A closed requester's asks go to the person. A router
95
+ * reads each Work's requester, each stand-in and the person once. Rights use the strict stand-in;
96
+ * only the hosts' routed read counts an Actor the caller can't see as closed.
100
97
  */
101
- function above(actors, actorID, through = standIn) {
102
- const manager = actors.manager(actorID);
103
- return manager === null ? null : through(actors, manager);
104
- }
105
- /** The Actor an open ask is routed to, from its last raiser or else its asker; null for none. */
106
- export function routeOf(actors, row) {
107
- return row.status === "open"
108
- ? above(actors, row.raises.at(-1)?.actorID ?? row.requesterID)
109
- : null;
98
+ export function router(actors, work, through = standIn) {
99
+ const requesterOf = remembered((workID) => work.get(workID)?.requesterID);
100
+ const standInOf = remembered((actorID) => through(actors, actorID));
101
+ let person;
102
+ return (row) => {
103
+ const requester = row.workID === undefined ? undefined : requesterOf(row.workID);
104
+ if (requester !== undefined && requester !== row.requesterID)
105
+ return standInOf(requester);
106
+ person ??= actors.person();
107
+ return person;
108
+ };
110
109
  }
111
- /** The route origins of open asks, among those the filter keeps, that route to an Actor. */
112
- export function routedTo(actors, forms, actorID, requester) {
113
- return forms
114
- .values("route", { status: ["open"], requester })
115
- .filter((origin) => above(actors, origin, routedStandIn) === actorID);
110
+ /** Reads each key once for the life of the returned function. */
111
+ function remembered(read) {
112
+ const values = new Map();
113
+ return (key) => {
114
+ if (!values.has(key))
115
+ values.set(key, read(key));
116
+ return values.get(key);
117
+ };
116
118
  }
117
119
  const CANCELLED = {
118
120
  submitted: "Cancelled with the Work's submission.",
@@ -126,9 +128,5 @@ export function cancelOpen(forms, ticketID, actorID, at, reason) {
126
128
  forms.save(row.formID, { ...stateOf(published(row)), status: "cancelled", resolution }, { actorID, at, action: "cancelled", detail: resolution }, row.revision);
127
129
  }
128
130
  }
129
- /** The Actors an ask mentions: its asker, responders and raisers. */
130
- export const mentioned = (row) => [
131
- row.requesterID,
132
- ...Object.keys(row.responses),
133
- ...row.raises.map((raise) => raise.actorID),
134
- ];
131
+ /** The Actors an ask mentions: its asker and responders. */
132
+ export const mentioned = (row) => [row.requesterID, ...Object.keys(row.responses)];
@@ -1,14 +1,14 @@
1
1
  /** Work, the unit of progress, and its asks: completed on submit or by its requester's answer. */
2
2
  import { randomUUID } from "node:crypto";
3
- import { activeActor, standIn } from "../actors/index.js";
3
+ import { activeActor, routedStandIn, standIn } from "../actors/index.js";
4
4
  import { AskQuery, parse, requireThat, SubmissionAnswer, WorkContracts, } from "../contracts/index.js";
5
- import { names, now, published, readThrough, recorded, stateOf, subtrees, traverse, } from "../records.js";
6
- import { asksAsRead, asksOn, cancelOpen, checkFields, closed, created, mentioned, raised, responded, routedTo, } from "./asks.js";
5
+ import { names, now, published, readThrough, recorded, stateOf, traverse } from "../records.js";
6
+ import { asksAsRead, asksOn, cancelOpen, checkFields, closed, created, mentioned, responded, router, } from "./asks.js";
7
7
  export function createWork(actors, store) {
8
8
  const table = readThrough(store.work, (row) => row.status === "blocked" ? { ...row, status: "active" } : row);
9
9
  const forms = asksAsRead(store.forms);
10
10
  const editable = (row) => requireThat(row.status === "open" || row.status === "active" || row.status === "changes_requested", "conflict", "work is frozen for review, accepted or cancelled");
11
- /** The requester's rights, which pass to its nearest open manager once it is closed. */
11
+ /** The requester's rights, which pass to the person once it is closed. */
12
12
  const requester = (row, caller) => standIn(actors, row.requesterID) === caller;
13
13
  /** What Work waits on must exist; text needs no check. */
14
14
  function waitable(waitsOn) {
@@ -58,7 +58,6 @@ export function createWork(actors, store) {
58
58
  }
59
59
  function ask(ticketID, input, caller) {
60
60
  checkFields(input.fields);
61
- requireThat(actors.manager(caller) !== null, "forbidden", "the root doesn't ask");
62
61
  return store.transaction(() => {
63
62
  const work = recorded(table, ticketID);
64
63
  requireThat(work.ownerID === caller, "forbidden", "only the Work's owner asks on it");
@@ -68,7 +67,6 @@ export function createWork(actors, store) {
68
67
  }
69
68
  function answerSubmission(request, caller) {
70
69
  const row = submitted(table, request.askID, request.ticketID);
71
- requireThat(!request.raise, "invalid_input", "a submission can't be raised");
72
70
  const { verdict } = parse(SubmissionAnswer, request).answers;
73
71
  requireThat(verdict === "accepted" || request.note !== undefined, "invalid_input", "requesting changes needs a note");
74
72
  requireThat(requester(row, caller), "forbidden", "only requester answers its submission");
@@ -124,11 +122,11 @@ export function createWork(actors, store) {
124
122
  actors.get();
125
123
  const { after: _after, limit: _limit, ...filter } = query;
126
124
  const waitedOn = (row) => row.waitsOn && "actorID" in row.waitsOn ? row.waitsOn.actorID : undefined;
127
- const page = traverse(actors, table, query, ["work", filter], () => subtrees(actors, table, {
128
- owner: query.ownerID === undefined ? undefined : [query.ownerID],
125
+ const page = traverse(actors, table, query, ["work", filter], () => ({
126
+ owner: query.ownerID === undefined ? undefined : [query.ownerID].flat(),
129
127
  requester: query.requesterID === undefined ? undefined : [query.requesterID].flat(),
130
128
  status: query.status,
131
- }, { owner: query.ownerWithin, requester: query.requesterWithin }), (row) => (query.waitsOnActorID === undefined || waitedOn(row) === query.waitsOnActorID) &&
129
+ }), (row) => (query.waitsOnActorID === undefined || waitedOn(row) === query.waitsOnActorID) &&
132
130
  (query.updatedSince === undefined || row.updatedAt >= query.updatedSince), (row) => [row.requesterID, row.ownerID, row.review?.reviewerID, waitedOn(row)]);
133
131
  const asks = asksOn(forms, page.items.map((item) => item.ticketID), { open: true, history: false });
134
132
  const items = page.items.map((item) => ({ ...item, asks: asks.get(item.ticketID) ?? [] }));
@@ -180,27 +178,23 @@ export function createWork(actors, store) {
180
178
  if (asked === undefined)
181
179
  return answerSubmission(request, caller);
182
180
  requireThat(request.ticketID === undefined || asked.workID === request.ticketID, "not_found", "record not found");
183
- return request.raise
184
- ? raised(actors, forms, asked, caller)
185
- : responded(actors, forms, asked, caller, request.answers ?? {}, request.note);
181
+ return responded(actors, forms, table, asked, caller, request.answers, request.note);
186
182
  });
187
183
  },
188
184
  asks(input = {}) {
189
185
  const query = parse(AskQuery, input);
190
186
  actors.get();
191
187
  const { after: _after, limit: _limit, ...filter } = query;
192
- return traverse(actors, forms, query, ["asks", filter], () => {
193
- const requester = query.requesterID === undefined ? undefined : [query.requesterID];
194
- const routed = query.routedTo;
195
- return {
196
- requester,
197
- // Only an open ask has a route.
198
- status: routed === undefined
199
- ? query.status
200
- : (query.status ?? ["open"]).filter((status) => status === "open"),
201
- route: routed === undefined ? undefined : routedTo(actors, forms, routed, requester),
202
- };
203
- }, (row) => query.answeredBy === undefined || Object.hasOwn(row.responses, query.answeredBy), mentioned);
188
+ const routed = query.routedTo;
189
+ const route = router(actors, table, routedStandIn);
190
+ return traverse(actors, forms, query, ["asks", filter], () => ({
191
+ requester: query.requesterID === undefined ? undefined : [query.requesterID],
192
+ // Only an open ask has a route.
193
+ status: routed === undefined
194
+ ? query.status
195
+ : (query.status ?? ["open"]).filter((status) => status === "open"),
196
+ }), (row) => (query.answeredBy === undefined || Object.hasOwn(row.responses, query.answeredBy)) &&
197
+ (routed === undefined || route(row) === routed), mentioned);
204
198
  },
205
199
  };
206
200
  }