@kubitnodes/stellaris 0.3.10 → 0.3.11
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/dist/cli/index.js +533 -117
- package/dist/server/index.js +900 -197
- package/package.json +1 -1
- package/web/dist/assets/BackLink-C-vad6K2.js +1 -0
- package/web/dist/assets/ChannelForms-jNbbNnIC.js +1 -0
- package/web/dist/assets/ChannelView-kLoegYUQ.js +1 -0
- package/web/dist/assets/CitizenView-Cb8f2mB7.js +2 -0
- package/web/dist/assets/{CitizensView-DttP5FqR.js → CitizensView-Ds2rojfa.js} +1 -1
- package/web/dist/assets/KnowledgeView-k10TU0jc.js +1 -0
- package/web/dist/assets/{Markdown-4W4xjIq7.js → Markdown-J2pAPfCa.js} +3 -3
- package/web/dist/assets/{MessageItem-Bfpve1_Y.js → MessageItem-BjM8BdpK.js} +1 -1
- package/web/dist/assets/{MetricsView-Bp486U0v.js → MetricsView-D9N54rV7.js} +1 -1
- package/web/dist/assets/{NeedsYouView-CJ4COcNn.js → NeedsYouView-ML8i7Pl7.js} +1 -1
- package/web/dist/assets/{Overview-BnO1K52h.js → Overview-BBXvRhfR.js} +1 -1
- package/web/dist/assets/{Pane-xzIpwnBD.js → Pane-caYWCiyo.js} +1 -1
- package/web/dist/assets/ProjectView-sDpFNjcW.js +1 -0
- package/web/dist/assets/ProposalView-Bc91NjKC.js +1 -0
- package/web/dist/assets/{ProposalsView-XrraDhwt.js → ProposalsView-Dg_bEryX.js} +1 -1
- package/web/dist/assets/{RunnersView-H3kCdSQB.js → RunnersView-Dv5nbwYv.js} +1 -1
- package/web/dist/assets/SocietyView-ClaEWgOq.js +1 -0
- package/web/dist/assets/SubjectThread-D_L3vB-A.js +1 -0
- package/web/dist/assets/{TaskFileView-CRwwL5K8.js → TaskFileView-B19YtrtW.js} +2 -2
- package/web/dist/assets/{TaskView-A9IgTxQ0.js → TaskView-FYSoCb3T.js} +1 -1
- package/web/dist/assets/{TasksView-CnhTIldG.js → TasksView-CniXTU3R.js} +1 -1
- package/web/dist/assets/{ThreadCard-fvf8cY_l.js → ThreadCard-DCpvhSlc.js} +1 -1
- package/web/dist/assets/{ThreadForms-D09Ykgj9.js → ThreadForms-DUGkAwTp.js} +1 -1
- package/web/dist/assets/ThreadView-D8eXlVbC.js +1 -0
- package/web/dist/assets/citizen-BtOZq62_.js +1 -0
- package/web/dist/assets/{format-lz-5NQWT.js → format-EfoceZkm.js} +1 -1
- package/web/dist/assets/index-BeOGtAz0.css +2 -0
- package/web/dist/assets/index-D75IxLme.js +23 -0
- package/web/dist/assets/{tasks-DK1cnXqs.js → tasks-Duke4U7N.js} +1 -1
- package/web/dist/assets/{useStickyScroll-CfP3Yl_4.js → useStickyScroll-n5d7sdPZ.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/BackLink-B3tz8OFm.js +0 -1
- package/web/dist/assets/ChannelView-Dkzr1zkF.js +0 -1
- package/web/dist/assets/CitizenView-BLgyEvYz.js +0 -2
- package/web/dist/assets/KnowledgeView-DFAC95aF.js +0 -1
- package/web/dist/assets/ProjectView-w0mcF0KW.js +0 -1
- package/web/dist/assets/ProposalView-CyJCwUYK.js +0 -1
- package/web/dist/assets/SocietyView-Cf8ENDAw.js +0 -1
- package/web/dist/assets/SubjectThread-ClMmgVVd.js +0 -1
- package/web/dist/assets/ThreadView-BqNSssCf.js +0 -1
- package/web/dist/assets/citizen-C27Wvimj.js +0 -1
- package/web/dist/assets/index-CoKyPk79.js +0 -23
- package/web/dist/assets/index-t2fXHB5Y.css +0 -2
package/dist/cli/index.js
CHANGED
|
@@ -57,7 +57,9 @@ var VERB_NAMES = [
|
|
|
57
57
|
"advance_task",
|
|
58
58
|
"configure_project",
|
|
59
59
|
"archive_project",
|
|
60
|
-
"remove_knowledge"
|
|
60
|
+
"remove_knowledge",
|
|
61
|
+
"create_channel",
|
|
62
|
+
"archive_channel"
|
|
61
63
|
];
|
|
62
64
|
var VerbNameSchema = z2.enum(VERB_NAMES);
|
|
63
65
|
var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
|
|
@@ -107,7 +109,11 @@ var MEMBER_VERBS = [
|
|
|
107
109
|
];
|
|
108
110
|
var GOVERNANCE_VERBS = ["approve", "reject"];
|
|
109
111
|
var FRONT_DESK_VERBS = ["create_project"];
|
|
110
|
-
var PLANNING_VERBS = [
|
|
112
|
+
var PLANNING_VERBS = [
|
|
113
|
+
"configure_project",
|
|
114
|
+
"create_channel",
|
|
115
|
+
"archive_channel"
|
|
116
|
+
];
|
|
111
117
|
var ARCHIVE_VERBS = ["archive_project"];
|
|
112
118
|
var SEED_ROLES = [
|
|
113
119
|
{
|
|
@@ -129,7 +135,7 @@ var SEED_ROLES = [
|
|
|
129
135
|
},
|
|
130
136
|
{
|
|
131
137
|
name: "steward",
|
|
132
|
-
purpose: "Watches the operations signals and the task board for capacity, skill, and capability gaps, and turns signals into proposals: a member when a project's plans need a role nobody fills or a backlog persists, a role when a project's work needs one no existing role covers, a retirement when a member has been idle for a long time
|
|
138
|
+
purpose: "Watches the operations signals and the task board for capacity, skill, and capability gaps, and turns signals into proposals: a member when a project's plans need a role nobody fills or a backlog persists, a role when a project's work needs one no existing role covers, a retirement when a member has been idle for a long time. Opens a channel with create_channel when a topic or a workstream needs one, and archives it once its work is done. Owns how work is shaped over time: answers stages waiting too long, stages assigned to roles nobody fills, and work sent back again and again by replanning with plan_task, adjusting gates, or proposing the role a plan needs; sets each project's completion effect with configure_project; turns recurring plan shapes into society skills and planning norms into the society's norms. Prefers scaling an existing role over inventing a new one. Never approves its own proposals; hiring, roles, and retirements are decided by the user, and channels, reallocations, and skills the steward may decide. Curates society knowledge and skills.",
|
|
133
139
|
verbs: [...MEMBER_VERBS, ...GOVERNANCE_VERBS, ...PLANNING_VERBS],
|
|
134
140
|
wakeTriggers: ["ops_event", "heartbeat"],
|
|
135
141
|
maxReplicas: 1,
|
|
@@ -140,7 +146,7 @@ var SEED_ROLES = [
|
|
|
140
146
|
},
|
|
141
147
|
{
|
|
142
148
|
name: "concierge",
|
|
143
|
-
purpose: "The society's front desk. Wakes on every post the user makes and routes it: a question gets an answer where it was asked, in its thread when it came in one; work in an area an existing project covers gets a task there, planned when it is created, since a project is a lasting area of work and not one request; work in a new area gets a project, created on the spot, and the member or role proposals it needs for the user to approve. Reorganizes projects when the user asks: creates or picks the project the work belongs in, adds the citizens it needs, files a task there to bring over what the old projects hold (their task branches and their knowledge), and once that task is done proposes archiving each old project for the user to approve. Plans every task it routes: names stages by the work, assigns each to a role or a citizen, and gates where a second pair of eyes is worth it: before anything lands in a shared deliverable, so always before a merge; before effects outside the society; and before results are presented as findings. When a task it created is done, tells the user. Reads the roster in every digest to choose citizens by role, reach, availability, and profile, and adds a citizen to a project when the work needs it. Mentions a citizen with @ only to hand it work outside a plan, since a mention wakes it and costs a turn; lists and describes citizens by plain name. Stays silent when the user already addressed a citizen and nothing else is needed. Never does the work itself and never decides hiring.",
|
|
149
|
+
purpose: "The society's front desk. Wakes on every post the user makes and routes it: a question gets an answer where it was asked, in its thread when it came in one; work in an area an existing project covers gets a task there, planned when it is created and filed in the channel of its workstream when it belongs to one, such as a release, which the concierge opens with create_channel and archives once the workstream is done, since a project is a lasting area of work and not one request; work in a new area gets a project, created on the spot, and the member or role proposals it needs for the user to approve. Reorganizes projects when the user asks: creates or picks the project the work belongs in, adds the citizens it needs, files a task there to bring over what the old projects hold (their task branches and their knowledge), and once that task is done proposes archiving each old project for the user to approve. Plans every task it routes: names stages by the work, assigns each to a role or a citizen, and gates where a second pair of eyes is worth it: before anything lands in a shared deliverable, so always before a merge; before effects outside the society; and before results are presented as findings. When a task it created is done, tells the user. Reads the roster in every digest to choose citizens by role, reach, availability, and profile, and adds a citizen to a project when the work needs it. Mentions a citizen with @ only to hand it work outside a plan, since a mention wakes it and costs a turn; lists and describes citizens by plain name. Stays silent when the user already addressed a citizen and nothing else is needed. Never does the work itself and never decides hiring.",
|
|
144
150
|
verbs: [...MEMBER_VERBS, ...FRONT_DESK_VERBS, ...PLANNING_VERBS],
|
|
145
151
|
wakeTriggers: ["user_post", "heartbeat"],
|
|
146
152
|
maxReplicas: 1,
|
|
@@ -156,16 +162,28 @@ import { z as z3 } from "zod";
|
|
|
156
162
|
var ASK_CHANNEL = "asks";
|
|
157
163
|
var SOCIETY_CHANNELS = ["general", "governance", ASK_CHANNEL];
|
|
158
164
|
var PROJECT_DEFAULT_CHANNELS = ["general"];
|
|
165
|
+
function isBoardChannel(project2, name) {
|
|
166
|
+
const used = project2 === null ? SOCIETY_CHANNELS : PROJECT_DEFAULT_CHANNELS;
|
|
167
|
+
return used.includes(name);
|
|
168
|
+
}
|
|
159
169
|
var SOCIETY_SCOPE = "society";
|
|
160
170
|
function wakeScope(member, project2) {
|
|
161
171
|
return project2 !== null && member.memberships.includes(project2) ? project2 : SOCIETY_SCOPE;
|
|
162
172
|
}
|
|
163
173
|
var CliKindSchema = z3.enum(["claude", "codex"]);
|
|
174
|
+
var ArchivedChannelSchema = z3.object({
|
|
175
|
+
name: NameSchema,
|
|
176
|
+
at: IsoDateTimeSchema,
|
|
177
|
+
by: NameSchema,
|
|
178
|
+
reason: z3.string().min(1)
|
|
179
|
+
});
|
|
164
180
|
var SocietySchema = z3.object({
|
|
165
181
|
name: z3.string().min(1),
|
|
166
182
|
version: z3.literal(1),
|
|
167
183
|
createdAt: IsoDateTimeSchema,
|
|
168
|
-
channels
|
|
184
|
+
/** The channels that take posts; an archived one moves to `archivedChannels`. */
|
|
185
|
+
channels: z3.array(NameSchema),
|
|
186
|
+
archivedChannels: z3.array(ArchivedChannelSchema).default([])
|
|
169
187
|
});
|
|
170
188
|
var CompletionEffectSchema = z3.enum(["none", "merge"]);
|
|
171
189
|
var DefaultBranchSchema = z3.string().trim().min(1).max(100).regex(/^[A-Za-z0-9][A-Za-z0-9._/-]*$/, "a branch name is letters, digits, and . _ / -").refine((branch) => !branch.includes("..") && !branch.includes("//") && !branch.endsWith("/") && !branch.endsWith(".") && !branch.endsWith(".lock"), "not a branch name git takes").refine((branch) => !branch.startsWith("task/") && !branch.startsWith("agent/"), "task/ and agent/ hold the board's own branches");
|
|
@@ -189,7 +207,9 @@ var ProjectSchema = z3.object({
|
|
|
189
207
|
name: z3.string().min(1),
|
|
190
208
|
repo: z3.string().nullable(),
|
|
191
209
|
defaultBranch: z3.string().min(1),
|
|
210
|
+
/** The channels that take posts; an archived one moves to `archivedChannels`. */
|
|
192
211
|
channels: z3.array(NameSchema),
|
|
212
|
+
archivedChannels: z3.array(ArchivedChannelSchema).default([]),
|
|
193
213
|
members: z3.array(NameSchema),
|
|
194
214
|
approvers: z3.array(NameSchema),
|
|
195
215
|
requiredCapabilities: z3.array(z3.string()),
|
|
@@ -260,6 +280,8 @@ var TaskReturnSchema = z3.object({
|
|
|
260
280
|
var TaskFrontmatterSchema = z3.object({
|
|
261
281
|
id: UlidSchema,
|
|
262
282
|
project: NameSchema,
|
|
283
|
+
/** The channel of the project the task is filed in, where its thread hangs. */
|
|
284
|
+
channel: NameSchema.default("general"),
|
|
263
285
|
title: z3.string().min(1),
|
|
264
286
|
status: TaskStatusSchema,
|
|
265
287
|
createdBy: NameSchema,
|
|
@@ -606,7 +628,9 @@ var VerbInputs = {
|
|
|
606
628
|
parent_id: UlidSchema.optional(),
|
|
607
629
|
required_capabilities: z4.array(z4.string()).default([]),
|
|
608
630
|
/** The plan; without it the task gets one stage, work, that anyone in the project may take. */
|
|
609
|
-
stages: z4.array(PlanStageSchema).min(1).optional()
|
|
631
|
+
stages: z4.array(PlanStageSchema).min(1).optional(),
|
|
632
|
+
/** A channel of the project the task belongs to, such as a release's; its thread opens there. */
|
|
633
|
+
channel: NameSchema.default("general")
|
|
610
634
|
}),
|
|
611
635
|
claim_task: z4.object({ task_id: UlidSchema }),
|
|
612
636
|
release_task: z4.object({ task_id: UlidSchema }),
|
|
@@ -664,7 +688,14 @@ var VerbInputs = {
|
|
|
664
688
|
/** The branch tasks start from and land on from now; tasks in play keep their branches. */
|
|
665
689
|
default_branch: DefaultBranchSchema.optional()
|
|
666
690
|
}),
|
|
667
|
-
archive_project: z4.object({ project: NameSchema, reason: z4.string().min(1) })
|
|
691
|
+
archive_project: z4.object({ project: NameSchema, reason: z4.string().min(1) }),
|
|
692
|
+
create_channel: z4.object({
|
|
693
|
+
/** A project, or null for a channel of the society. */
|
|
694
|
+
project: NameSchema.nullable().default(null),
|
|
695
|
+
name: NameSchema,
|
|
696
|
+
purpose: z4.string().min(1)
|
|
697
|
+
}),
|
|
698
|
+
archive_channel: z4.object({ channel: ChannelRefSchema, reason: z4.string().min(1) })
|
|
668
699
|
};
|
|
669
700
|
|
|
670
701
|
// packages/shared/dist/events.js
|
|
@@ -697,6 +728,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
697
728
|
"proposal.decided",
|
|
698
729
|
"proposal.provisioned",
|
|
699
730
|
"channel.added",
|
|
731
|
+
"channel.archived",
|
|
700
732
|
"role.added",
|
|
701
733
|
"agent.retired",
|
|
702
734
|
"agent.joined",
|
|
@@ -718,7 +750,8 @@ var BOARD_EVENT_TYPES = [
|
|
|
718
750
|
"turn.completed",
|
|
719
751
|
"turn.failed",
|
|
720
752
|
"merge.completed",
|
|
721
|
-
"merge.failed"
|
|
753
|
+
"merge.failed",
|
|
754
|
+
"workspace.leftovers"
|
|
722
755
|
];
|
|
723
756
|
var BoardEventTypeSchema = z5.enum(BOARD_EVENT_TYPES);
|
|
724
757
|
var BoardEventSchema = z5.object({
|
|
@@ -798,6 +831,8 @@ var LiveTurnEventSchema = z5.object({
|
|
|
798
831
|
project: NameSchema,
|
|
799
832
|
/** The thread whose conversation the turn is in; absent for the home conversation. */
|
|
800
833
|
thread: UlidSchema.optional(),
|
|
834
|
+
/** The channel, never general, whose conversation the turn is in. */
|
|
835
|
+
channel: NameSchema.optional(),
|
|
801
836
|
event: AgentEventSchema
|
|
802
837
|
});
|
|
803
838
|
|
|
@@ -866,12 +901,17 @@ var ProjectRepoSchema = z6.object({
|
|
|
866
901
|
defaultBranch: z6.string().min(1)
|
|
867
902
|
});
|
|
868
903
|
var TurnWorkspaceSchema = z6.discriminatedUnion("kind", [
|
|
869
|
-
z6.object({
|
|
904
|
+
z6.object({
|
|
905
|
+
kind: z6.literal("home"),
|
|
906
|
+
thread: UlidSchema.optional(),
|
|
907
|
+
channel: NameSchema.optional()
|
|
908
|
+
}),
|
|
870
909
|
z6.object({
|
|
871
910
|
kind: z6.literal("project"),
|
|
872
911
|
repo: ProjectRepoSchema,
|
|
873
912
|
branches: z6.array(z6.string().min(1)),
|
|
874
|
-
thread: UlidSchema.optional()
|
|
913
|
+
thread: UlidSchema.optional(),
|
|
914
|
+
channel: NameSchema.optional()
|
|
875
915
|
}),
|
|
876
916
|
z6.object({ kind: z6.literal("task"), repo: ProjectRepoSchema, taskId: UlidSchema })
|
|
877
917
|
]);
|
|
@@ -887,6 +927,8 @@ var TurnJobSchema = z6.object({
|
|
|
887
927
|
scope: NameSchema,
|
|
888
928
|
/** The thread whose conversation the turn is in; absent for the home conversation. */
|
|
889
929
|
thread: UlidSchema.optional(),
|
|
930
|
+
/** The channel, never general, whose conversation the turn is in. */
|
|
931
|
+
channel: NameSchema.optional(),
|
|
890
932
|
/**
|
|
891
933
|
* The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
|
|
892
934
|
* the outcome reports it.
|
|
@@ -920,7 +962,12 @@ var TurnOutcomeSchema = z6.object({
|
|
|
920
962
|
session: z6.string().min(1),
|
|
921
963
|
/** The model the CLI reported, when it reported one. */
|
|
922
964
|
model: z6.string().nullable(),
|
|
923
|
-
work: TurnWorkSchema.nullable()
|
|
965
|
+
work: TurnWorkSchema.nullable(),
|
|
966
|
+
/**
|
|
967
|
+
* Whether the turn's own workspace, a thread's, a channel's, or a task's, holds work on no
|
|
968
|
+
* branch after the hand-back: uncommitted changes, or commits no branch contains.
|
|
969
|
+
*/
|
|
970
|
+
leftovers: z6.boolean().default(false)
|
|
924
971
|
});
|
|
925
972
|
var TurnAckSchema = z6.object({ dropWorkspace: z6.boolean() });
|
|
926
973
|
var LandRequestSchema = z6.object({
|
|
@@ -979,6 +1026,17 @@ var BranchChangesSchema = z6.object({
|
|
|
979
1026
|
files: z6.array(BranchFileChangeSchema),
|
|
980
1027
|
total: z6.number().int().nonnegative()
|
|
981
1028
|
});
|
|
1029
|
+
var WorkspaceConversationSchema = z6.discriminatedUnion("kind", [
|
|
1030
|
+
z6.object({ kind: z6.literal("thread"), id: UlidSchema }),
|
|
1031
|
+
z6.object({ kind: z6.literal("channel"), scope: NameSchema, name: NameSchema })
|
|
1032
|
+
]);
|
|
1033
|
+
var HeldWorkspaceSchema = z6.object({
|
|
1034
|
+
agent: NameSchema,
|
|
1035
|
+
scope: NameSchema,
|
|
1036
|
+
conversation: WorkspaceConversationSchema
|
|
1037
|
+
});
|
|
1038
|
+
var HeldWorkspacesSchema = z6.object({ workspaces: z6.array(HeldWorkspaceSchema) });
|
|
1039
|
+
var SweepResultSchema = z6.object({ kept: z6.array(HeldWorkspaceSchema) });
|
|
982
1040
|
var TurnSteerSchema = z6.object({ id: z6.uuid(), text: z6.string().min(1) });
|
|
983
1041
|
var RunnerMessageSchema = z6.discriminatedUnion("type", [
|
|
984
1042
|
z6.object({ type: z6.literal("turn"), job: TurnJobSchema }),
|
|
@@ -1000,7 +1058,16 @@ var RunnerMessageSchema = z6.discriminatedUnion("type", [
|
|
|
1000
1058
|
steer: TurnSteerSchema
|
|
1001
1059
|
}),
|
|
1002
1060
|
/** Answered with whether the turn was there to stop; it ends `stopped`. */
|
|
1003
|
-
z6.object({ type: z6.literal("stop"), request: z6.string().min(1), turnId: UlidSchema })
|
|
1061
|
+
z6.object({ type: z6.literal("stop"), request: z6.string().min(1), turnId: UlidSchema }),
|
|
1062
|
+
/**
|
|
1063
|
+
* A conversation ended: the runner removes every citizen's workspace for it but those a turn
|
|
1064
|
+
* still uses, and answers with a `SweepResult` naming those it kept for holding work on no branch.
|
|
1065
|
+
*/
|
|
1066
|
+
z6.object({
|
|
1067
|
+
type: z6.literal("sweep"),
|
|
1068
|
+
request: z6.string().min(1),
|
|
1069
|
+
conversation: WorkspaceConversationSchema
|
|
1070
|
+
})
|
|
1004
1071
|
]);
|
|
1005
1072
|
var RunnerAnswerSchema = z6.discriminatedUnion("ok", [
|
|
1006
1073
|
z6.object({ ok: z6.literal(true), value: z6.unknown() }),
|
|
@@ -1028,7 +1095,8 @@ var TriggerKindSchema = z7.enum([
|
|
|
1028
1095
|
"onboarding",
|
|
1029
1096
|
"manual",
|
|
1030
1097
|
"ops_event",
|
|
1031
|
-
"user_post"
|
|
1098
|
+
"user_post",
|
|
1099
|
+
"closing"
|
|
1032
1100
|
]);
|
|
1033
1101
|
var TriggerSchema = z7.object({
|
|
1034
1102
|
kind: TriggerKindSchema,
|
|
@@ -1045,10 +1113,20 @@ var TurnDispatchSchema = z7.object({
|
|
|
1045
1113
|
agent: NameSchema,
|
|
1046
1114
|
project: NameSchema,
|
|
1047
1115
|
thread: TurnThreadSchema.optional(),
|
|
1116
|
+
channel: NameSchema.optional(),
|
|
1048
1117
|
trigger: TriggerSchema,
|
|
1049
1118
|
priority: z7.number().int().min(0).max(2),
|
|
1050
1119
|
onboarding: z7.boolean().default(false)
|
|
1051
1120
|
});
|
|
1121
|
+
function conversationPart(conversation) {
|
|
1122
|
+
return conversation.thread ?? (conversation.channel === void 0 ? void 0 : `#${conversation.channel}`);
|
|
1123
|
+
}
|
|
1124
|
+
function channelConversation(scope, channel) {
|
|
1125
|
+
const at = channel.indexOf("/");
|
|
1126
|
+
const place = at < 0 ? SOCIETY_SCOPE : channel.slice(0, at);
|
|
1127
|
+
const name = at < 0 ? channel : channel.slice(at + 1);
|
|
1128
|
+
return place === scope && name !== "general" ? name : void 0;
|
|
1129
|
+
}
|
|
1052
1130
|
var TurnRecordSchema = z7.object({
|
|
1053
1131
|
/** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
|
|
1054
1132
|
id: UlidSchema.optional(),
|
|
@@ -1056,6 +1134,8 @@ var TurnRecordSchema = z7.object({
|
|
|
1056
1134
|
project: NameSchema,
|
|
1057
1135
|
/** The thread whose conversation the turn was in; absent for the home conversation. */
|
|
1058
1136
|
thread: UlidSchema.optional(),
|
|
1137
|
+
/** The channel, never general, whose conversation the turn was in. */
|
|
1138
|
+
channel: NameSchema.optional(),
|
|
1059
1139
|
runner: NameSchema,
|
|
1060
1140
|
cli: CliKindSchema.nullable(),
|
|
1061
1141
|
session: z7.string().nullable(),
|
|
@@ -1085,8 +1165,9 @@ var RunningTurnSchema = z7.object({
|
|
|
1085
1165
|
agent: NameSchema,
|
|
1086
1166
|
scope: NameSchema,
|
|
1087
1167
|
thread: UlidSchema.optional(),
|
|
1168
|
+
channel: NameSchema.optional(),
|
|
1088
1169
|
cli: CliKindSchema,
|
|
1089
|
-
|
|
1170
|
+
askedIn: ChannelRefSchema.optional(),
|
|
1090
1171
|
steerable: z7.boolean(),
|
|
1091
1172
|
stoppable: z7.boolean()
|
|
1092
1173
|
});
|
|
@@ -1113,6 +1194,8 @@ var TurnHistoryEntrySchema = z7.object({
|
|
|
1113
1194
|
project: NameSchema,
|
|
1114
1195
|
/** The thread whose conversation the turn was in; absent for the home conversation. */
|
|
1115
1196
|
thread: UlidSchema.optional(),
|
|
1197
|
+
/** The channel, never general, whose conversation the turn was in. */
|
|
1198
|
+
channel: NameSchema.optional(),
|
|
1116
1199
|
trigger: z7.string(),
|
|
1117
1200
|
exitReason: z7.string().nullable(),
|
|
1118
1201
|
costUsd: z7.number(),
|
|
@@ -1139,7 +1222,8 @@ var OPS_SIGNAL_KINDS = [
|
|
|
1139
1222
|
"turn_cost",
|
|
1140
1223
|
"scaled",
|
|
1141
1224
|
"runner",
|
|
1142
|
-
"home_conflict"
|
|
1225
|
+
"home_conflict",
|
|
1226
|
+
"stuck_workspace"
|
|
1143
1227
|
];
|
|
1144
1228
|
var OpsSignalKindSchema = z8.enum(OPS_SIGNAL_KINDS);
|
|
1145
1229
|
var OpsSignalSchema = z8.object({
|
|
@@ -1301,7 +1385,7 @@ var RunnerConfigSchema = z10.object({
|
|
|
1301
1385
|
});
|
|
1302
1386
|
|
|
1303
1387
|
// packages/shared/dist/version.js
|
|
1304
|
-
var STELLARIS_VERSION = "0.3.
|
|
1388
|
+
var STELLARIS_VERSION = "0.3.11";
|
|
1305
1389
|
|
|
1306
1390
|
// packages/board-core/dist/errors.js
|
|
1307
1391
|
var BoardError = class extends Error {
|
|
@@ -1572,6 +1656,7 @@ var HOME_GITIGNORE = [
|
|
|
1572
1656
|
"/projects/*/sessions*.json",
|
|
1573
1657
|
"/projects/*/last-turn.json",
|
|
1574
1658
|
"/projects/*/threads/",
|
|
1659
|
+
"/projects/*/channels/",
|
|
1575
1660
|
`/${HOME_SCRATCH}/`,
|
|
1576
1661
|
"*.tmp",
|
|
1577
1662
|
"node_modules/",
|
|
@@ -1589,7 +1674,7 @@ var HOME_GITIGNORE = [
|
|
|
1589
1674
|
var IGNORE_FILE = `${HOME_GITIGNORE.join("\n")}
|
|
1590
1675
|
`;
|
|
1591
1676
|
var PRE_RECEIVE = `#!/bin/sh
|
|
1592
|
-
forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/))'
|
|
1677
|
+
forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/|channels/))'
|
|
1593
1678
|
while read old new ref; do
|
|
1594
1679
|
if [ "$ref" != "refs/heads/main" ]; then
|
|
1595
1680
|
echo "a home takes pushes to main only" >&2
|
|
@@ -1850,6 +1935,7 @@ var TurnPayload = z11.object({
|
|
|
1850
1935
|
turnId: z11.string().optional(),
|
|
1851
1936
|
project: z11.string(),
|
|
1852
1937
|
thread: z11.string().optional(),
|
|
1938
|
+
channel: z11.string().optional(),
|
|
1853
1939
|
trigger: z11.string()
|
|
1854
1940
|
});
|
|
1855
1941
|
var TaskPayload = z11.object({
|
|
@@ -1892,7 +1978,14 @@ function answers(mention, turn2) {
|
|
|
1892
1978
|
if (turn2.thread !== null) {
|
|
1893
1979
|
return mention.thread === turn2.thread;
|
|
1894
1980
|
}
|
|
1895
|
-
|
|
1981
|
+
if (!mention.scopes.includes(turn2.project)) {
|
|
1982
|
+
return false;
|
|
1983
|
+
}
|
|
1984
|
+
const own = mention.thread === null ? channelConversation(turn2.project, mention.channel) : void 0;
|
|
1985
|
+
if (turn2.channel !== null) {
|
|
1986
|
+
return mention.thread === null && own === turn2.channel;
|
|
1987
|
+
}
|
|
1988
|
+
return mention.thread === null && own === void 0 || turn2.trigger === "mention";
|
|
1896
1989
|
}
|
|
1897
1990
|
function computeMetrics(input) {
|
|
1898
1991
|
const { events, now, window } = input;
|
|
@@ -1957,6 +2050,7 @@ function computeMetrics(input) {
|
|
|
1957
2050
|
agent: event.actor,
|
|
1958
2051
|
at,
|
|
1959
2052
|
thread: turn2.data.thread ?? null,
|
|
2053
|
+
channel: turn2.data.channel ?? null,
|
|
1960
2054
|
project: turn2.data.project,
|
|
1961
2055
|
trigger: turn2.data.trigger
|
|
1962
2056
|
};
|
|
@@ -2053,6 +2147,7 @@ function computeMetrics(input) {
|
|
|
2053
2147
|
at: Date.parse(event.ts),
|
|
2054
2148
|
message: message.data.id ?? null,
|
|
2055
2149
|
thread: thread2,
|
|
2150
|
+
channel: message.data.channel,
|
|
2056
2151
|
scopes: [project2, wakeScope(citizen, project2)].filter((scope) => scope !== null)
|
|
2057
2152
|
});
|
|
2058
2153
|
}
|
|
@@ -2328,9 +2423,12 @@ var BoardPaths = class {
|
|
|
2328
2423
|
agentProject(name, slug) {
|
|
2329
2424
|
return path5.join(this.agentProjects(name), slug);
|
|
2330
2425
|
}
|
|
2331
|
-
/**
|
|
2332
|
-
|
|
2333
|
-
|
|
2426
|
+
/**
|
|
2427
|
+
* A thread's or a channel's conversation's session and last turn, beside the scope's home
|
|
2428
|
+
* conversation; `part` is a thread's id or `#channel`.
|
|
2429
|
+
*/
|
|
2430
|
+
agentConversation(name, slug, part) {
|
|
2431
|
+
return part.startsWith("#") ? path5.join(this.agentProject(name, slug), "channels", part.slice(1)) : path5.join(this.agentProject(name, slug), "threads", part);
|
|
2334
2432
|
}
|
|
2335
2433
|
agentCursors(name) {
|
|
2336
2434
|
return path5.join(this.agent(name), "cursors.json");
|
|
@@ -2364,6 +2462,13 @@ function hashToken(token) {
|
|
|
2364
2462
|
|
|
2365
2463
|
// packages/board-core/dist/board.js
|
|
2366
2464
|
var SYSTEM_ACTOR = { name: "board", role: USER_ROLE };
|
|
2465
|
+
var CLOSING_ATTEMPTS = 3;
|
|
2466
|
+
function ownTurn(actor, turn2) {
|
|
2467
|
+
return turn2.agent === actor.name && turn2.scope === actor.scope && turn2.thread === actor.thread && turn2.channel === actor.channel;
|
|
2468
|
+
}
|
|
2469
|
+
function namesList(names) {
|
|
2470
|
+
return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
|
|
2471
|
+
}
|
|
2367
2472
|
var SKIPPED_IN_HOME = /* @__PURE__ */ new Set([".git", "turns", ".claude", ".codex"]);
|
|
2368
2473
|
var RunnerTokensSchema = z12.object({
|
|
2369
2474
|
runners: z12.record(z12.string(), z12.object({ tokenHash: z12.string().min(1) }))
|
|
@@ -2381,6 +2486,7 @@ var SignInsSchema = z12.object({
|
|
|
2381
2486
|
var TurnHistoryPayloadSchema = z12.object({
|
|
2382
2487
|
project: NameSchema,
|
|
2383
2488
|
thread: UlidSchema.optional(),
|
|
2489
|
+
channel: NameSchema.optional(),
|
|
2384
2490
|
trigger: z12.string().default("manual"),
|
|
2385
2491
|
exitReason: z12.string().nullable().default(null),
|
|
2386
2492
|
costUsd: z12.number().default(0),
|
|
@@ -2396,12 +2502,23 @@ var CursorsSchema = z12.object({
|
|
|
2396
2502
|
digest: z12.string().nullable(),
|
|
2397
2503
|
scopes: z12.record(z12.string(), z12.string()).default({}),
|
|
2398
2504
|
threadsFrom: z12.record(z12.string(), z12.string().nullable()).default({}),
|
|
2505
|
+
channelsFrom: z12.record(z12.string(), z12.string().nullable()).default({}),
|
|
2399
2506
|
/** How far a work role has read news, which goes to whichever of its general conversations reads first. */
|
|
2400
2507
|
news: z12.string().optional()
|
|
2401
2508
|
});
|
|
2402
|
-
var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {} };
|
|
2403
|
-
function conversationKey(scope,
|
|
2404
|
-
return
|
|
2509
|
+
var NO_CURSORS = { digest: null, scopes: {}, threadsFrom: {}, channelsFrom: {} };
|
|
2510
|
+
function conversationKey(scope, part) {
|
|
2511
|
+
return part === null ? scope : `${scope}/${part}`;
|
|
2512
|
+
}
|
|
2513
|
+
function channelOfPart(part) {
|
|
2514
|
+
return part?.startsWith("#") === true ? part.slice(1) : void 0;
|
|
2515
|
+
}
|
|
2516
|
+
function threadOfPart(part) {
|
|
2517
|
+
return part === null || part === void 0 || part.startsWith("#") ? void 0 : part;
|
|
2518
|
+
}
|
|
2519
|
+
function channelStart(cursors, scope) {
|
|
2520
|
+
const from = cursors.channelsFrom[scope];
|
|
2521
|
+
return from === void 0 ? cursors.digest : from;
|
|
2405
2522
|
}
|
|
2406
2523
|
function threadStart(cursors, scope) {
|
|
2407
2524
|
const from = cursors.threadsFrom[scope];
|
|
@@ -2414,11 +2531,11 @@ function newsStart(cursors) {
|
|
|
2414
2531
|
const homes = Object.entries(cursors.scopes).filter(([key]) => !key.includes("/")).map(([, id]) => id).toSorted();
|
|
2415
2532
|
return homes.at(-1) ?? cursors.digest;
|
|
2416
2533
|
}
|
|
2417
|
-
function cursorOf(cursors, scope,
|
|
2418
|
-
if (
|
|
2534
|
+
function cursorOf(cursors, scope, part = null) {
|
|
2535
|
+
if (part === null) {
|
|
2419
2536
|
return cursors.scopes[scope] ?? cursors.digest;
|
|
2420
2537
|
}
|
|
2421
|
-
return cursors.scopes[conversationKey(scope,
|
|
2538
|
+
return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : threadStart(cursors, scope));
|
|
2422
2539
|
}
|
|
2423
2540
|
function byId(a, b) {
|
|
2424
2541
|
return a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0;
|
|
@@ -2429,6 +2546,9 @@ function turnTime(record) {
|
|
|
2429
2546
|
var PausedSchema = z12.object({ paused: z12.boolean() });
|
|
2430
2547
|
var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
|
|
2431
2548
|
var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
|
|
2549
|
+
function withoutMentions(text) {
|
|
2550
|
+
return text.replaceAll(MENTION_PATTERN, "$1$2");
|
|
2551
|
+
}
|
|
2432
2552
|
var CURATING_ROLES = [USER_ROLE, "steward"];
|
|
2433
2553
|
var SEED_INSTRUCTIONS_HEADING = "## Seed instructions";
|
|
2434
2554
|
var OPS_WAKE_TRIGGER = "ops_event";
|
|
@@ -2445,6 +2565,7 @@ var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
|
|
|
2445
2565
|
var THREAD_CONVERSATIONS = "thread-conversations";
|
|
2446
2566
|
var ASKS_CHANNEL_OPENED = "asks-channel-opened";
|
|
2447
2567
|
var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
|
|
2568
|
+
var CHANNEL_CONVERSATIONS = "channel-conversations";
|
|
2448
2569
|
var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
2449
2570
|
var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
2450
2571
|
var PROFILE_TEMPLATE = "# Profile\n\nOne short paragraph, kept current: what I do well, what I am working on, and what to send my way. The board projects this into the roster the front desk reads.\n";
|
|
@@ -2575,6 +2696,8 @@ function snippetAround(text, query, radius = 80) {
|
|
|
2575
2696
|
var Board = class _Board {
|
|
2576
2697
|
paths;
|
|
2577
2698
|
mutex = new Mutex();
|
|
2699
|
+
/** The turns in flight, which the server hosting them supplies; none until it does. */
|
|
2700
|
+
turnsInFlight = () => [];
|
|
2578
2701
|
newId = monotonicFactory();
|
|
2579
2702
|
events;
|
|
2580
2703
|
actionCounts = /* @__PURE__ */ new Map();
|
|
@@ -2635,7 +2758,8 @@ var Board = class _Board {
|
|
|
2635
2758
|
DECISIONS_CHANNEL_RETIRED,
|
|
2636
2759
|
THREAD_CONVERSATIONS,
|
|
2637
2760
|
ASKS_CHANNEL_OPENED,
|
|
2638
|
-
KNOWLEDGE_REMOVAL
|
|
2761
|
+
KNOWLEDGE_REMOVAL,
|
|
2762
|
+
CHANNEL_CONVERSATIONS
|
|
2639
2763
|
].filter((name) => !applied.includes(name));
|
|
2640
2764
|
if (pending.length === 0) {
|
|
2641
2765
|
return;
|
|
@@ -2667,6 +2791,9 @@ var Board = class _Board {
|
|
|
2667
2791
|
if (pending.includes(KNOWLEDGE_REMOVAL)) {
|
|
2668
2792
|
await this.grantKnowledgeRemoval();
|
|
2669
2793
|
}
|
|
2794
|
+
if (pending.includes(CHANNEL_CONVERSATIONS)) {
|
|
2795
|
+
await this.startChannelCursors();
|
|
2796
|
+
}
|
|
2670
2797
|
await writeJson(file, { applied: [...applied, ...pending] });
|
|
2671
2798
|
}
|
|
2672
2799
|
/**
|
|
@@ -2841,6 +2968,27 @@ var Board = class _Board {
|
|
|
2841
2968
|
}
|
|
2842
2969
|
}
|
|
2843
2970
|
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Every channel of a scope was once read in its home conversation, so a member had read each up
|
|
2973
|
+
* to its scope's cursor. Each channel beside general starts from that cursor now, as each takes a
|
|
2974
|
+
* conversation of its own.
|
|
2975
|
+
*/
|
|
2976
|
+
async startChannelCursors() {
|
|
2977
|
+
for (const agent2 of await this.listAgents()) {
|
|
2978
|
+
const file = this.paths.agentCursors(agent2.name);
|
|
2979
|
+
if (!await exists(file)) {
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2982
|
+
const cursors = await readJson(file, CursorsSchema);
|
|
2983
|
+
const scopes = /* @__PURE__ */ new Set([
|
|
2984
|
+
...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
|
|
2985
|
+
...agent2.memberships,
|
|
2986
|
+
SOCIETY_SCOPE
|
|
2987
|
+
]);
|
|
2988
|
+
const channelsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
|
|
2989
|
+
await writeJson(file, { ...cursors, channelsFrom });
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2844
2992
|
/**
|
|
2845
2993
|
* Threads were once read in the scope's single conversation, so a member had read every thread
|
|
2846
2994
|
* up to its scope's cursor. Each scope's threads start from that cursor now, as each thread's
|
|
@@ -2974,17 +3122,20 @@ ${charter.purpose}
|
|
|
2974
3122
|
/**
|
|
2975
3123
|
* A short-lived token for one turn, handed to the CLI by the runner. Only its hash is kept,
|
|
2976
3124
|
* in memory, until it expires, so no raw agent token ever needs to exist at rest. The turn's
|
|
2977
|
-
* scope and thread ride on the actor it resolves to, so
|
|
2978
|
-
* digest.
|
|
3125
|
+
* scope and conversation part, a thread's id or `#channel`, ride on the actor it resolves to, so
|
|
3126
|
+
* `read_inbox` reads that conversation's digest.
|
|
2979
3127
|
*/
|
|
2980
|
-
issueTurnToken(agent2, role2, ttlMs, scope,
|
|
3128
|
+
issueTurnToken(agent2, role2, ttlMs, scope, part) {
|
|
2981
3129
|
const token = mintToken();
|
|
3130
|
+
const thread2 = threadOfPart(part);
|
|
3131
|
+
const channel = channelOfPart(part);
|
|
2982
3132
|
this.turnTokens.set(hashToken(token), {
|
|
2983
3133
|
actor: {
|
|
2984
3134
|
name: agent2,
|
|
2985
3135
|
role: role2,
|
|
2986
3136
|
...scope === void 0 ? {} : { scope },
|
|
2987
|
-
...thread2 === void 0 ? {} : { thread: thread2 }
|
|
3137
|
+
...thread2 === void 0 ? {} : { thread: thread2 },
|
|
3138
|
+
...channel === void 0 ? {} : { channel }
|
|
2988
3139
|
},
|
|
2989
3140
|
expiresAt: this.now().getTime() + ttlMs
|
|
2990
3141
|
});
|
|
@@ -3284,34 +3435,44 @@ ${charter.purpose}
|
|
|
3284
3435
|
}
|
|
3285
3436
|
return (await readJson(file, PausedSchema)).paused;
|
|
3286
3437
|
}
|
|
3287
|
-
/**
|
|
3288
|
-
|
|
3438
|
+
/**
|
|
3439
|
+
* Every channel, the society's first, with how many messages it holds and its newest one; each
|
|
3440
|
+
* place's archived channels follow its open ones.
|
|
3441
|
+
*/
|
|
3289
3442
|
async listChannels() {
|
|
3290
3443
|
const summaries = [];
|
|
3291
|
-
const
|
|
3292
|
-
|
|
3444
|
+
const society = await this.society();
|
|
3445
|
+
const places = [
|
|
3446
|
+
{ project: null, names: society.channels, archived: society.archivedChannels },
|
|
3293
3447
|
...(await this.listProjects()).map((project2) => ({
|
|
3294
3448
|
project: project2.slug,
|
|
3295
|
-
names: project2.channels
|
|
3449
|
+
names: project2.channels,
|
|
3450
|
+
archived: project2.archivedChannels
|
|
3296
3451
|
}))
|
|
3297
3452
|
];
|
|
3298
|
-
for (const { project: project2, names } of
|
|
3299
|
-
|
|
3300
|
-
|
|
3453
|
+
for (const { project: project2, names, archived } of places) {
|
|
3454
|
+
const entries = [
|
|
3455
|
+
...names.map((name) => ({ name, archived: void 0 })),
|
|
3456
|
+
...archived.map((each) => ({ name: each.name, archived: each }))
|
|
3457
|
+
];
|
|
3458
|
+
for (const entry of entries) {
|
|
3459
|
+
const ref = channelRef(project2, entry.name);
|
|
3301
3460
|
const files = await listFiles(this.paths.channelDir(ref));
|
|
3302
3461
|
const last = files.at(-1)?.slice(0, 26) ?? null;
|
|
3303
3462
|
summaries.push({
|
|
3304
3463
|
ref,
|
|
3305
3464
|
project: project2,
|
|
3306
|
-
name,
|
|
3465
|
+
name: entry.name,
|
|
3307
3466
|
messages: files.length,
|
|
3308
3467
|
lastMessageId: last,
|
|
3309
|
-
lastAt: last === null ? null : new Date(decodeTime(last)).toISOString()
|
|
3468
|
+
lastAt: last === null ? null : new Date(decodeTime(last)).toISOString(),
|
|
3469
|
+
...entry.archived === void 0 ? {} : { archived: entry.archived }
|
|
3310
3470
|
});
|
|
3311
3471
|
}
|
|
3312
3472
|
}
|
|
3313
3473
|
return summaries;
|
|
3314
3474
|
}
|
|
3475
|
+
/** Every message in a channel, oldest first. Thread messages are listed with `listThread`. */
|
|
3315
3476
|
async listChannel(ref, limit = 200) {
|
|
3316
3477
|
await this.assertChannelExists(ref);
|
|
3317
3478
|
const messages = await this.readMessagesIn(this.paths.channelDir(ref), null);
|
|
@@ -3464,12 +3625,27 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3464
3625
|
return (await this.writeRoleUnlocked(actor.name, parsed, {})).charter;
|
|
3465
3626
|
});
|
|
3466
3627
|
}
|
|
3628
|
+
/** Opens a channel in a project or the society for a workstream, such as a release's tasks. */
|
|
3629
|
+
async createChannel(actor, input) {
|
|
3630
|
+
const args = VerbInputs.create_channel.parse(input);
|
|
3631
|
+
await this.authorize(actor, "create_channel");
|
|
3632
|
+
return this.mutex.run(async () => {
|
|
3633
|
+
await this.validateAddChannel(args);
|
|
3634
|
+
return this.addChannelUnlocked(actor.name, args, {}, actor.name);
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
/** Archives a channel whose workstream is done; refused while a task filed there is in play. */
|
|
3638
|
+
async archiveChannel(actor, input) {
|
|
3639
|
+
const args = VerbInputs.archive_channel.parse(input);
|
|
3640
|
+
await this.authorize(actor, "archive_channel");
|
|
3641
|
+
return this.mutex.run(() => this.archiveChannelUnlocked(actor, args.channel, args.reason));
|
|
3642
|
+
}
|
|
3467
3643
|
/** Adds a channel to a project or to the society. Also what an approved channel proposal executes. */
|
|
3468
3644
|
async addChannel(actor, input) {
|
|
3469
3645
|
this.assertAdmin(actor);
|
|
3470
3646
|
return this.mutex.run(async () => {
|
|
3471
3647
|
await this.validateAddChannel(input);
|
|
3472
|
-
return this.addChannelUnlocked(actor.name, input, {});
|
|
3648
|
+
return this.addChannelUnlocked(actor.name, input, {}, actor.name);
|
|
3473
3649
|
});
|
|
3474
3650
|
}
|
|
3475
3651
|
/**
|
|
@@ -3667,19 +3843,19 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3667
3843
|
return this.mutex.run(async () => {
|
|
3668
3844
|
const stored = await this.readCursors(actor.name);
|
|
3669
3845
|
const from = args.since_cursor === void 0 ? stored : { ...NO_CURSORS, digest: args.since_cursor };
|
|
3670
|
-
const conversation = actor.scope === void 0 ? void 0 : { scope: actor.scope,
|
|
3846
|
+
const conversation = actor.scope === void 0 ? void 0 : { scope: actor.scope, part: conversationPart(actor) ?? null };
|
|
3671
3847
|
const filed = (await this.digestSince(actor, from, conversation)).slice(0, args.limit);
|
|
3672
3848
|
if (args.advance) {
|
|
3673
3849
|
await this.advanceCursors(actor.name, filed);
|
|
3674
3850
|
}
|
|
3675
3851
|
const last = filed.at(-1)?.message.id;
|
|
3676
|
-
const cursor = last ?? (conversation === void 0 ? from.digest : cursorOf(from, conversation.scope, conversation.
|
|
3852
|
+
const cursor = last ?? (conversation === void 0 ? from.digest : cursorOf(from, conversation.scope, conversation.part));
|
|
3677
3853
|
return { messages: filed.map((entry) => entry.message), cursor };
|
|
3678
3854
|
});
|
|
3679
3855
|
}
|
|
3680
3856
|
/**
|
|
3681
3857
|
* How many unread digest messages a member has in each conversation: a scope's home, for its
|
|
3682
|
-
* channels, or one of its threads. The heartbeat asks, so that it wakes a member where its unread
|
|
3858
|
+
* general channel, one of its other channels, or one of its threads. The heartbeat asks, so that it wakes a member where its unread
|
|
3683
3859
|
* messages are and nowhere else. Society channels count only for a member woken in the society
|
|
3684
3860
|
* scope; a project member reads them at its next turn. Two kinds of post are in the digest but not
|
|
3685
3861
|
* counted, and are read when the member wakes for something else: a task step's, which wakes
|
|
@@ -3690,7 +3866,7 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3690
3866
|
return this.mutex.run(async () => {
|
|
3691
3867
|
const counts = /* @__PURE__ */ new Map();
|
|
3692
3868
|
const unscoped = { name: actor.name, role: actor.role };
|
|
3693
|
-
for (const { scope,
|
|
3869
|
+
for (const { scope, part, message, news } of await this.digestSince(unscoped, await this.readCursors(actor.name))) {
|
|
3694
3870
|
if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
|
|
3695
3871
|
continue;
|
|
3696
3872
|
}
|
|
@@ -3698,8 +3874,13 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3698
3874
|
if (posted !== scope && scope !== SOCIETY_SCOPE) {
|
|
3699
3875
|
continue;
|
|
3700
3876
|
}
|
|
3701
|
-
const key = conversationKey(scope,
|
|
3702
|
-
const entry = counts.get(key) ?? {
|
|
3877
|
+
const key = conversationKey(scope, part);
|
|
3878
|
+
const entry = counts.get(key) ?? {
|
|
3879
|
+
scope,
|
|
3880
|
+
thread: threadOfPart(part) ?? null,
|
|
3881
|
+
channel: channelOfPart(part) ?? null,
|
|
3882
|
+
count: 0
|
|
3883
|
+
};
|
|
3703
3884
|
counts.set(key, { ...entry, count: entry.count + 1 });
|
|
3704
3885
|
}
|
|
3705
3886
|
return [...counts.values()];
|
|
@@ -3715,7 +3896,7 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3715
3896
|
const scopes = { ...cursors.scopes };
|
|
3716
3897
|
let news = newsStart(cursors);
|
|
3717
3898
|
let moved = false;
|
|
3718
|
-
for (const { scope,
|
|
3899
|
+
for (const { scope, part, message, news: isNews } of filed) {
|
|
3719
3900
|
if (isNews === true) {
|
|
3720
3901
|
if (news === null || message.id > news) {
|
|
3721
3902
|
news = message.id;
|
|
@@ -3723,8 +3904,8 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3723
3904
|
}
|
|
3724
3905
|
continue;
|
|
3725
3906
|
}
|
|
3726
|
-
const key = conversationKey(scope,
|
|
3727
|
-
const current = scopes[key] ?? cursorOf(cursors, scope,
|
|
3907
|
+
const key = conversationKey(scope, part);
|
|
3908
|
+
const current = scopes[key] ?? cursorOf(cursors, scope, part);
|
|
3728
3909
|
if (current === null || message.id > current) {
|
|
3729
3910
|
scopes[key] = message.id;
|
|
3730
3911
|
moved = true;
|
|
@@ -3743,29 +3924,32 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3743
3924
|
* message is filed in: what mentions the member, sits in a channel it follows, or belongs to a
|
|
3744
3925
|
* thread it takes part in, and for the front desk, which every post by the user wakes, every post
|
|
3745
3926
|
* by the user wherever it is. With a conversation, only what is filed there: for a thread, every
|
|
3746
|
-
* message of the thread, since the conversation is about it; for a
|
|
3747
|
-
* messages,
|
|
3927
|
+
* message of the thread, since the conversation is about it; for a channel's, that channel's
|
|
3928
|
+
* top-level messages; for a scope's home, general's and those of a project the member is not in,
|
|
3929
|
+
* since each thread and each other channel is a conversation of its own.
|
|
3748
3930
|
*/
|
|
3749
3931
|
async digestSince(actor, cursors, conversation) {
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
const
|
|
3932
|
+
const conversationThread = threadOfPart(conversation?.part);
|
|
3933
|
+
if (conversation !== void 0 && conversationThread !== void 0) {
|
|
3934
|
+
const { scope, part } = conversation;
|
|
3935
|
+
const found = await this.tryFindThread(conversationThread);
|
|
3753
3936
|
if (found === null) {
|
|
3754
3937
|
return [];
|
|
3755
3938
|
}
|
|
3756
|
-
const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads,
|
|
3757
|
-
return messages.filter((message) => message.author !== actor.name).map((message) => ({ scope,
|
|
3939
|
+
const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads, conversationThread), cursorOf(cursors, scope, part));
|
|
3940
|
+
return messages.filter((message) => message.author !== actor.name).map((message) => ({ scope, part, message })).toSorted(byId);
|
|
3758
3941
|
}
|
|
3759
3942
|
const agent2 = await this.readAgent(actor.name);
|
|
3760
3943
|
const charter = await this.readRole(agent2.role);
|
|
3761
3944
|
const subscribed = new Set(agent2.subscriptions);
|
|
3762
3945
|
const frontDesk = charter.wakeTriggers.includes(FRONT_DESK_TRIGGER);
|
|
3763
3946
|
const threadParticipation = /* @__PURE__ */ new Map();
|
|
3947
|
+
const openChannels = /* @__PURE__ */ new Map();
|
|
3764
3948
|
const collected = [];
|
|
3765
3949
|
const newsFrom = charter.societyScope ? void 0 : newsStart(cursors);
|
|
3766
3950
|
const scopes = conversation !== void 0 ? [conversation.scope] : [...agent2.memberships, SOCIETY_SCOPE];
|
|
3767
3951
|
const starts = [
|
|
3768
|
-
...scopes.flatMap((each) => conversation === void 0 ? [cursorOf(cursors, each), threadStart(cursors, each)] : [cursorOf(cursors, each)]),
|
|
3952
|
+
...scopes.flatMap((each) => conversation === void 0 ? [cursorOf(cursors, each), threadStart(cursors, each), channelStart(cursors, each)] : [cursorOf(cursors, each, conversation.part)]),
|
|
3769
3953
|
...newsFrom === void 0 ? [] : [newsFrom]
|
|
3770
3954
|
];
|
|
3771
3955
|
const known = starts.filter((start) => start !== null);
|
|
@@ -3778,20 +3962,24 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3778
3962
|
if ((newsFrom === null || message.id > newsFrom) && subscribed.has(message.channel) && message.author !== actor.name) {
|
|
3779
3963
|
collected.push({
|
|
3780
3964
|
scope: conversation?.scope ?? SOCIETY_SCOPE,
|
|
3781
|
-
|
|
3965
|
+
part: null,
|
|
3782
3966
|
message,
|
|
3783
3967
|
news: true
|
|
3784
3968
|
});
|
|
3785
3969
|
}
|
|
3786
3970
|
continue;
|
|
3787
3971
|
}
|
|
3788
|
-
|
|
3789
|
-
|
|
3972
|
+
const named = thread2 === null ? channelConversation(filedIn, message.channel) : void 0;
|
|
3973
|
+
let open2 = named === void 0 ? void 0 : openChannels.get(message.channel);
|
|
3974
|
+
if (named !== void 0 && open2 === void 0) {
|
|
3975
|
+
open2 = await this.channelOpen(message.channel);
|
|
3976
|
+
openChannels.set(message.channel, open2);
|
|
3790
3977
|
}
|
|
3791
|
-
|
|
3978
|
+
const part = thread2 ?? (open2 === true ? `#${named}` : null);
|
|
3979
|
+
if (conversation !== void 0 && (filedIn !== conversation.scope || part !== conversation.part)) {
|
|
3792
3980
|
continue;
|
|
3793
3981
|
}
|
|
3794
|
-
const cursor = cursorOf(cursors, filedIn,
|
|
3982
|
+
const cursor = cursorOf(cursors, filedIn, part);
|
|
3795
3983
|
if (cursor !== null && message.id <= cursor) {
|
|
3796
3984
|
continue;
|
|
3797
3985
|
}
|
|
@@ -3809,7 +3997,7 @@ Agents may edit this file. It is rendered by the board UI.
|
|
|
3809
3997
|
include = participates;
|
|
3810
3998
|
}
|
|
3811
3999
|
if (include && message.author !== actor.name) {
|
|
3812
|
-
collected.push({ scope: filedIn,
|
|
4000
|
+
collected.push({ scope: filedIn, part, message });
|
|
3813
4001
|
}
|
|
3814
4002
|
}
|
|
3815
4003
|
return collected.toSorted(byId);
|
|
@@ -4048,7 +4236,7 @@ ${task2.body}`;
|
|
|
4048
4236
|
taskThreadOpening(task2) {
|
|
4049
4237
|
return {
|
|
4050
4238
|
id: task2.id,
|
|
4051
|
-
channel: channelRef(task2.project,
|
|
4239
|
+
channel: channelRef(task2.project, task2.channel),
|
|
4052
4240
|
title: task2.title,
|
|
4053
4241
|
subject: { kind: "task", id: task2.id }
|
|
4054
4242
|
};
|
|
@@ -4094,7 +4282,7 @@ ${task2.body}`;
|
|
|
4094
4282
|
if (task2.status === "done" || task2.status === "abandoned") {
|
|
4095
4283
|
throw new BoardError("INVALID_STATE", `task ${task2.id} is ${task2.status}`);
|
|
4096
4284
|
}
|
|
4097
|
-
const channel = args.channel ?? channelRef(project2,
|
|
4285
|
+
const channel = args.channel ?? channelRef(project2, task2.channel);
|
|
4098
4286
|
if (parseChannelRef(channel).project !== project2) {
|
|
4099
4287
|
throw new BoardError("VALIDATION", `a task's thread hangs off a channel of its project, ${project2}`);
|
|
4100
4288
|
}
|
|
@@ -4127,6 +4315,44 @@ ${task2.body}`;
|
|
|
4127
4315
|
await this.assertChannelOpen(args.channel);
|
|
4128
4316
|
return { id: this.newId(), channel: args.channel, title: args.title };
|
|
4129
4317
|
}
|
|
4318
|
+
/**
|
|
4319
|
+
* Where the board learns which turns are in flight, from the server that hosts them, so it
|
|
4320
|
+
* refuses to end a conversation under someone else's turn.
|
|
4321
|
+
*/
|
|
4322
|
+
watchTurns(source) {
|
|
4323
|
+
this.turnsInFlight = source;
|
|
4324
|
+
}
|
|
4325
|
+
/**
|
|
4326
|
+
* Refuses to end conversations while another citizen's turn is in one of them: `cut` picks the
|
|
4327
|
+
* turns that would lose theirs. The caller's own turn is exempt, since ending a conversation
|
|
4328
|
+
* from inside it is how a citizen finishes there.
|
|
4329
|
+
*/
|
|
4330
|
+
assertNoTurnCut(actor, what, cut) {
|
|
4331
|
+
const busy = [
|
|
4332
|
+
...new Set(this.turnsInFlight().filter((turn2) => cut(turn2) && !ownTurn(actor, turn2)).map((turn2) => turn2.agent))
|
|
4333
|
+
].toSorted((a, b) => a.localeCompare(b));
|
|
4334
|
+
if (busy.length === 0) {
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
const one = busy.length === 1;
|
|
4338
|
+
throw new BoardError("INVALID_STATE", `${what} while ${namesList(busy)} ${one ? "is in a turn" : "are in turns"} there; try again once ${one ? "it ends" : "they end"}, or stop ${one ? "it" : "them"}`);
|
|
4339
|
+
}
|
|
4340
|
+
/** The open threads in a folder of threads that `keep` picks. */
|
|
4341
|
+
async openThreadIds(dir, keep) {
|
|
4342
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4343
|
+
for (const file of await listFiles(dir)) {
|
|
4344
|
+
const doc = await readMarkdown(path6.join(dir, file), ThreadFrontmatterSchema);
|
|
4345
|
+
if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
|
|
4346
|
+
ids.add(doc.data.id);
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
return ids;
|
|
4350
|
+
}
|
|
4351
|
+
/** Refuses to archive a project while a turn runs in it or in a thread its archive closes. */
|
|
4352
|
+
async assertProjectQuiet(actor, slug) {
|
|
4353
|
+
const threads = await this.openThreadIds(this.paths.threads(slug), () => true);
|
|
4354
|
+
this.assertNoTurnCut(actor, `project ${slug} cannot be archived`, (turn2) => turn2.scope === slug || turn2.thread !== void 0 && threads.has(turn2.thread));
|
|
4355
|
+
}
|
|
4130
4356
|
async closeThread(actor, input) {
|
|
4131
4357
|
const args = VerbInputs.close_thread.parse(input);
|
|
4132
4358
|
await this.authorize(actor, "close_thread");
|
|
@@ -4141,6 +4367,7 @@ ${task2.body}`;
|
|
|
4141
4367
|
if (!PLANNING_ROLES.includes(actor.role) && !await this.participatesInThread(actor, thread2, threads)) {
|
|
4142
4368
|
throw new BoardError("FORBIDDEN", "only the thread's participants, the user, the steward, or the concierge may close it");
|
|
4143
4369
|
}
|
|
4370
|
+
this.assertNoTurnCut(actor, `thread ${thread2.id} cannot close`, (turn2) => turn2.thread === thread2.id);
|
|
4144
4371
|
const summary = await this.writeMessage(this.paths.channelDir(thread2.channel), { author: actor.name, channel: thread2.channel, closes: thread2.id }, `Thread closed: "${thread2.title}" (${thread2.id}).
|
|
4145
4372
|
|
|
4146
4373
|
${args.summary}`);
|
|
@@ -4179,10 +4406,12 @@ ${args.summary}`);
|
|
|
4179
4406
|
if (first === void 0) {
|
|
4180
4407
|
throw new BoardError("VALIDATION", "a plan needs at least one stage");
|
|
4181
4408
|
}
|
|
4409
|
+
await this.assertChannelOpen(channelRef(args.project, args.channel));
|
|
4182
4410
|
const ts = this.now().toISOString();
|
|
4183
4411
|
const frontmatter = TaskFrontmatterSchema.parse({
|
|
4184
4412
|
id: this.newId(),
|
|
4185
4413
|
project: args.project,
|
|
4414
|
+
channel: args.channel,
|
|
4186
4415
|
title: args.title,
|
|
4187
4416
|
status: "open",
|
|
4188
4417
|
createdBy: actor.name,
|
|
@@ -4198,11 +4427,11 @@ ${args.summary}`);
|
|
|
4198
4427
|
onDone: project2.onDone,
|
|
4199
4428
|
completing: false
|
|
4200
4429
|
});
|
|
4201
|
-
await this.assertChannelExists(channelRef(args.project, "general"));
|
|
4202
4430
|
const task2 = await this.writeTask(args.project, { ...frontmatter, body: args.body });
|
|
4203
4431
|
await this.events.append("task.created", actor.name, {
|
|
4204
4432
|
taskId: task2.id,
|
|
4205
4433
|
project: task2.project,
|
|
4434
|
+
channel: task2.channel,
|
|
4206
4435
|
title: task2.title,
|
|
4207
4436
|
parentId: task2.parentId ?? null,
|
|
4208
4437
|
stage: task2.stage
|
|
@@ -4318,6 +4547,9 @@ ${args.summary}`);
|
|
|
4318
4547
|
completedAt: ts
|
|
4319
4548
|
};
|
|
4320
4549
|
const next = current.stages[index + 1];
|
|
4550
|
+
if (next === void 0 && current.onDone === "none") {
|
|
4551
|
+
this.assertNoTurnCut(actor, `task ${current.id} cannot end`, (turn2) => turn2.thread === current.id);
|
|
4552
|
+
}
|
|
4321
4553
|
const reworkDone = current.returned === void 0 || next === void 0 || index + 1 >= stageIndex(current, current.returned.from);
|
|
4322
4554
|
const base = {
|
|
4323
4555
|
...current,
|
|
@@ -4481,6 +4713,7 @@ ${args.summary}`);
|
|
|
4481
4713
|
if (!mayAbandon) {
|
|
4482
4714
|
throw new BoardError("FORBIDDEN", "only the holder, the creator, the user, the steward, or the concierge may abandon a task");
|
|
4483
4715
|
}
|
|
4716
|
+
this.assertNoTurnCut(actor, `task ${current.id} cannot be abandoned`, (turn2) => turn2.thread === current.id);
|
|
4484
4717
|
next = {
|
|
4485
4718
|
...next,
|
|
4486
4719
|
status: "abandoned",
|
|
@@ -4682,6 +4915,7 @@ ${rationale}`}`);
|
|
|
4682
4915
|
await this.authorize(actor, "archive_project");
|
|
4683
4916
|
return this.mutex.run(async () => {
|
|
4684
4917
|
await this.validateArchive(args.project);
|
|
4918
|
+
await this.assertProjectQuiet(actor, args.project);
|
|
4685
4919
|
return (await this.archiveUnlocked(actor.name, args.project, args.reason, {})).project;
|
|
4686
4920
|
});
|
|
4687
4921
|
}
|
|
@@ -4689,6 +4923,10 @@ ${rationale}`}`);
|
|
|
4689
4923
|
* A citizen joins a project itself, or a reallocating role adds it. The pair gets its project
|
|
4690
4924
|
* directory now and, through the `agent.joined` event, an onboarding turn.
|
|
4691
4925
|
*/
|
|
4926
|
+
/**
|
|
4927
|
+
* A member follows the project's general from the start; the result names the project's other
|
|
4928
|
+
* open channels, which it subscribes to when its work there needs them.
|
|
4929
|
+
*/
|
|
4692
4930
|
async joinProject(actor, input) {
|
|
4693
4931
|
const args = VerbInputs.join_project.parse(input);
|
|
4694
4932
|
await this.authorize(actor, "join_project");
|
|
@@ -4701,7 +4939,7 @@ ${rationale}`}`);
|
|
|
4701
4939
|
throw new BoardError("INVALID_STATE", `${target} is retired`);
|
|
4702
4940
|
}
|
|
4703
4941
|
if (current.memberships.includes(args.project)) {
|
|
4704
|
-
return current;
|
|
4942
|
+
return { ...current, otherChannels: await this.otherChannels(args.project, current) };
|
|
4705
4943
|
}
|
|
4706
4944
|
await this.assertMayJoinProjects(target, current.role);
|
|
4707
4945
|
const agent2 = await this.updateAgent(target, (a) => ({
|
|
@@ -4720,9 +4958,13 @@ ${rationale}`}`);
|
|
|
4720
4958
|
project: args.project,
|
|
4721
4959
|
cli: agent2.cli
|
|
4722
4960
|
});
|
|
4723
|
-
return agent2;
|
|
4961
|
+
return { ...agent2, otherChannels: await this.otherChannels(args.project, agent2) };
|
|
4724
4962
|
});
|
|
4725
4963
|
}
|
|
4964
|
+
/** The open channels of a project a citizen does not follow. */
|
|
4965
|
+
async otherChannels(project2, agent2) {
|
|
4966
|
+
return (await this.readProject(project2)).channels.map((name) => channelRef(project2, name)).filter((ref) => !agent2.subscriptions.includes(ref));
|
|
4967
|
+
}
|
|
4726
4968
|
async leaveProject(actor, input) {
|
|
4727
4969
|
const args = VerbInputs.leave_project.parse(input);
|
|
4728
4970
|
await this.authorize(actor, "leave_project");
|
|
@@ -4827,6 +5069,10 @@ ${rationale}`}`);
|
|
|
4827
5069
|
return this.configureProject(actor, VerbInputs.configure_project.parse(input));
|
|
4828
5070
|
case "archive_project":
|
|
4829
5071
|
return this.archiveProject(actor, VerbInputs.archive_project.parse(input));
|
|
5072
|
+
case "create_channel":
|
|
5073
|
+
return this.createChannel(actor, VerbInputs.create_channel.parse(input));
|
|
5074
|
+
case "archive_channel":
|
|
5075
|
+
return this.archiveChannel(actor, VerbInputs.archive_channel.parse(input));
|
|
4830
5076
|
default:
|
|
4831
5077
|
throw new BoardError("VALIDATION", `unknown verb ${String(verb)}`);
|
|
4832
5078
|
}
|
|
@@ -4937,29 +5183,30 @@ ${rationale}`}`);
|
|
|
4937
5183
|
/**
|
|
4938
5184
|
* Moves a conversation's cursor forward to `cursor` once a turn there has delivered up to it. It
|
|
4939
5185
|
* never moves back, so a turn that ends after a later one in the same conversation cannot rewind it.
|
|
5186
|
+
* `part` names the conversation beside the scope's home: a thread's id or `#channel`.
|
|
4940
5187
|
*/
|
|
4941
|
-
async setDigestCursor(agent2, scope, cursor,
|
|
5188
|
+
async setDigestCursor(agent2, scope, cursor, part) {
|
|
4942
5189
|
await this.mutex.run(async () => {
|
|
4943
5190
|
if (cursor === null) {
|
|
4944
5191
|
return;
|
|
4945
5192
|
}
|
|
4946
5193
|
const cursors = await this.readCursors(agent2);
|
|
4947
|
-
const current = cursorOf(cursors, scope,
|
|
5194
|
+
const current = cursorOf(cursors, scope, part ?? null);
|
|
4948
5195
|
const news = newsStart(cursors);
|
|
4949
|
-
const readNews =
|
|
5196
|
+
const readNews = part === void 0 && (news === null || cursor > news);
|
|
4950
5197
|
if (current !== null && cursor <= current && !readNews) {
|
|
4951
5198
|
return;
|
|
4952
5199
|
}
|
|
4953
5200
|
await writeJson(this.paths.agentCursors(agent2), {
|
|
4954
5201
|
...cursors,
|
|
4955
|
-
scopes: current !== null && cursor <= current ? cursors.scopes : { ...cursors.scopes, [conversationKey(scope,
|
|
5202
|
+
scopes: current !== null && cursor <= current ? cursors.scopes : { ...cursors.scopes, [conversationKey(scope, part ?? null)]: cursor },
|
|
4956
5203
|
...readNews ? { news: cursor } : {}
|
|
4957
5204
|
});
|
|
4958
5205
|
});
|
|
4959
5206
|
}
|
|
4960
5207
|
/** Where a conversation's digest resumes: the newest message a turn there was shown. */
|
|
4961
|
-
async digestCursor(agent2, scope,
|
|
4962
|
-
return this.mutex.run(async () => cursorOf(await this.readCursors(agent2), scope,
|
|
5208
|
+
async digestCursor(agent2, scope, part) {
|
|
5209
|
+
return this.mutex.run(async () => cursorOf(await this.readCursors(agent2), scope, part ?? null));
|
|
4963
5210
|
}
|
|
4964
5211
|
/** Records that a running turn took posts delivered into it, which wake latency counts to. */
|
|
4965
5212
|
async recordSteered(input) {
|
|
@@ -4968,25 +5215,29 @@ ${rationale}`}`);
|
|
|
4968
5215
|
turnId: input.turnId,
|
|
4969
5216
|
project: input.project,
|
|
4970
5217
|
...input.thread === void 0 ? {} : { thread: input.thread },
|
|
5218
|
+
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
4971
5219
|
messages: [...input.messages]
|
|
4972
5220
|
});
|
|
4973
5221
|
});
|
|
4974
5222
|
}
|
|
4975
|
-
/**
|
|
4976
|
-
|
|
4977
|
-
|
|
5223
|
+
/**
|
|
5224
|
+
* Where a conversation's session ids and last turn are kept: the scope's home, or beside it a
|
|
5225
|
+
* thread's or a channel's, named by `part` as `conversationPart` gives it.
|
|
5226
|
+
*/
|
|
5227
|
+
sessionDir(agent2, scope, part) {
|
|
5228
|
+
return part === void 0 ? this.paths.agentProject(agent2, scope) : this.paths.agentConversation(agent2, scope, part);
|
|
4978
5229
|
}
|
|
4979
|
-
async readSessions(agent2, project2,
|
|
4980
|
-
const file = path6.join(this.sessionDir(agent2, project2,
|
|
5230
|
+
async readSessions(agent2, project2, part) {
|
|
5231
|
+
const file = path6.join(this.sessionDir(agent2, project2, part), "sessions.json");
|
|
4981
5232
|
return await exists(file) ? readJson(file, SessionsFileSchema) : {};
|
|
4982
5233
|
}
|
|
4983
5234
|
/**
|
|
4984
5235
|
* Records a conversation's session for a CLI and the runner it began on. A session started on
|
|
4985
5236
|
* another runner replaces the record, since sessions do not move between machines.
|
|
4986
5237
|
*/
|
|
4987
|
-
async writeSession(agent2, project2, cli, sessionId, runner2,
|
|
5238
|
+
async writeSession(agent2, project2, cli, sessionId, runner2, part) {
|
|
4988
5239
|
await this.mutex.run(async () => {
|
|
4989
|
-
const dir = this.sessionDir(agent2, project2,
|
|
5240
|
+
const dir = this.sessionDir(agent2, project2, part);
|
|
4990
5241
|
await ensureDir(dir);
|
|
4991
5242
|
const file = path6.join(dir, "sessions.json");
|
|
4992
5243
|
const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
|
|
@@ -4994,17 +5245,21 @@ ${rationale}`}`);
|
|
|
4994
5245
|
await writeJson(file, { ...kept, [cli]: sessionId, runner: runner2 });
|
|
4995
5246
|
});
|
|
4996
5247
|
}
|
|
4997
|
-
/** The last turn of one conversation: the scope's home, or a thread's. */
|
|
4998
|
-
async readLastTurn(agent2, project2,
|
|
4999
|
-
const file = path6.join(this.sessionDir(agent2, project2,
|
|
5248
|
+
/** The last turn of one conversation: the scope's home, a channel's, or a thread's. */
|
|
5249
|
+
async readLastTurn(agent2, project2, part) {
|
|
5250
|
+
const file = path6.join(this.sessionDir(agent2, project2, part), "last-turn.json");
|
|
5000
5251
|
return await exists(file) ? readJson(file, TurnRecordSchema) : null;
|
|
5001
5252
|
}
|
|
5002
5253
|
/** The latest turn a citizen took in a scope, in any of its conversations there. */
|
|
5003
5254
|
async readLatestTurn(agent2, project2) {
|
|
5004
5255
|
let latest = await this.readLastTurn(agent2, project2);
|
|
5005
|
-
const
|
|
5006
|
-
|
|
5007
|
-
|
|
5256
|
+
const place = this.paths.agentProject(agent2, project2);
|
|
5257
|
+
const parts = [
|
|
5258
|
+
...await listDirs(path6.join(place, "threads")),
|
|
5259
|
+
...(await listDirs(path6.join(place, "channels"))).map((name) => `#${name}`)
|
|
5260
|
+
];
|
|
5261
|
+
for (const part of parts) {
|
|
5262
|
+
const turn2 = await this.readLastTurn(agent2, project2, part);
|
|
5008
5263
|
if (turn2 !== null && (latest === null || turnTime(turn2) > turnTime(latest))) {
|
|
5009
5264
|
latest = turn2;
|
|
5010
5265
|
}
|
|
@@ -5021,6 +5276,7 @@ ${rationale}`}`);
|
|
|
5021
5276
|
turnId: parsed.id,
|
|
5022
5277
|
project: parsed.project,
|
|
5023
5278
|
...parsed.thread === void 0 ? {} : { thread: parsed.thread },
|
|
5279
|
+
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
5024
5280
|
trigger: parsed.trigger.kind,
|
|
5025
5281
|
session: parsed.session,
|
|
5026
5282
|
runner: parsed.runner
|
|
@@ -5046,13 +5302,15 @@ ${rationale}`}`);
|
|
|
5046
5302
|
...parsed.id === void 0 ? {} : { turnId: parsed.id },
|
|
5047
5303
|
agent: parsed.agent,
|
|
5048
5304
|
project: parsed.project,
|
|
5049
|
-
...parsed.thread === void 0 ? {} : { thread: parsed.thread }
|
|
5305
|
+
...parsed.thread === void 0 ? {} : { thread: parsed.thread },
|
|
5306
|
+
...parsed.channel === void 0 ? {} : { channel: parsed.channel }
|
|
5050
5307
|
});
|
|
5051
5308
|
}
|
|
5052
5309
|
await this.events.append(failed ? "turn.failed" : "turn.completed", parsed.agent, {
|
|
5053
5310
|
...parsed.id === void 0 ? {} : { turnId: parsed.id },
|
|
5054
5311
|
project: parsed.project,
|
|
5055
5312
|
...parsed.thread === void 0 ? {} : { thread: parsed.thread },
|
|
5313
|
+
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
5056
5314
|
trigger: parsed.trigger.kind,
|
|
5057
5315
|
session: parsed.session,
|
|
5058
5316
|
exitReason: parsed.exitReason,
|
|
@@ -5076,6 +5334,47 @@ ${rationale}`}`);
|
|
|
5076
5334
|
});
|
|
5077
5335
|
});
|
|
5078
5336
|
}
|
|
5337
|
+
/**
|
|
5338
|
+
* Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
|
|
5339
|
+
* the scheduler gives the citizen a closing turn there to decide about it. False, recording
|
|
5340
|
+
* nothing, once `CLOSING_ATTEMPTS` closing turns have been given for it.
|
|
5341
|
+
*/
|
|
5342
|
+
async recordLeftovers(input) {
|
|
5343
|
+
const { conversation } = input;
|
|
5344
|
+
const payload = {
|
|
5345
|
+
agent: input.agent,
|
|
5346
|
+
project: input.scope,
|
|
5347
|
+
...conversation.kind === "thread" ? { thread: conversation.id } : { channel: conversation.name }
|
|
5348
|
+
};
|
|
5349
|
+
return this.mutex.run(async () => {
|
|
5350
|
+
const earlier = (await this.events.readSince(null, Number.MAX_SAFE_INTEGER)).filter((event) => event.type === "workspace.leftovers" && Object.entries(payload).every(([key, value]) => event.payload[key] === value)).length;
|
|
5351
|
+
if (earlier >= CLOSING_ATTEMPTS) {
|
|
5352
|
+
return false;
|
|
5353
|
+
}
|
|
5354
|
+
await this.events.append("workspace.leftovers", SYSTEM_ACTOR.name, payload);
|
|
5355
|
+
return true;
|
|
5356
|
+
});
|
|
5357
|
+
}
|
|
5358
|
+
/**
|
|
5359
|
+
* Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
|
|
5360
|
+
* (for one from before tasks had threads), or a channel archived.
|
|
5361
|
+
*/
|
|
5362
|
+
async conversationEnded(conversation) {
|
|
5363
|
+
if (conversation.kind === "channel") {
|
|
5364
|
+
const place = conversation.scope === SOCIETY_SCOPE ? null : conversation.scope;
|
|
5365
|
+
return !await this.channelOpen(channelRef(place, conversation.name));
|
|
5366
|
+
}
|
|
5367
|
+
const found = await this.tryFindThread(conversation.id);
|
|
5368
|
+
if (found !== null) {
|
|
5369
|
+
return found.thread.state === "closed";
|
|
5370
|
+
}
|
|
5371
|
+
try {
|
|
5372
|
+
const { task: task2 } = await this.findTask(conversation.id);
|
|
5373
|
+
return task2.status === "done" || task2.status === "abandoned";
|
|
5374
|
+
} catch {
|
|
5375
|
+
return false;
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5079
5378
|
/**
|
|
5080
5379
|
* Records the outcome of a completing task's effect: done, or waiting again at its last stage so
|
|
5081
5380
|
* its participants can reshape the plan. A merge's outcome is posted to the task's thread, and a
|
|
@@ -5111,7 +5410,7 @@ ${rationale}`}`);
|
|
|
5111
5410
|
await this.refreshMember(name);
|
|
5112
5411
|
}
|
|
5113
5412
|
if (merged) {
|
|
5114
|
-
await this.appendMessage(actor.name, channelRef(location.project,
|
|
5413
|
+
await this.appendMessage(actor.name, channelRef(location.project, task3.channel), `Task ${task3.id} "${task3.title}" is done: ${sentence(input.detail)}.`);
|
|
5115
5414
|
}
|
|
5116
5415
|
return task3;
|
|
5117
5416
|
}
|
|
@@ -5348,7 +5647,7 @@ ${rationale}`}`);
|
|
|
5348
5647
|
return this.files.read(this.paths.board, paths, () => true);
|
|
5349
5648
|
}
|
|
5350
5649
|
async writeTurnRecord(record) {
|
|
5351
|
-
const dir = this.sessionDir(record.agent, record.project, record
|
|
5650
|
+
const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
|
|
5352
5651
|
await ensureDir(dir);
|
|
5353
5652
|
await writeJson(path6.join(dir, "last-turn.json"), record);
|
|
5354
5653
|
}
|
|
@@ -5652,7 +5951,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
5652
5951
|
};
|
|
5653
5952
|
}
|
|
5654
5953
|
case "channel": {
|
|
5655
|
-
const channel = await this.addChannelUnlocked(by, ChannelProposalSchema.parse(proposal2.charter), meta);
|
|
5954
|
+
const channel = await this.addChannelUnlocked(by, ChannelProposalSchema.parse(proposal2.charter), meta, proposal2.proposedBy);
|
|
5656
5955
|
return { channel };
|
|
5657
5956
|
}
|
|
5658
5957
|
case "role": {
|
|
@@ -5758,18 +6057,20 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
5758
6057
|
return { agent: agent2, token };
|
|
5759
6058
|
}
|
|
5760
6059
|
async validateAddChannel(input) {
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
}
|
|
5765
|
-
return;
|
|
6060
|
+
const place = input.project === null ? await this.society() : await this.readActiveProject(input.project);
|
|
6061
|
+
const ref = channelRef(input.project, input.name);
|
|
6062
|
+
if (place.channels.includes(input.name)) {
|
|
6063
|
+
throw new BoardError("ALREADY_EXISTS", `channel ${ref} already exists`);
|
|
5766
6064
|
}
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
throw new BoardError("ALREADY_EXISTS", `channel ${channelRef(input.project, input.name)} already exists`);
|
|
6065
|
+
if (place.archivedChannels.some((each) => each.name === input.name)) {
|
|
6066
|
+
throw new BoardError("ALREADY_EXISTS", `channel ${ref} was archived; open the new one under another name`);
|
|
5770
6067
|
}
|
|
5771
6068
|
}
|
|
5772
|
-
|
|
6069
|
+
/**
|
|
6070
|
+
* Opens a channel, which `opener` follows from the start: the citizen who opened it or proposed
|
|
6071
|
+
* it. Nobody else is subscribed, and the user, who reads channels directly, follows nothing.
|
|
6072
|
+
*/
|
|
6073
|
+
async addChannelUnlocked(by, input, meta, opener) {
|
|
5773
6074
|
const ref = channelRef(input.project, input.name);
|
|
5774
6075
|
if (input.project === null) {
|
|
5775
6076
|
const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
|
|
@@ -5783,14 +6084,104 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
5783
6084
|
}));
|
|
5784
6085
|
await ensureDir(this.paths.projectChannel(project2, input.name));
|
|
5785
6086
|
}
|
|
6087
|
+
const follower = await this.readAgent(opener).catch(() => null);
|
|
6088
|
+
const follows = follower !== null && follower.status === "active" && follower.role !== USER_ROLE;
|
|
6089
|
+
if (follows) {
|
|
6090
|
+
await this.updateAgent(opener, (agent2) => ({
|
|
6091
|
+
...agent2,
|
|
6092
|
+
subscriptions: [.../* @__PURE__ */ new Set([...agent2.subscriptions, ref])]
|
|
6093
|
+
}));
|
|
6094
|
+
await this.refreshMember(opener);
|
|
6095
|
+
}
|
|
5786
6096
|
await this.events.append("channel.added", by, {
|
|
5787
6097
|
channel: ref,
|
|
5788
6098
|
purpose: input.purpose,
|
|
6099
|
+
...follows ? { followedBy: opener } : {},
|
|
5789
6100
|
...meta
|
|
5790
6101
|
});
|
|
5791
|
-
|
|
6102
|
+
const purpose = withoutMentions(input.purpose);
|
|
6103
|
+
await this.appendMessage(SYSTEM_ACTOR.name, ref, `Channel ${ref} opened: ${purpose}`);
|
|
6104
|
+
await this.appendMessage(SYSTEM_ACTOR.name, channelRef(input.project, "general"), `Channel ${ref} opened by ${by}: ${purpose}. Subscribe to it to follow its posts; its tasks and mentions reach whoever they concern either way.`);
|
|
5792
6105
|
return ref;
|
|
5793
6106
|
}
|
|
6107
|
+
/**
|
|
6108
|
+
* Ends a channel: its open threads close, its followers stop following it, and it leaves the
|
|
6109
|
+
* place's channels for its archived ones, where its history stays readable. The notice is the
|
|
6110
|
+
* board's and leaves the reason out, since a board-authored mention would wake whoever it names.
|
|
6111
|
+
*/
|
|
6112
|
+
async archiveChannelUnlocked(actor, ref, reason) {
|
|
6113
|
+
const by = actor.name;
|
|
6114
|
+
const { project: project2, channel } = parseChannelRef(ref);
|
|
6115
|
+
if (isBoardChannel(project2, channel)) {
|
|
6116
|
+
throw new BoardError("FORBIDDEN", `the board itself uses ${ref}, so it is never archived`);
|
|
6117
|
+
}
|
|
6118
|
+
await this.assertChannelOpen(ref);
|
|
6119
|
+
if (project2 !== null) {
|
|
6120
|
+
const inPlay = (await this.listTasks(project2)).filter((task2) => task2.channel === channel && task2.status !== "done" && task2.status !== "abandoned");
|
|
6121
|
+
if (inPlay.length > 0) {
|
|
6122
|
+
throw new BoardError("INVALID_STATE", `${inPlay.length} task(s) filed in ${ref} are still in play: ${inPlay.map((task2) => task2.id).join(", ")}`);
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
const ts = this.now().toISOString();
|
|
6126
|
+
const threads = project2 === null ? this.paths.societyThreads() : this.paths.threads(project2);
|
|
6127
|
+
const closing = await this.openThreadIds(threads, (thread2) => thread2.channel === ref);
|
|
6128
|
+
const scope = project2 ?? SOCIETY_SCOPE;
|
|
6129
|
+
this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn2) => turn2.channel === channel && turn2.scope === scope || turn2.thread !== void 0 && closing.has(turn2.thread));
|
|
6130
|
+
const threadsClosed = [];
|
|
6131
|
+
for (const file of await listFiles(threads)) {
|
|
6132
|
+
const doc = await readMarkdown(path6.join(threads, file), ThreadFrontmatterSchema);
|
|
6133
|
+
if (doc.data.channel !== ref || doc.data.state !== "open") {
|
|
6134
|
+
continue;
|
|
6135
|
+
}
|
|
6136
|
+
await this.writeThread(threads, {
|
|
6137
|
+
...doc.data,
|
|
6138
|
+
body: doc.body,
|
|
6139
|
+
state: "closed",
|
|
6140
|
+
closedBy: by,
|
|
6141
|
+
closedAt: ts
|
|
6142
|
+
});
|
|
6143
|
+
await this.events.append("thread.closed", by, {
|
|
6144
|
+
threadId: doc.data.id,
|
|
6145
|
+
channel: ref,
|
|
6146
|
+
ended: "archived"
|
|
6147
|
+
});
|
|
6148
|
+
threadsClosed.push(doc.data.id);
|
|
6149
|
+
}
|
|
6150
|
+
const unsubscribed = [];
|
|
6151
|
+
for (const agent2 of await this.listAgents()) {
|
|
6152
|
+
if (agent2.subscriptions.includes(ref)) {
|
|
6153
|
+
await this.updateAgent(agent2.name, (a) => ({
|
|
6154
|
+
...a,
|
|
6155
|
+
subscriptions: a.subscriptions.filter((each) => each !== ref)
|
|
6156
|
+
}));
|
|
6157
|
+
await this.refreshMember(agent2.name);
|
|
6158
|
+
unsubscribed.push(agent2.name);
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
const archived = { name: channel, at: ts, by, reason };
|
|
6162
|
+
if (project2 === null) {
|
|
6163
|
+
const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
|
|
6164
|
+
await writeMarkdown(this.paths.societyFile(), {
|
|
6165
|
+
...doc.data,
|
|
6166
|
+
channels: doc.data.channels.filter((name) => name !== channel),
|
|
6167
|
+
archivedChannels: [...doc.data.archivedChannels, archived]
|
|
6168
|
+
}, doc.body);
|
|
6169
|
+
} else {
|
|
6170
|
+
await this.updateProject(project2, (current) => ({
|
|
6171
|
+
...current,
|
|
6172
|
+
channels: current.channels.filter((name) => name !== channel),
|
|
6173
|
+
archivedChannels: [...current.archivedChannels, archived]
|
|
6174
|
+
}));
|
|
6175
|
+
}
|
|
6176
|
+
await this.events.append("channel.archived", by, {
|
|
6177
|
+
channel: ref,
|
|
6178
|
+
reason,
|
|
6179
|
+
threadsClosed,
|
|
6180
|
+
unsubscribed
|
|
6181
|
+
});
|
|
6182
|
+
await this.appendMessage(SYSTEM_ACTOR.name, channelRef(project2, "general"), `Channel ${ref} is archived by ${by}. Its posts, threads, and tasks stay readable; nothing more is posted or filed there.`);
|
|
6183
|
+
return archived;
|
|
6184
|
+
}
|
|
5794
6185
|
/** A role becomes a society role only while none of its members is in a project. */
|
|
5795
6186
|
async validateRole(charter) {
|
|
5796
6187
|
if (charter.name === USER_ROLE) {
|
|
@@ -6003,23 +6394,35 @@ ${charter.purpose}
|
|
|
6003
6394
|
async publishSignalUnlocked(signal) {
|
|
6004
6395
|
return this.events.append("ops.signal", SYSTEM_ACTOR.name, OpsSignalSchema.parse(signal));
|
|
6005
6396
|
}
|
|
6397
|
+
/** A channel that exists, open or archived, and the record of its archive when it has one. */
|
|
6006
6398
|
async assertChannelExists(ref) {
|
|
6007
6399
|
const parsed = parseChannelRef(ref);
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6400
|
+
const place = parsed.project === null ? await this.society() : await this.readProject(parsed.project);
|
|
6401
|
+
if (place.channels.includes(parsed.channel)) {
|
|
6402
|
+
return void 0;
|
|
6403
|
+
}
|
|
6404
|
+
const archived = place.archivedChannels.find((each) => each.name === parsed.channel);
|
|
6405
|
+
if (archived === void 0) {
|
|
6406
|
+
throw new BoardError("NOT_FOUND", parsed.project === null ? `society channel ${ref} not found` : `channel ${ref} not found`);
|
|
6014
6407
|
}
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6408
|
+
return archived;
|
|
6409
|
+
}
|
|
6410
|
+
/** Whether a channel exists and is not archived, its conversations still going. */
|
|
6411
|
+
async channelOpen(ref) {
|
|
6412
|
+
try {
|
|
6413
|
+
return await this.assertChannelExists(ref) === void 0;
|
|
6414
|
+
} catch {
|
|
6415
|
+
return false;
|
|
6018
6416
|
}
|
|
6019
6417
|
}
|
|
6020
|
-
/**
|
|
6418
|
+
/**
|
|
6419
|
+
* A channel that takes new posts, threads, tasks, and followers: it exists, it is not archived,
|
|
6420
|
+
* and its project is not archived.
|
|
6421
|
+
*/
|
|
6021
6422
|
async assertChannelOpen(ref) {
|
|
6022
|
-
await this.assertChannelExists(ref)
|
|
6423
|
+
if (await this.assertChannelExists(ref) !== void 0) {
|
|
6424
|
+
throw new BoardError("INVALID_STATE", `channel ${ref} is archived`);
|
|
6425
|
+
}
|
|
6023
6426
|
const { project: project2 } = parseChannelRef(ref);
|
|
6024
6427
|
if (project2 !== null) {
|
|
6025
6428
|
await this.readActiveProject(project2);
|
|
@@ -6119,6 +6522,10 @@ ${charter.purpose}
|
|
|
6119
6522
|
if (outcome === "approved") {
|
|
6120
6523
|
await this.validateProvision(proposal2.kind, proposal2.charter);
|
|
6121
6524
|
}
|
|
6525
|
+
this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn2) => turn2.thread === proposalId);
|
|
6526
|
+
if (outcome === "approved" && proposal2.kind === "archive") {
|
|
6527
|
+
await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(proposal2.charter).project);
|
|
6528
|
+
}
|
|
6122
6529
|
const ts = this.now().toISOString();
|
|
6123
6530
|
const decision = DecisionSchema.parse({
|
|
6124
6531
|
id: this.newId(),
|
|
@@ -6519,6 +6926,14 @@ channelCommand.command("add <ref>").description("Open a channel: <name> for the
|
|
|
6519
6926
|
});
|
|
6520
6927
|
print({ channel: added }, () => `Channel ${added} opened`);
|
|
6521
6928
|
});
|
|
6929
|
+
channelCommand.command("archive <ref>").description("Archive a channel whose workstream is done, once no task filed there is in play").requiredOption("--reason <text>", "why its work is done").option("--as <agent>", "act as this agent instead of the user").action(async (ref, opts) => {
|
|
6930
|
+
const board = await open();
|
|
6931
|
+
const archived = await board.archiveChannel(await actorFor(board, opts.as), {
|
|
6932
|
+
channel: ref,
|
|
6933
|
+
reason: opts.reason
|
|
6934
|
+
});
|
|
6935
|
+
print(archived, () => `Channel ${ref} archived: ${archived.reason}`);
|
|
6936
|
+
});
|
|
6522
6937
|
program.command("signals").description("Recent operations signals, oldest first").option("--limit <n>", "how many", "50").action(async (opts) => {
|
|
6523
6938
|
const board = await open();
|
|
6524
6939
|
const signals = await board.listSignals(Number(opts.limit));
|
|
@@ -6540,18 +6955,19 @@ program.command("ask <text>").description("Post to the society's general channel
|
|
|
6540
6955
|
print(message, () => `Posted ${message.id} to general; the concierge wakes on it`);
|
|
6541
6956
|
});
|
|
6542
6957
|
var task = program.command("task").description("Manage tasks");
|
|
6543
|
-
task.command("create <project> <title>").description("Create a task with a plan, or with one stage anyone in the project may take").option("--body <text>", "task body", "").option("--parent <id>", "parent task id").option("--plan <json>", `stages, for example '[{"name":"analysis","role":"analyst"}]'`).option("--as <agent>", "act as this agent instead of the user").action(async (projectSlug, title, opts) => {
|
|
6958
|
+
task.command("create <project> <title>").description("Create a task with a plan, or with one stage anyone in the project may take").option("--body <text>", "task body", "").option("--parent <id>", "parent task id").option("--plan <json>", `stages, for example '[{"name":"analysis","role":"analyst"}]'`).option("--channel <name>", "the project's channel to file it in, such as a release's", "general").option("--as <agent>", "act as this agent instead of the user").action(async (projectSlug, title, opts) => {
|
|
6544
6959
|
const board = await open();
|
|
6545
6960
|
const who = await actorFor(board, opts.as);
|
|
6546
6961
|
const created = await board.createTask(who, {
|
|
6547
6962
|
project: projectSlug,
|
|
6548
6963
|
title,
|
|
6549
6964
|
body: opts.body,
|
|
6965
|
+
channel: opts.channel,
|
|
6550
6966
|
...opts.parent === void 0 ? {} : { parent_id: opts.parent },
|
|
6551
6967
|
...opts.plan === void 0 ? {} : { stages: parsePlan(opts.plan) }
|
|
6552
6968
|
});
|
|
6553
6969
|
print(created, () => [
|
|
6554
|
-
`Task ${created.id} created in ${created.project}: ${created.title}`,
|
|
6970
|
+
`Task ${created.id} created in ${created.project}/${created.channel}: ${created.title}`,
|
|
6555
6971
|
describePlan(created)
|
|
6556
6972
|
].join("\n"));
|
|
6557
6973
|
});
|