@kubitnodes/stellaris 0.3.20 → 0.3.21
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 +182 -56
- package/dist/server/index.js +281 -85
- package/package.json +1 -1
- package/web/dist/assets/BackLink-23Q1JvmZ.js +1 -0
- package/web/dist/assets/{ChannelForms-BYrcUldk.js → ChannelForms-0Cwh59E7.js} +1 -1
- package/web/dist/assets/{ChannelView-GJ_DusLQ.js → ChannelView-CqWV1zza.js} +2 -2
- package/web/dist/assets/{CitizenView-Cx7IYSRr.js → CitizenView-s5OdjXmB.js} +2 -2
- package/web/dist/assets/{CitizensView-IKH7_yNQ.js → CitizensView-r6SZLnVl.js} +1 -1
- package/web/dist/assets/{CliIcon-vSuwLv2K.js → CliIcon-BOSygXeI.js} +1 -1
- package/web/dist/assets/{CronsView-DJbi8Qhx.js → CronsView-Y-bAE0l2.js} +1 -1
- package/web/dist/assets/{KnowledgeView-CoYOG9tI.js → KnowledgeView-CI4PJWwm.js} +1 -1
- package/web/dist/assets/{Markdown-YC-fbHiE.js → Markdown-DyckB74r.js} +4 -4
- package/web/dist/assets/{MessageItem-D7MUCA_T.js → MessageItem-DI05jbrI.js} +1 -1
- package/web/dist/assets/{MetricsView-CE94VW0I.js → MetricsView-DKh-lEW5.js} +1 -1
- package/web/dist/assets/{NeedsYouView-Otnm3lcG.js → NeedsYouView-C6FfiTsg.js} +1 -1
- package/web/dist/assets/{Overview-B8f6rLb2.js → Overview-D5RvWBg5.js} +1 -1
- package/web/dist/assets/{Pane-_D7n5uKH.js → Pane-BendOaje.js} +1 -1
- package/web/dist/assets/{Picker-DUiiQMFN.js → Picker-BuVnakvi.js} +1 -1
- package/web/dist/assets/{ProjectView-D_ACOmEd.js → ProjectView-blBEh70W.js} +1 -1
- package/web/dist/assets/ProposalView-CMntpKTc.js +2 -0
- package/web/dist/assets/{ProposalsView-BovF6CMD.js → ProposalsView-CVUAV2Mm.js} +1 -1
- package/web/dist/assets/{RunnersView-B2_uCVHC.js → RunnersView-B-wuWVgB.js} +1 -1
- package/web/dist/assets/{SocietyView-Bf0iX8AY.js → SocietyView-CaVbfJWz.js} +1 -1
- package/web/dist/assets/{SubjectThread-B_Vmgs3d.js → SubjectThread-Bd_-sDhJ.js} +1 -1
- package/web/dist/assets/{TaskFileView-BzO0gjE6.js → TaskFileView-CO4Y97OT.js} +2 -2
- package/web/dist/assets/{TaskView-DB_Nji99.js → TaskView-DRZoHALo.js} +2 -2
- package/web/dist/assets/{TasksView-Bz36YYWq.js → TasksView-CQVQo3zF.js} +1 -1
- package/web/dist/assets/{ThreadForms-1mbMZsbv.js → ThreadForms-CUSeWu0c.js} +1 -1
- package/web/dist/assets/{ThreadItem-QJSA92C4.js → ThreadItem-BPYYXh3V.js} +1 -1
- package/web/dist/assets/{ThreadView-BECLqt5B.js → ThreadView-BcI2i6FI.js} +1 -1
- package/web/dist/assets/{citizen-DVFr5T6R.js → citizen-Eff1h9rA.js} +1 -1
- package/web/dist/assets/{compose-_6feqP1t.js → compose-D73Agshx.js} +1 -1
- package/web/dist/assets/{format-DxXtbdNM.js → format-fw0G_Vff.js} +1 -1
- package/web/dist/assets/governance-sRsfOw52.js +1 -0
- package/web/dist/assets/index-CYiok0pT.js +23 -0
- package/web/dist/assets/link-Cl7vKbOz.js +1 -0
- package/web/dist/assets/{tasks-B6I9DJ8O.js → tasks-CyXIxoAy.js} +1 -1
- package/web/dist/assets/useMatch-D-tEHQEw.js +1 -0
- package/web/dist/assets/{useStickyScroll-Ca7sWbGH.js → useStickyScroll-Bn4YyEXL.js} +1 -1
- package/web/dist/index.html +8 -8
- package/web/dist/assets/BackLink-BjfX2KZl.js +0 -1
- package/web/dist/assets/ProposalView-4d0U3nEb.js +0 -2
- package/web/dist/assets/governance-C9xH4QTm.js +0 -1
- package/web/dist/assets/index-BEVCPm4C.js +0 -23
- package/web/dist/assets/link-CTjFMNoY.js +0 -1
- package/web/dist/assets/useMatch-B3RzAe8y.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -146,7 +146,7 @@ var SEED_ROLES = [
|
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
name: "steward",
|
|
149
|
-
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
|
|
149
|
+
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, by a reallocation of its replicas, backlog threshold, wakes, or verbs. Never approves its own proposals; hiring, roles, retirements, and reallocations that change a role's verbs or replicas are decided by the user, and channels, skills, and other reallocations the steward may decide. Curates society knowledge and skills.",
|
|
150
150
|
verbs: [...MEMBER_VERBS, ...GOVERNANCE_VERBS, ...PLANNING_VERBS, ...CURATING_VERBS],
|
|
151
151
|
wakeTriggers: ["ops_event", "heartbeat"],
|
|
152
152
|
maxReplicas: 1,
|
|
@@ -631,9 +631,54 @@ var ChannelProposalSchema = z4.object({
|
|
|
631
631
|
name: NameSchema,
|
|
632
632
|
purpose: z4.string().min(1)
|
|
633
633
|
});
|
|
634
|
+
var RoleChangesSchema = z4.strictObject({
|
|
635
|
+
maxReplicas: z4.number().int().min(1).optional(),
|
|
636
|
+
backlogThreshold: z4.number().positive().optional(),
|
|
637
|
+
wakeTriggers: z4.array(CharterTriggerSchema).optional(),
|
|
638
|
+
verbs: z4.array(VerbNameSchema).optional()
|
|
639
|
+
}).refine((changes) => Object.values(changes).some((value) => value !== void 0), {
|
|
640
|
+
message: "a reallocation changes at least one of maxReplicas, backlogThreshold, wakeTriggers, verbs"
|
|
641
|
+
});
|
|
634
642
|
var ReallocationProposalSchema = z4.object({
|
|
643
|
+
role: NameSchema,
|
|
644
|
+
changes: RoleChangesSchema,
|
|
635
645
|
description: z4.string().min(1)
|
|
636
646
|
});
|
|
647
|
+
var LegacyReallocationSchema = z4.object({ description: z4.string().min(1) });
|
|
648
|
+
function applyRoleChanges(role, changes) {
|
|
649
|
+
return {
|
|
650
|
+
...role,
|
|
651
|
+
...changes.maxReplicas === void 0 ? {} : { maxReplicas: changes.maxReplicas },
|
|
652
|
+
...changes.backlogThreshold === void 0 ? {} : { backlogThreshold: changes.backlogThreshold },
|
|
653
|
+
...changes.wakeTriggers === void 0 ? {} : { wakeTriggers: [...changes.wakeTriggers] },
|
|
654
|
+
...changes.verbs === void 0 ? {} : { verbs: [...changes.verbs] }
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function listedOrNone(values) {
|
|
658
|
+
return values.length === 0 ? "none" : values.join(", ");
|
|
659
|
+
}
|
|
660
|
+
function roleChangeLines(role, changes) {
|
|
661
|
+
const lines = [];
|
|
662
|
+
if (changes.maxReplicas !== void 0 && changes.maxReplicas !== role.maxReplicas) {
|
|
663
|
+
lines.push(`maxReplicas ${role.maxReplicas} \u2192 ${changes.maxReplicas}`);
|
|
664
|
+
}
|
|
665
|
+
if (changes.backlogThreshold !== void 0 && changes.backlogThreshold !== role.backlogThreshold) {
|
|
666
|
+
lines.push(`backlogThreshold ${role.backlogThreshold} \u2192 ${changes.backlogThreshold}`);
|
|
667
|
+
}
|
|
668
|
+
const wakes = changes.wakeTriggers;
|
|
669
|
+
if (wakes !== void 0 && (wakes.length !== role.wakeTriggers.length || wakes.some((trigger) => !role.wakeTriggers.includes(trigger)))) {
|
|
670
|
+
lines.push(`wakeTriggers ${listedOrNone(role.wakeTriggers)} \u2192 ${listedOrNone(wakes)}`);
|
|
671
|
+
}
|
|
672
|
+
if (changes.verbs !== void 0) {
|
|
673
|
+
const next = changes.verbs;
|
|
674
|
+
const added = next.filter((verb) => !role.verbs.includes(verb)).map((verb) => `+${verb}`);
|
|
675
|
+
const removed = role.verbs.filter((verb) => !next.includes(verb)).map((verb) => `\u2212${verb}`);
|
|
676
|
+
if (added.length + removed.length > 0) {
|
|
677
|
+
lines.push(`verbs ${[...added, ...removed].join(" ")}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return lines;
|
|
681
|
+
}
|
|
637
682
|
var RetirementProposalSchema = z4.object({
|
|
638
683
|
agent: NameSchema,
|
|
639
684
|
reason: z4.string().min(1)
|
|
@@ -665,6 +710,32 @@ var ROLE_KIND_APPROVERS = {
|
|
|
665
710
|
reallocation: [USER_ROLE, "steward"],
|
|
666
711
|
skill: [USER_ROLE, "steward"]
|
|
667
712
|
};
|
|
713
|
+
var USER_DECIDED_ROLE_CHANGES = ["verbs", "maxReplicas"];
|
|
714
|
+
function proposalApprovers(kind, charter) {
|
|
715
|
+
if (kind === "reallocation") {
|
|
716
|
+
const parsed = ReallocationProposalSchema.safeParse(charter);
|
|
717
|
+
if (parsed.success && USER_DECIDED_ROLE_CHANGES.some((field) => parsed.data.changes[field] !== void 0)) {
|
|
718
|
+
return [USER_ROLE];
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return ROLE_KIND_APPROVERS[kind];
|
|
722
|
+
}
|
|
723
|
+
function describeRoleChanges(changes) {
|
|
724
|
+
const parts = [];
|
|
725
|
+
if (changes.maxReplicas !== void 0) {
|
|
726
|
+
parts.push(`up to ${changes.maxReplicas} per project`);
|
|
727
|
+
}
|
|
728
|
+
if (changes.backlogThreshold !== void 0) {
|
|
729
|
+
parts.push(`backlog threshold ${changes.backlogThreshold}`);
|
|
730
|
+
}
|
|
731
|
+
if (changes.wakeTriggers !== void 0) {
|
|
732
|
+
parts.push(changes.wakeTriggers.length === 0 ? "no ambient wakes" : `wakes on ${changes.wakeTriggers.join(", ")}`);
|
|
733
|
+
}
|
|
734
|
+
if (changes.verbs !== void 0) {
|
|
735
|
+
parts.push(`${changes.verbs.length} verbs`);
|
|
736
|
+
}
|
|
737
|
+
return parts.join(", ");
|
|
738
|
+
}
|
|
668
739
|
function describeCharter(kind, charter) {
|
|
669
740
|
switch (kind) {
|
|
670
741
|
case "member": {
|
|
@@ -696,9 +767,14 @@ function describeCharter(kind, charter) {
|
|
|
696
767
|
}
|
|
697
768
|
case "reallocation": {
|
|
698
769
|
const parsed = ReallocationProposalSchema.safeParse(charter);
|
|
699
|
-
if (
|
|
770
|
+
if (parsed.success) {
|
|
771
|
+
const { role, changes, description } = parsed.data;
|
|
772
|
+
return `reallocation of role ${role} (${describeRoleChanges(changes)}): ${description}`;
|
|
773
|
+
}
|
|
774
|
+
const legacy = LegacyReallocationSchema.safeParse(charter);
|
|
775
|
+
if (!legacy.success)
|
|
700
776
|
break;
|
|
701
|
-
return `reallocation: ${
|
|
777
|
+
return `reallocation: ${legacy.data.description}`;
|
|
702
778
|
}
|
|
703
779
|
case "skill": {
|
|
704
780
|
const parsed = SkillProposalSchema.safeParse(charter);
|
|
@@ -1007,7 +1083,7 @@ var VERB_DESCRIPTIONS = {
|
|
|
1007
1083
|
create_task: "Create a task in a project with its plan: stages of {name, role or agent, gate}. Without stages it gets one stage, work, that anyone in the project may take. File it in a channel of the project, such as a release's, or general by default; its thread opens there with it, under the task's id, for everything said about the work.",
|
|
1008
1084
|
claim_task: "Hold the task's current stage. Claims are leases renewed by every turn that touches the task.",
|
|
1009
1085
|
release_task: "Let go of the stage you hold so someone else can take it.",
|
|
1010
|
-
update_task: "Move a task back to an earlier stage, abandon it, set
|
|
1086
|
+
update_task: "Move a task back to an earlier stage, abandon it, set the tasks it is blocked by (blocked_by: its stage wakes nobody until they have all ended), or link its pull request (pull_request: url, with the merge commit's merge_subject and merge_body), which a ghpr task needs before its last stage completes. A note is posted to the task's thread.",
|
|
1011
1087
|
get_task: "Read a task with its plan, its brief, and the messages of its thread.",
|
|
1012
1088
|
subscribe: "Subscribe to a channel. Subscriptions feed your digest; they never wake you.",
|
|
1013
1089
|
unsubscribe: "Unsubscribe from a channel.",
|
|
@@ -1869,7 +1945,7 @@ var RunnerConfigSchema = z11.object({
|
|
|
1869
1945
|
});
|
|
1870
1946
|
|
|
1871
1947
|
// packages/shared/dist/version.js
|
|
1872
|
-
var STELLARIS_VERSION = "0.3.
|
|
1948
|
+
var STELLARIS_VERSION = "0.3.21";
|
|
1873
1949
|
|
|
1874
1950
|
// packages/board-core/dist/errors.js
|
|
1875
1951
|
var BoardError = class extends Error {
|
|
@@ -4606,18 +4682,16 @@ ${DASHBOARD_LINE}
|
|
|
4606
4682
|
async readDigest(actor, input = {}) {
|
|
4607
4683
|
const args = VerbInputs.read_inbox.parse(input);
|
|
4608
4684
|
await this.authorize(actor, "read_inbox");
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
return { messages: filed.map((entry) => entry.message), cursor };
|
|
4620
|
-
});
|
|
4685
|
+
const { cursors: stored, upto } = await this.digestSnapshot(actor.name);
|
|
4686
|
+
const from = args.since_cursor === void 0 ? stored : { ...NO_CURSORS, digest: args.since_cursor };
|
|
4687
|
+
const conversation = actor.scope === void 0 ? void 0 : { scope: actor.scope, part: conversationPart(actor) ?? null };
|
|
4688
|
+
const filed = (await this.digestSince(actor, from, upto, conversation)).slice(0, args.limit);
|
|
4689
|
+
if (args.advance) {
|
|
4690
|
+
await this.mutex.run(() => this.advanceCursors(actor.name, filed));
|
|
4691
|
+
}
|
|
4692
|
+
const last = filed.at(-1)?.message.id;
|
|
4693
|
+
const cursor = last ?? (conversation === void 0 ? from.digest : cursorOf(from, conversation.scope, conversation.part));
|
|
4694
|
+
return { messages: filed.map((entry) => entry.message), cursor };
|
|
4621
4695
|
}
|
|
4622
4696
|
/**
|
|
4623
4697
|
* How many unread digest messages a member has in each conversation: a scope's home, for its
|
|
@@ -4629,28 +4703,39 @@ ${DASHBOARD_LINE}
|
|
|
4629
4703
|
* whose signals that call for action wake their readers directly.
|
|
4630
4704
|
*/
|
|
4631
4705
|
async unreadByConversation(actor) {
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
}
|
|
4639
|
-
const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
|
|
4640
|
-
if (posted !== scope && scope !== SOCIETY_SCOPE) {
|
|
4641
|
-
continue;
|
|
4642
|
-
}
|
|
4643
|
-
const key = conversationKey(scope, part);
|
|
4644
|
-
const entry = counts.get(key) ?? {
|
|
4645
|
-
scope,
|
|
4646
|
-
thread: threadOfPart(part) ?? null,
|
|
4647
|
-
channel: channelOfPart(part) ?? null,
|
|
4648
|
-
count: 0
|
|
4649
|
-
};
|
|
4650
|
-
counts.set(key, { ...entry, count: entry.count + 1 });
|
|
4706
|
+
const { cursors, upto } = await this.digestSnapshot(actor.name);
|
|
4707
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4708
|
+
const unscoped = { name: actor.name, role: actor.role };
|
|
4709
|
+
for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
|
|
4710
|
+
if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
|
|
4711
|
+
continue;
|
|
4651
4712
|
}
|
|
4652
|
-
|
|
4653
|
-
|
|
4713
|
+
const posted = parseChannelRef(message.channel).project ?? SOCIETY_SCOPE;
|
|
4714
|
+
if (posted !== scope && scope !== SOCIETY_SCOPE) {
|
|
4715
|
+
continue;
|
|
4716
|
+
}
|
|
4717
|
+
const key = conversationKey(scope, part);
|
|
4718
|
+
const entry = counts.get(key) ?? {
|
|
4719
|
+
scope,
|
|
4720
|
+
thread: threadOfPart(part) ?? null,
|
|
4721
|
+
channel: channelOfPart(part) ?? null,
|
|
4722
|
+
count: 0
|
|
4723
|
+
};
|
|
4724
|
+
counts.set(key, { ...entry, count: entry.count + 1 });
|
|
4725
|
+
}
|
|
4726
|
+
return [...counts.values()];
|
|
4727
|
+
}
|
|
4728
|
+
/**
|
|
4729
|
+
* A reader's cursors and the id below which every message is already on disk, taken under the
|
|
4730
|
+
* mutex, which a digest scan then leaves: messages carry ids from the same monotonic source and are
|
|
4731
|
+
* written under the mutex, so the scan, which counts nothing newer than the mark, cannot pass over
|
|
4732
|
+
* one written while it reads, and a post never waits for a scan.
|
|
4733
|
+
*/
|
|
4734
|
+
digestSnapshot(agent) {
|
|
4735
|
+
return this.mutex.run(async () => ({
|
|
4736
|
+
cursors: await this.readCursors(agent),
|
|
4737
|
+
upto: this.newId()
|
|
4738
|
+
}));
|
|
4654
4739
|
}
|
|
4655
4740
|
async readCursors(agent) {
|
|
4656
4741
|
const file = this.paths.agentCursors(agent);
|
|
@@ -4692,9 +4777,10 @@ ${DASHBOARD_LINE}
|
|
|
4692
4777
|
* by the user wherever it is. With a conversation, only what is filed there: for a thread, every
|
|
4693
4778
|
* message of the thread, since the conversation is about it; for a channel's, that channel's
|
|
4694
4779
|
* top-level messages; for a scope's home, general's and those of a project the member is not in,
|
|
4695
|
-
* since each thread and each other channel is a conversation of its own.
|
|
4780
|
+
* since each thread and each other channel is a conversation of its own. Nothing newer than
|
|
4781
|
+
* `upto` counts (see `digestSnapshot`).
|
|
4696
4782
|
*/
|
|
4697
|
-
async digestSince(actor, cursors, conversation) {
|
|
4783
|
+
async digestSince(actor, cursors, upto, conversation) {
|
|
4698
4784
|
const conversationThread = threadOfPart(conversation?.part);
|
|
4699
4785
|
if (conversation !== void 0 && conversationThread !== void 0) {
|
|
4700
4786
|
const { scope, part } = conversation;
|
|
@@ -4702,7 +4788,7 @@ ${DASHBOARD_LINE}
|
|
|
4702
4788
|
if (found === null) {
|
|
4703
4789
|
return [];
|
|
4704
4790
|
}
|
|
4705
|
-
const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads, conversationThread), cursorOf(cursors, scope, part));
|
|
4791
|
+
const messages = await this.readMessagesIn(this.paths.threadMessages(found.threads, conversationThread), cursorOf(cursors, scope, part), upto);
|
|
4706
4792
|
return messages.filter((message) => message.author !== actor.name).map((message) => ({ scope, part, message })).toSorted(byId);
|
|
4707
4793
|
}
|
|
4708
4794
|
const agent = await this.readAgent(actor.name);
|
|
@@ -4720,7 +4806,7 @@ ${DASHBOARD_LINE}
|
|
|
4720
4806
|
];
|
|
4721
4807
|
const known = starts.filter((start) => start !== null);
|
|
4722
4808
|
const since = known.length < starts.length ? null : known.toSorted((a, b) => a < b ? -1 : a > b ? 1 : 0)[0] ?? null;
|
|
4723
|
-
for await (const message of this.iterateMessages(since)) {
|
|
4809
|
+
for await (const message of this.iterateMessages(since, void 0, upto)) {
|
|
4724
4810
|
const filedIn = wakeScope(agent, parseChannelRef(message.channel).project);
|
|
4725
4811
|
const thread = message.thread ?? null;
|
|
4726
4812
|
const asked = message.mentions.includes(actor.name) || frontDesk && message.author === USER_NAME;
|
|
@@ -5844,6 +5930,9 @@ ${args.summary}`);
|
|
|
5844
5930
|
}
|
|
5845
5931
|
if (args.blocked_by !== void 0) {
|
|
5846
5932
|
for (const id of args.blocked_by) {
|
|
5933
|
+
if (id === current.id) {
|
|
5934
|
+
throw new BoardError("VALIDATION", `task ${current.id} cannot be blocked by itself`);
|
|
5935
|
+
}
|
|
5847
5936
|
await this.findTask(id);
|
|
5848
5937
|
}
|
|
5849
5938
|
next = { ...next, blockedBy: [...args.blocked_by] };
|
|
@@ -5880,6 +5969,7 @@ ${args.summary}`);
|
|
|
5880
5969
|
from: current.status,
|
|
5881
5970
|
to: task.status,
|
|
5882
5971
|
note: args.note ?? null,
|
|
5972
|
+
...args.blocked_by === void 0 ? {} : { blockedBy: task.blockedBy, previousBlockedBy: current.blockedBy },
|
|
5883
5973
|
...args.pull_request === void 0 ? {} : { pullRequest: task.pullRequest?.url ?? null }
|
|
5884
5974
|
});
|
|
5885
5975
|
}
|
|
@@ -7178,12 +7268,37 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7178
7268
|
case "archive":
|
|
7179
7269
|
await this.validateArchive(ArchiveProposalSchema.parse(charter).project);
|
|
7180
7270
|
return;
|
|
7181
|
-
case "reallocation":
|
|
7271
|
+
case "reallocation": {
|
|
7272
|
+
const parsed = ReallocationProposalSchema.safeParse(charter);
|
|
7273
|
+
if (parsed.success) {
|
|
7274
|
+
await this.reallocatedRole(parsed.data);
|
|
7275
|
+
} else {
|
|
7276
|
+
LegacyReallocationSchema.parse(charter);
|
|
7277
|
+
}
|
|
7182
7278
|
return;
|
|
7279
|
+
}
|
|
7183
7280
|
default:
|
|
7184
7281
|
return;
|
|
7185
7282
|
}
|
|
7186
7283
|
}
|
|
7284
|
+
/**
|
|
7285
|
+
* The role as a reallocation would leave it, with what changes: refused for the user's charter,
|
|
7286
|
+
* a role that does not exist, a charter the changes would make invalid, and changes the role
|
|
7287
|
+
* has already.
|
|
7288
|
+
*/
|
|
7289
|
+
async reallocatedRole(reallocation) {
|
|
7290
|
+
if (reallocation.role === USER_ROLE) {
|
|
7291
|
+
throw new BoardError("FORBIDDEN", "the user charter is not subject to proposals");
|
|
7292
|
+
}
|
|
7293
|
+
const current = await this.readRole(reallocation.role);
|
|
7294
|
+
const next = RoleCharterSchema.parse(applyRoleChanges(current, reallocation.changes));
|
|
7295
|
+
await this.validateRole(next);
|
|
7296
|
+
const changed = roleChangeLines(current, reallocation.changes);
|
|
7297
|
+
if (changed.length === 0) {
|
|
7298
|
+
throw new BoardError("VALIDATION", `role ${current.name} has these values already; the reallocation changes nothing`);
|
|
7299
|
+
}
|
|
7300
|
+
return { next, changed };
|
|
7301
|
+
}
|
|
7187
7302
|
async executeProvision(by, proposal) {
|
|
7188
7303
|
const meta = { proposalId: proposal.id };
|
|
7189
7304
|
switch (proposal.kind) {
|
|
@@ -7220,8 +7335,15 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7220
7335
|
const { members } = await this.archiveUnlocked(by, input.project, input.reason, meta);
|
|
7221
7336
|
return { project: input.project, members };
|
|
7222
7337
|
}
|
|
7223
|
-
case "reallocation":
|
|
7224
|
-
|
|
7338
|
+
case "reallocation": {
|
|
7339
|
+
const parsed = ReallocationProposalSchema.safeParse(proposal.charter);
|
|
7340
|
+
if (!parsed.success) {
|
|
7341
|
+
return void 0;
|
|
7342
|
+
}
|
|
7343
|
+
const { next, changed } = await this.reallocatedRole(parsed.data);
|
|
7344
|
+
await this.writeRoleUnlocked(by, next, meta);
|
|
7345
|
+
return { role: next.name, changed };
|
|
7346
|
+
}
|
|
7225
7347
|
default:
|
|
7226
7348
|
return void 0;
|
|
7227
7349
|
}
|
|
@@ -7921,7 +8043,7 @@ ${charter.purpose}
|
|
|
7921
8043
|
if (filed.proposedBy === actor.name) {
|
|
7922
8044
|
throw new BoardError("FORBIDDEN", "a proposer never decides its own proposal");
|
|
7923
8045
|
}
|
|
7924
|
-
const approvers =
|
|
8046
|
+
const approvers = proposalApprovers(filed.kind, filed.charter);
|
|
7925
8047
|
if (!approvers.includes(actor.role)) {
|
|
7926
8048
|
throw new BoardError("FORBIDDEN", `a ${filed.kind} proposal requires one of: ${approvers.join(", ")}`);
|
|
7927
8049
|
}
|
|
@@ -8004,7 +8126,7 @@ ${charter.purpose}
|
|
|
8004
8126
|
}
|
|
8005
8127
|
if (subject?.kind === "proposal") {
|
|
8006
8128
|
const proposal = await this.readProposal(subject.id);
|
|
8007
|
-
if (proposal.proposedBy === actor.name || proposal.decidedBy === actor.name ||
|
|
8129
|
+
if (proposal.proposedBy === actor.name || proposal.decidedBy === actor.name || proposalApprovers(proposal.kind, proposal.charter).includes(actor.role)) {
|
|
8008
8130
|
return true;
|
|
8009
8131
|
}
|
|
8010
8132
|
return (await this.readRole(actor.role)).wakeTriggers.includes(OPS_WAKE_TRIGGER);
|
|
@@ -8061,11 +8183,12 @@ ${charter.purpose}
|
|
|
8061
8183
|
});
|
|
8062
8184
|
await this.endCronsUnlocked(by, `its thread closed: its ${found.thread.subject?.kind ?? "subject"} was ${ended}`, (cron) => cron.thread === subjectId);
|
|
8063
8185
|
}
|
|
8064
|
-
|
|
8186
|
+
/** The messages in a folder after `since` and, when given, up to `upto`, read by name first. */
|
|
8187
|
+
async readMessagesIn(dir, since, upto) {
|
|
8065
8188
|
const messages = [];
|
|
8066
8189
|
for (const file of await listFiles(dir)) {
|
|
8067
8190
|
const id = file.slice(0, 26);
|
|
8068
|
-
if (since !== null && id <= since) {
|
|
8191
|
+
if (since !== null && id <= since || upto !== void 0 && id > upto) {
|
|
8069
8192
|
continue;
|
|
8070
8193
|
}
|
|
8071
8194
|
const doc = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
|
|
@@ -8073,20 +8196,23 @@ ${charter.purpose}
|
|
|
8073
8196
|
}
|
|
8074
8197
|
return messages;
|
|
8075
8198
|
}
|
|
8076
|
-
/**
|
|
8077
|
-
|
|
8199
|
+
/**
|
|
8200
|
+
* Every message newer than `since`, and no newer than `upto` when given, across society
|
|
8201
|
+
* channels, project channels, and threads.
|
|
8202
|
+
*/
|
|
8203
|
+
async *iterateMessages(since, project, upto) {
|
|
8078
8204
|
if (project === void 0) {
|
|
8079
8205
|
for (const channel of await listDirs(this.paths.societyChannels())) {
|
|
8080
|
-
yield* await this.readMessagesIn(this.paths.societyChannel(channel), since);
|
|
8206
|
+
yield* await this.readMessagesIn(this.paths.societyChannel(channel), since, upto);
|
|
8081
8207
|
}
|
|
8082
|
-
yield* this.threadMessagesIn(this.paths.societyThreads(), since);
|
|
8208
|
+
yield* this.threadMessagesIn(this.paths.societyThreads(), since, upto);
|
|
8083
8209
|
}
|
|
8084
8210
|
const projects = project === void 0 ? await listDirs(this.paths.projects()) : [project];
|
|
8085
8211
|
for (const slug of projects) {
|
|
8086
8212
|
for (const channel of await listDirs(this.paths.projectChannels(slug))) {
|
|
8087
|
-
yield* await this.readMessagesIn(this.paths.projectChannel(slug, channel), since);
|
|
8213
|
+
yield* await this.readMessagesIn(this.paths.projectChannel(slug, channel), since, upto);
|
|
8088
8214
|
}
|
|
8089
|
-
yield* this.threadMessagesIn(this.paths.threads(slug), since);
|
|
8215
|
+
yield* this.threadMessagesIn(this.paths.threads(slug), since, upto);
|
|
8090
8216
|
}
|
|
8091
8217
|
}
|
|
8092
8218
|
/** Every folder that holds messages: each channel's and each thread's, in the society and every project. */
|
|
@@ -8106,9 +8232,9 @@ ${charter.purpose}
|
|
|
8106
8232
|
}
|
|
8107
8233
|
}
|
|
8108
8234
|
}
|
|
8109
|
-
async *threadMessagesIn(threads, since) {
|
|
8235
|
+
async *threadMessagesIn(threads, since, upto) {
|
|
8110
8236
|
for (const id of await listDirs(threads)) {
|
|
8111
|
-
yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since);
|
|
8237
|
+
yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since, upto);
|
|
8112
8238
|
}
|
|
8113
8239
|
}
|
|
8114
8240
|
};
|
|
@@ -8492,7 +8618,7 @@ var Scheduler = class {
|
|
|
8492
8618
|
return;
|
|
8493
8619
|
}
|
|
8494
8620
|
if (stringOf(payload["thread"]) !== taskId) {
|
|
8495
|
-
await this.wakeHolder(taskId, event.actor, now);
|
|
8621
|
+
await this.wakeHolder(taskId, event.actor, event.actor, now);
|
|
8496
8622
|
}
|
|
8497
8623
|
await this.pruneStageWakes(taskId);
|
|
8498
8624
|
return;
|
|
@@ -8518,6 +8644,7 @@ var Scheduler = class {
|
|
|
8518
8644
|
if (creator !== null && creator !== event.actor) {
|
|
8519
8645
|
await this.wakeCreator(taskId, project, creator, event.actor, now);
|
|
8520
8646
|
}
|
|
8647
|
+
await this.wakeDependents(taskId, event.actor, now);
|
|
8521
8648
|
const threaded = await this.board.readThread(taskId).then(() => true, () => false);
|
|
8522
8649
|
if (!threaded) {
|
|
8523
8650
|
this.sweep({ kind: "thread", id: taskId });
|
|
@@ -8537,12 +8664,17 @@ var Scheduler = class {
|
|
|
8537
8664
|
}
|
|
8538
8665
|
case "task.updated": {
|
|
8539
8666
|
const taskId = stringOf(payload["taskId"]);
|
|
8540
|
-
if (taskId
|
|
8667
|
+
if (taskId === null) {
|
|
8668
|
+
return;
|
|
8669
|
+
}
|
|
8670
|
+
if (payload["to"] === "abandoned") {
|
|
8541
8671
|
delete this.state.releaseCounts[taskId];
|
|
8542
8672
|
await this.queueClosing(taskId);
|
|
8673
|
+
await this.wakeDependents(taskId, event.actor, now);
|
|
8543
8674
|
}
|
|
8544
|
-
|
|
8545
|
-
|
|
8675
|
+
await this.pruneStageWakes(taskId);
|
|
8676
|
+
if (Array.isArray(payload["blockedBy"]) && (await this.blockersInPlay(stringArray(payload["previousBlockedBy"]))).length > 0) {
|
|
8677
|
+
await this.wakeUnblocked(taskId, event.actor, now);
|
|
8546
8678
|
}
|
|
8547
8679
|
return;
|
|
8548
8680
|
}
|
|
@@ -8713,9 +8845,10 @@ var Scheduler = class {
|
|
|
8713
8845
|
}
|
|
8714
8846
|
/**
|
|
8715
8847
|
* Entering a stage wakes its named citizen, else its last holder when work returns to it, else
|
|
8716
|
-
* the project's members of its role, else every member of the project who may hold it.
|
|
8848
|
+
* the project's members of its role, else every member of the project who may hold it. A stage
|
|
8849
|
+
* of a blocked task wakes nobody until its last blocker ends (`wakeUnblocked`).
|
|
8717
8850
|
*/
|
|
8718
|
-
async wakeStage(taskId, from, now,
|
|
8851
|
+
async wakeStage(taskId, from, now, options = {}) {
|
|
8719
8852
|
let task;
|
|
8720
8853
|
try {
|
|
8721
8854
|
task = (await this.board.findTask(taskId)).task;
|
|
@@ -8726,6 +8859,10 @@ var Scheduler = class {
|
|
|
8726
8859
|
if (stage === void 0 || task.status !== "open" || task.completing) {
|
|
8727
8860
|
return;
|
|
8728
8861
|
}
|
|
8862
|
+
if ((await this.blockersInPlay(task.blockedBy)).length > 0) {
|
|
8863
|
+
return;
|
|
8864
|
+
}
|
|
8865
|
+
const { only, state = "is waiting for you" } = options;
|
|
8729
8866
|
const lastHolder = stage.holders.at(-1);
|
|
8730
8867
|
const targets = stage.agent !== void 0 ? [stage.agent] : lastHolder !== void 0 ? [lastHolder] : stage.role !== void 0 ? (await this.board.membersWithRole(task.project, stage.role)).map((a) => a.name) : (await this.board.projectMembers(task.project)).map((a) => a.name);
|
|
8731
8868
|
for (const name of targets) {
|
|
@@ -8736,7 +8873,7 @@ var Scheduler = class {
|
|
|
8736
8873
|
if (agent === null || !mayHoldStage(agent, task)) {
|
|
8737
8874
|
continue;
|
|
8738
8875
|
}
|
|
8739
|
-
await this.enqueueStage(agent, task, from, now,
|
|
8876
|
+
await this.enqueueStage(agent, task, from, now, state);
|
|
8740
8877
|
}
|
|
8741
8878
|
}
|
|
8742
8879
|
/**
|
|
@@ -8745,7 +8882,7 @@ var Scheduler = class {
|
|
|
8745
8882
|
*/
|
|
8746
8883
|
async wakeNewcomer(name, project, now) {
|
|
8747
8884
|
for (const task of await this.board.openTasks(project).catch(() => [])) {
|
|
8748
|
-
await this.wakeStage(task.id, name, now, name);
|
|
8885
|
+
await this.wakeStage(task.id, name, now, { only: name });
|
|
8749
8886
|
}
|
|
8750
8887
|
}
|
|
8751
8888
|
/**
|
|
@@ -8753,7 +8890,7 @@ var Scheduler = class {
|
|
|
8753
8890
|
* worked in that conversation, which nothing else would start before the next heartbeat. Turns
|
|
8754
8891
|
* of one conversation run one at a time, so the turn this queues is the one that sees the claim.
|
|
8755
8892
|
*/
|
|
8756
|
-
async wakeHolder(taskId, holder, now) {
|
|
8893
|
+
async wakeHolder(taskId, holder, from, now, state = "is yours; its work happens here") {
|
|
8757
8894
|
let task;
|
|
8758
8895
|
try {
|
|
8759
8896
|
task = (await this.board.findTask(taskId)).task;
|
|
@@ -8764,12 +8901,66 @@ var Scheduler = class {
|
|
|
8764
8901
|
if (agent === null || task.status !== "claimed" || task.claimedBy !== holder) {
|
|
8765
8902
|
return;
|
|
8766
8903
|
}
|
|
8767
|
-
const key = await this.enqueueStage(agent, task,
|
|
8904
|
+
const key = await this.enqueueStage(agent, task, from, now, state);
|
|
8768
8905
|
const item = key === null ? void 0 : this.pending.get(key);
|
|
8769
8906
|
if (item?.stageWakes !== void 0) {
|
|
8770
8907
|
item.holder = true;
|
|
8771
8908
|
}
|
|
8772
8909
|
}
|
|
8910
|
+
/**
|
|
8911
|
+
* The ids among these that name a task still open or claimed, a landing one included. A task
|
|
8912
|
+
* blocked by any of them waits for them rather than for a holder: its stage wakes nobody, by
|
|
8913
|
+
* coming up or on a heartbeat, until the last one ends.
|
|
8914
|
+
*/
|
|
8915
|
+
async blockersInPlay(ids) {
|
|
8916
|
+
const blocking = [];
|
|
8917
|
+
for (const id of ids) {
|
|
8918
|
+
try {
|
|
8919
|
+
const { task } = await this.board.findTask(id);
|
|
8920
|
+
if (task.status === "open" || task.status === "claimed") {
|
|
8921
|
+
blocking.push(id);
|
|
8922
|
+
}
|
|
8923
|
+
} catch {
|
|
8924
|
+
}
|
|
8925
|
+
}
|
|
8926
|
+
return blocking;
|
|
8927
|
+
}
|
|
8928
|
+
/**
|
|
8929
|
+
* A task's last blocker has ended, or its blockers were changed to none in play: its stage wakes
|
|
8930
|
+
* as on coming up, or its holder in the task's conversation, since nothing woke either for it
|
|
8931
|
+
* while it was blocked.
|
|
8932
|
+
*/
|
|
8933
|
+
async wakeUnblocked(taskId, from, now) {
|
|
8934
|
+
let task;
|
|
8935
|
+
try {
|
|
8936
|
+
task = (await this.board.findTask(taskId)).task;
|
|
8937
|
+
} catch {
|
|
8938
|
+
return;
|
|
8939
|
+
}
|
|
8940
|
+
if (task.completing || (await this.blockersInPlay(task.blockedBy)).length > 0) {
|
|
8941
|
+
return;
|
|
8942
|
+
}
|
|
8943
|
+
if (task.status === "open") {
|
|
8944
|
+
await this.wakeStage(task.id, from, now, {
|
|
8945
|
+
state: "is waiting for you, now that nothing blocks it"
|
|
8946
|
+
});
|
|
8947
|
+
} else if (task.status === "claimed" && task.claimedBy !== void 0) {
|
|
8948
|
+
await this.wakeHolder(task.id, task.claimedBy, from, now, "is yours, and nothing blocks it any more");
|
|
8949
|
+
}
|
|
8950
|
+
}
|
|
8951
|
+
/** A task that ended unblocks every task in play that names it, once nothing else blocks them. */
|
|
8952
|
+
async wakeDependents(taskId, from, now) {
|
|
8953
|
+
for (const project of await this.board.listProjects()) {
|
|
8954
|
+
if (project.archived !== void 0) {
|
|
8955
|
+
continue;
|
|
8956
|
+
}
|
|
8957
|
+
for (const task of await this.board.listTasks(project.slug)) {
|
|
8958
|
+
if (task.blockedBy.includes(taskId) && (task.status === "open" || task.status === "claimed")) {
|
|
8959
|
+
await this.wakeUnblocked(task.id, from, now);
|
|
8960
|
+
}
|
|
8961
|
+
}
|
|
8962
|
+
}
|
|
8963
|
+
}
|
|
8773
8964
|
/** Queues a stage wake in the task's conversation and returns its key, or null for no wake. */
|
|
8774
8965
|
async enqueueStage(agent, task, from, now, state) {
|
|
8775
8966
|
if (agent.status !== "active" || agent.cli === null) {
|
|
@@ -8805,9 +8996,9 @@ var Scheduler = class {
|
|
|
8805
8996
|
}
|
|
8806
8997
|
}
|
|
8807
8998
|
/**
|
|
8808
|
-
* The first of these stage wakes still due: its task waits, unheld, on a stage the
|
|
8809
|
-
* hold, or, with `holder`, the agent holds it, having claimed it outside the task's
|
|
8810
|
-
* A wake for a stage the agent took in the turn the wake waited on is dropped.
|
|
8999
|
+
* The first of these stage wakes still due: its task waits, unheld and unblocked, on a stage the
|
|
9000
|
+
* agent may hold, or, with `holder`, the agent holds it, having claimed it outside the task's
|
|
9001
|
+
* conversation. A wake for a stage the agent took in the turn the wake waited on is dropped.
|
|
8811
9002
|
*/
|
|
8812
9003
|
async stillWaiting(name, wakes, holder) {
|
|
8813
9004
|
const agent = await this.tryReadAgent(name);
|
|
@@ -8823,7 +9014,7 @@ var Scheduler = class {
|
|
|
8823
9014
|
if (task.completing || currentStage(task) === void 0) {
|
|
8824
9015
|
continue;
|
|
8825
9016
|
}
|
|
8826
|
-
if (task.status === "open" && mayHoldStage(agent, task)) {
|
|
9017
|
+
if (task.status === "open" && mayHoldStage(agent, task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
|
|
8827
9018
|
return wake;
|
|
8828
9019
|
}
|
|
8829
9020
|
if (holder && task.status === "claimed" && task.claimedBy === agent.name) {
|
|
@@ -9046,9 +9237,9 @@ var Scheduler = class {
|
|
|
9046
9237
|
/**
|
|
9047
9238
|
* A heartbeat per member and scope on the cadence, which then looks at every conversation of the
|
|
9048
9239
|
* scope: the unread messages of its home, of each other channel, and of each thread, a stage the
|
|
9049
|
-
* member holds, and a stage
|
|
9050
|
-
*
|
|
9051
|
-
*
|
|
9240
|
+
* member holds, and a stage of an unblocked task waiting that it may take, each in its task's
|
|
9241
|
+
* thread. Each conversation with something wakes on its own, so a heartbeat never brings one
|
|
9242
|
+
* task's business into another's turn.
|
|
9052
9243
|
*/
|
|
9053
9244
|
async checkHeartbeats(now) {
|
|
9054
9245
|
for (const agent of await this.board.listAgents()) {
|
|
@@ -9100,7 +9291,7 @@ var Scheduler = class {
|
|
|
9100
9291
|
}
|
|
9101
9292
|
if (project !== SOCIETY_SCOPE) {
|
|
9102
9293
|
for (const task of await this.board.openTasks(project)) {
|
|
9103
|
-
if (mayHoldStage(agent, task)) {
|
|
9294
|
+
if (mayHoldStage(agent, task) && (await this.blockersInPlay(task.blockedBy)).length === 0) {
|
|
9104
9295
|
reasonsIn({ thread: { id: task.id, task: true } }).waiting += 1;
|
|
9105
9296
|
}
|
|
9106
9297
|
}
|
|
@@ -9252,9 +9443,10 @@ var Scheduler = class {
|
|
|
9252
9443
|
return latest;
|
|
9253
9444
|
}
|
|
9254
9445
|
/**
|
|
9255
|
-
* A current stage without a holder past the threshold is logged as a signal once while it waits
|
|
9256
|
-
* It wakes nobody by itself: its assignees see the stage on
|
|
9257
|
-
*
|
|
9446
|
+
* A current stage without a holder past the threshold is logged as a signal once while it waits,
|
|
9447
|
+
* naming the tasks that still block it. It wakes nobody by itself: its assignees see the stage on
|
|
9448
|
+
* their heartbeat, or a blocked one when its last blocker ends, and the steward reads the signal
|
|
9449
|
+
* at its next turn and decides whether to replan or to mention someone.
|
|
9258
9450
|
*/
|
|
9259
9451
|
async checkWaitingStages(now) {
|
|
9260
9452
|
const stillWaiting = /* @__PURE__ */ new Set();
|
|
@@ -9275,10 +9467,12 @@ var Scheduler = class {
|
|
|
9275
9467
|
}
|
|
9276
9468
|
this.state.waitingSeen[key] = iso(now);
|
|
9277
9469
|
const assignee2 = stage.agent ?? stage.role ?? "anyone in the project";
|
|
9470
|
+
const blockers = await this.blockersInPlay(task.blockedBy);
|
|
9471
|
+
const waited = blockers.length === 0 ? "for a holder" : `blocked by ${blockers.map((id) => `task ${id}`).join(", ")}`;
|
|
9278
9472
|
await this.board.publishSignal({
|
|
9279
9473
|
kind: "waiting_stage",
|
|
9280
9474
|
key: `waiting_stage:${key}`,
|
|
9281
|
-
summary: `stage "${stage.name}" of task ${task.id} "${task.title}" in ${project.slug}, for ${assignee2}, has waited ${describeDuration(age)}
|
|
9475
|
+
summary: `stage "${stage.name}" of task ${task.id} "${task.title}" in ${project.slug}, for ${assignee2}, has waited ${describeDuration(age)} ${waited}`,
|
|
9282
9476
|
value: age,
|
|
9283
9477
|
threshold: this.timings.waitingStageMs,
|
|
9284
9478
|
project: project.slug,
|
|
@@ -9741,6 +9935,7 @@ var TURN_CONTRACT = [
|
|
|
9741
9935
|
"- To come back to something later, set a cron with create_cron: a note on what to do then, and a five-field cron expression or one time with at. It wakes you in this conversation unless you name another, at most every fifteen minutes, and every fire is a paid turn, so end a cron with remove_cron once it has done its job. Never wait by holding a stage, sleeping, or leaving a process running: a turn's board token ends with the turn, so nothing it leaves behind can act on the board. The society's crons are under society/crons/ in the board projection.",
|
|
9742
9936
|
"- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
|
|
9743
9937
|
"- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
|
|
9938
|
+
"- To show an image in a post, such as a screenshot or a chart, commit the file to a task's branch and link it by its path in the task's worktree, as ``: the board shows a file on a task's branch in place once it is committed, at the latest when your turn ends. An image at an https address shows too. Never put an image or any other file into a message as base64 or a data: URL: the board does not show it, and every reader of the conversation gets it in full in their prompt.",
|
|
9744
9939
|
"- Route every lesson: about you, your craft, or the user, write it to memory/core.md in your home directory, and keep that file short by moving detail to memory/<topic>.md, your archive; a durable fact about a project's codebase or process goes through `write_knowledge` on that project; something the whole project should know now, post it to the project channel, and anything about one task, in the task's thread.",
|
|
9745
9940
|
'- A procedure you have followed twice is a skill: write it to skills/<name>/SKILL.md in your home, frontmatter with `name` and a one-line `description` and then the steps, and your skills index lists it from the next turn. Files the steps use, such as scripts/ and references/, go in the same folder, named in SKILL.md by paths relative to it. Propose it with kind "skill" when the whole society would use it.',
|
|
9746
9941
|
"- To take a published skill for yourself, install it in a temporary folder and copy it into your home: in a folder from `mktemp -d`, `npx -y skills add <source> --skill <name> -a universal --copy -y` writes `.agents/skills/<name>/` and a lock file there, and you copy that skill folder to skills/<name>/ in your home, named in lowercase letters, digits, and hyphens. Never install one inside a worktree, where it would be committed into the project, or with -g, which writes to the runner's own home, shared by every citizen on it and listed in no skills index.",
|
|
@@ -9754,21 +9949,22 @@ var PLANNING = [
|
|
|
9754
9949
|
"- Name stages by what gets done. Plan the next few steps rather than everything, and reshape with plan_task as the work teaches you.",
|
|
9755
9950
|
"- Assign a stage to a role when anyone in it could do it, to a citizen only when it must be them, and to nobody when anyone in the project could. A citizen named on a stage must be a member of the task's project; add them first. A stage wakes everyone it is open to when it comes up, so the narrower the assignee, the fewer turns it costs.",
|
|
9756
9951
|
"- Reshape rather than force: another round is an inserted stage, and a wait on something outside the society is a stage named for what it waits on. When a check finds work unfinished, send the task back to an earlier stage with update_task.",
|
|
9952
|
+
"- A task that cannot start until other tasks finish names them with update_task blocked_by. While any of them is in play, its stage wakes nobody, neither when it comes up nor on a heartbeat; once the last has ended, done or abandoned, the stage wakes as if it had just come up, or wakes its holder. Nobody needs to clear the block or mention anyone when they end; change blocked_by only when the plan changes.",
|
|
9757
9953
|
"- A gated stage is an independent check: nobody who held an earlier stage of the task may hold it, and only the user, the steward, and the concierge may add, remove, move, reassign, or ungate it. Ask them in the task's thread when a gate should change.",
|
|
9758
9954
|
"- Commit your work on the task's branch before you advance, and say what you did in the advance note; it is the handover the next holder reads in the task's thread."
|
|
9759
9955
|
].join("\n");
|
|
9760
9956
|
var GOVERNANCE = [
|
|
9761
|
-
"- Anything the society lacks is a proposal: call `propose` with a kind and a charter. It opens the proposal's thread in governance with your rationale, where it is discussed and where the decision is posted. Approval provisions it on the spot, and either decision wakes the proposer. The user decides members, roles, retirements, and
|
|
9957
|
+
"- Anything the society lacks is a proposal: call `propose` with a kind and a charter. It opens the proposal's thread in governance with your rationale, where it is discussed and where the decision is posted. Approval provisions it on the spot, and either decision wakes the proposer. The user decides members, roles, retirements, archives, and reallocations that change a role's verbs or replicas; the steward may also decide channels, skills, and other reallocations. Nobody decides their own proposal.",
|
|
9762
9958
|
"- Charter shapes, as JSON objects:",
|
|
9763
9959
|
' - member: {"name", "role", "cli": "claude" or "codex", "memberships": [project slugs], "model"?, "homeRunner"?, "subscriptions"?, "seedInstructions"?}',
|
|
9764
9960
|
' - role: {"name", "purpose", "verbs": [board verbs], "wakeTriggers"?: any of "user_post", "ops_event", "heartbeat" (heartbeat when omitted), "maxReplicas"?, "backlogThreshold"?, "societyScope"?: true for a society role, whose members are never in a project and keep watch over the whole society}. Mentions, stages that become yours, and finished tasks you created wake every role; a heartbeat wakes you when something is unread, held, or waiting for you.',
|
|
9765
9961
|
' - channel: {"project": slug or null, "name", "purpose"}',
|
|
9766
9962
|
' - retirement: {"agent", "reason"}',
|
|
9767
9963
|
' - archive: {"project", "reason"}: approval archives the project: its members leave, its open threads close, and nothing more is posted, filed, or joined there, while its files and history stay; refused while a task there is in play',
|
|
9768
|
-
|
|
9769
|
-
' - skill: {"name", "summary"}: the folder skills/<name>/ in your home, SKILL.md and every file beside it, which the board takes as it stands when this turn ends into society/proposals/<id>/, where whoever decides reads it; a proposal whose folder has no SKILL.md then is rejected by the board. Approval publishes it under society/skills/<name>/, where every citizen\'s skills index lists it until the steward or the user removes it with `remove_skill`; ask the steward to remove one that is wrong or superseded. A proposal from before folders carries "body", its SKILL.md text alone',
|
|
9964
|
+
` - reallocation: {"role", "changes": {"maxReplicas"?, "backlogThreshold"?, "wakeTriggers"?, "verbs"?}, "description"}: approval applies the changes to the existing role, each the field's new value and verbs and wakeTriggers whole lists, for its members from their next turn; refused for a role that does not exist or that has those values already. Only the user decides one that changes verbs or maxReplicas`,
|
|
9965
|
+
' - skill: {"name", "summary"}: the folder skills/<name>/ in your home, SKILL.md and every file beside it, which the board takes as it stands when this turn ends into society/proposals/<id>/, where whoever decides reads it; a proposal whose folder has no SKILL.md then is rejected by the board. Approval publishes it under society/skills/<name>/, where every citizen\'s skills index lists it until the steward or the user removes it with `remove_skill`; ask the steward to remove one that is wrong or superseded. Once yours is approved, delete skills/<name>/ from your home and report memoryUpdated: the board leaves your copy there, and your index would list it beside the published one and follow it as it drifts from what was reviewed. To revise a published skill, copy its folder home, edit it, and propose it again. A proposal from before folders carries "body", its SKILL.md text alone',
|
|
9770
9966
|
"- Projects and membership: a project is a lasting area of work, not one request; `create_project` opens one with its general channel (front desk and user), and `configure_project` sets its completion effect, its display name, or its default branch, never its slug (user, steward, concierge). A workstream within a project or the society, such as a release, gets a channel of its own: `create_channel` opens it, which its opener follows, and announces it in the place's general, where members subscribe when it concerns them, `create_task` with `channel` files the workstream's tasks there, and `archive_channel` ends it once none of its tasks is in play, leaving its history readable (user, steward, concierge; others propose a channel). A project whose work is finished or has moved to another project is archived by proposal, once no task there is in play. `join_project` and `leave_project` move yourself, or another citizen when you are the concierge, the steward, or the user; joining gives the pair a worktree and an onboarding turn. A society role, one that keeps watch over the whole society as the concierge and the steward do, is never in a project: it moves others, never itself.",
|
|
9771
|
-
"- Prefer scaling an existing role over inventing one; a new role is justified by work a project needs that no existing role covers. A role that needs a tool the board lacks is an engineering task, not a hiring request.",
|
|
9967
|
+
"- Prefer scaling an existing role over inventing one, with a reallocation of its replicas, backlog threshold, wakes, or verbs; a new role is justified by work a project needs that no existing role covers. A role that needs a tool the board lacks is an engineering task, not a hiring request.",
|
|
9772
9968
|
"- Operations signals are counters and timers the board logs, listed in the prompt of a role that reads them: stages waiting for a holder, backlog per role, stages waiting on a role nobody fills, churn, stale threads, idle members, missing capabilities, replicas added, spend. They are not posts in any channel; interpreting them is your judgment."
|
|
9773
9969
|
].join("\n");
|
|
9774
9970
|
function renderOnboardingPreamble(context) {
|
|
@@ -9951,7 +10147,7 @@ function buildTurnPrompt(input) {
|
|
|
9951
10147
|
lines.push("A task you created is done. Tell whoever asked for it, if anyone did.");
|
|
9952
10148
|
}
|
|
9953
10149
|
if (dispatch.trigger.kind === "proposal_decided") {
|
|
9954
|
-
lines.push("A proposal you made was decided; the decision is the last post in the proposal's thread. A decision wakes you in the conversation you filed the proposal from, or at home once that has ended. Carry on with what an approval enables or a rejection asks, and stay silent if nothing follows.");
|
|
10150
|
+
lines.push("A proposal you made was decided; the decision is the last post in the proposal's thread. A decision wakes you in the conversation you filed the proposal from, or at home once that has ended. Carry on with what an approval enables or a rejection asks, and stay silent if nothing follows. An approved skill is published under society/skills/<name>/: delete your own skills/<name>/ from your home and report memoryUpdated, so your index lists the published copy alone.");
|
|
9955
10151
|
}
|
|
9956
10152
|
if (dispatch.trigger.kind === "cron") {
|
|
9957
10153
|
lines.push("A cron came due: what it asks is under Crons below.");
|
|
@@ -10737,7 +10933,7 @@ var TurnHost = class {
|
|
|
10737
10933
|
if (await this.board.hasMentioned(actor.name, USER_NAME, since)) {
|
|
10738
10934
|
return;
|
|
10739
10935
|
}
|
|
10740
|
-
const proposed = (await this.board.listProposals()).some((proposal) => proposal.proposedBy === actor.name && proposal.createdAt >= since &&
|
|
10936
|
+
const proposed = (await this.board.listProposals()).some((proposal) => proposal.proposedBy === actor.name && proposal.createdAt >= since && proposalApprovers(proposal.kind, proposal.charter).includes(USER_ROLE));
|
|
10741
10937
|
if (proposed) {
|
|
10742
10938
|
return;
|
|
10743
10939
|
}
|