@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.
- package/README.md +3 -3
- package/dist/src/bindings/mcp/encoding.js +1 -4
- package/dist/src/clients/cli/commands.js +1 -1
- package/dist/src/clients/cli/index.js +1 -1
- package/dist/src/clients/mcp/agent.d.ts +3 -5
- package/dist/src/clients/mcp/agent.js +10 -23
- package/dist/src/clients/mcp/command.d.ts +1 -1
- package/dist/src/clients/mcp/command.js +5 -11
- package/dist/src/clients/mcp/projects.d.ts +1 -2
- package/dist/src/clients/mcp/projects.js +5 -9
- package/dist/src/clients/mcp/sessions.d.ts +1 -5
- package/dist/src/clients/mcp/sessions.js +1 -10
- package/dist/src/clients/mcp/setup.js +4 -8
- package/dist/src/clients/mcp/waiter.d.ts +0 -2
- package/dist/src/clients/mcp/waiter.js +9 -20
- package/dist/src/domain/actors.js +3 -7
- package/dist/src/domain/attention.d.ts +8 -7
- package/dist/src/domain/attention.js +31 -47
- package/dist/src/domain/capabilities.js +0 -1
- package/dist/src/domain/claims.js +2 -2
- package/dist/src/domain/errors.d.ts +1 -1
- package/dist/src/domain/errors.js +1 -1
- package/dist/src/domain/index.d.ts +1 -15
- package/dist/src/domain/model/attention.d.ts +1 -5
- package/dist/src/domain/model/attention.js +5 -6
- package/dist/src/domain/model/builtins.d.ts +12 -179
- package/dist/src/domain/model/builtins.js +3 -6
- package/dist/src/domain/model/claims.d.ts +1 -3
- package/dist/src/domain/model/claims.js +1 -3
- package/dist/src/domain/model/index.d.ts +0 -7
- package/dist/src/domain/model/index.js +0 -3
- package/dist/src/domain/model/json-schema.d.ts +1 -4
- package/dist/src/domain/model/records.d.ts +0 -3
- package/dist/src/domain/model/records.js +3 -7
- package/dist/src/domain/model/system.d.ts +1 -5
- package/dist/src/domain/model/system.js +1 -5
- package/dist/src/domain/model/team.d.ts +4 -119
- package/dist/src/domain/model/team.js +8 -19
- package/dist/src/domain/model/workspace.d.ts +2 -86
- package/dist/src/domain/model/workspace.js +0 -1
- package/dist/src/domain/ports.d.ts +3 -4
- package/dist/src/domain/registry.d.ts +2 -16
- package/dist/src/domain/registry.js +22 -54
- package/dist/src/domain/system.js +1 -5
- package/dist/src/domain/team.d.ts +1 -1
- package/dist/src/domain/team.js +13 -33
- package/dist/src/domain/workspace.js +2 -10
- package/dist/src/home.d.ts +5 -5
- package/dist/src/home.js +6 -6
- package/dist/src/server/command.js +2 -2
- package/dist/src/server/index.d.ts +1 -1
- package/dist/src/server/index.js +2 -2
- package/dist/src/storage/sqlite/database.d.ts +5 -0
- package/dist/src/storage/sqlite/database.js +32 -1
- package/dist/src/storage/sqlite/index.js +1 -2
- package/dist/src/storage/sqlite/schema.d.ts +3 -4
- package/dist/src/storage/sqlite/schema.js +4 -4
- package/node_modules/@uniac/agentic-workspace/README.md +5 -5
- package/node_modules/@uniac/agentic-workspace/dist/examples/authority.d.ts +0 -1
- package/node_modules/@uniac/agentic-workspace/dist/examples/authority.js +3 -16
- package/node_modules/@uniac/agentic-workspace/dist/src/actors/index.d.ts +13 -13
- package/node_modules/@uniac/agentic-workspace/dist/src/actors/index.js +9 -25
- package/node_modules/@uniac/agentic-workspace/dist/src/contracts/common.d.ts +1 -9
- package/node_modules/@uniac/agentic-workspace/dist/src/contracts/common.js +2 -4
- package/node_modules/@uniac/agentic-workspace/dist/src/contracts/forms.d.ts +0 -13
- package/node_modules/@uniac/agentic-workspace/dist/src/contracts/forms.js +0 -3
- package/node_modules/@uniac/agentic-workspace/dist/src/contracts/messages.d.ts +0 -3
- package/node_modules/@uniac/agentic-workspace/dist/src/contracts/messages.js +3 -3
- package/node_modules/@uniac/agentic-workspace/dist/src/contracts/work.d.ts +2 -15
- package/node_modules/@uniac/agentic-workspace/dist/src/contracts/work.js +9 -14
- package/node_modules/@uniac/agentic-workspace/dist/src/mcp/index.js +1 -1
- package/node_modules/@uniac/agentic-workspace/dist/src/persistence/index.d.ts +1 -4
- package/node_modules/@uniac/agentic-workspace/dist/src/persistence/sqlite.js +2 -15
- package/node_modules/@uniac/agentic-workspace/dist/src/records.d.ts +0 -7
- package/node_modules/@uniac/agentic-workspace/dist/src/records.js +0 -14
- package/node_modules/@uniac/agentic-workspace/dist/src/work/asks.d.ts +20 -19
- package/node_modules/@uniac/agentic-workspace/dist/src/work/asks.js +39 -41
- package/node_modules/@uniac/agentic-workspace/dist/src/work/index.js +18 -24
- package/node_modules/@uniac/agentic-workspace/docs/contracts.md +23 -29
- package/node_modules/@uniac/agentic-workspace/docs/design.md +13 -13
- package/node_modules/@uniac/agentic-workspace/docs/hub-integration.md +25 -50
- package/node_modules/@uniac/agentic-workspace/package.json +1 -1
- package/package.json +3 -3
- package/skills/agentorg/SKILL.md +39 -38
- package/skills/agentorg/references/claude-code.md +3 -4
- package/vendor/uniac-agentic-workspace-0.0.2-8aebc44.tgz +0 -0
- package/vendor/workspace.json +6 -6
- package/dist/src/clients/mcp/levels.d.ts +0 -17
- package/dist/src/clients/mcp/levels.js +0 -7
- 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.
|
|
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:
|
|
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("
|
|
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("
|
|
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("
|
|
92
|
-
work_update: contract("
|
|
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("
|
|
121
|
-
work_answer: contract("
|
|
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))
|
|
122
|
+
answers: z.record(z.string(), z.string().max(20000)),
|
|
126
123
|
note: Text.optional(),
|
|
127
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
49
|
-
//
|
|
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"
|
|
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
|
|
7
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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,
|
|
3
|
-
*
|
|
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 {
|
|
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
|
|
11
|
-
*
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
70
|
-
|
|
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
|
|
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
|
-
*
|
|
99
|
-
*
|
|
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
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
.
|
|
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
|
|
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,
|
|
6
|
-
import { asksAsRead, asksOn, cancelOpen, checkFields, closed, created, mentioned,
|
|
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
|
|
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], () =>
|
|
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
|
-
}
|
|
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.
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
status
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
}
|