@kubitnodes/stellaris 0.3.23 → 0.3.25
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 +543 -682
- package/dist/server/index.js +866 -735
- package/package.json +4 -4
- package/web/dist/assets/BackLink-UTiLF1J4.js +1 -0
- package/web/dist/assets/{Button-f4jgBZY1.js → Button-Cp4cNRUX.js} +1 -1
- package/web/dist/assets/ChannelForms-DD2B0tey.js +1 -0
- package/web/dist/assets/ChannelView-CY3oG9hZ.js +2 -0
- package/web/dist/assets/CitizenView-BNK5ro-6.js +2 -0
- package/web/dist/assets/CitizensView-WPQ6Jhl3.js +1 -0
- package/web/dist/assets/{CliIcon-CLHaA65A.js → CliIcon-C7bwChMP.js} +1 -1
- package/web/dist/assets/CronsView-_pdgwX1I.js +1 -0
- package/web/dist/assets/FileTree-jyDUSu7g.js +1 -0
- package/web/dist/assets/FileView-DcQvlc8V.js +4 -0
- package/web/dist/assets/KnowledgeView-DDl0HQPG.js +1 -0
- package/web/dist/assets/Markdown-DT3V-EVe.js +30 -0
- package/web/dist/assets/{MessageItem-Bk9eBp1U.js → MessageItem-agtEugfG.js} +1 -1
- package/web/dist/assets/MetricsView-DiXyYeWP.js +1 -0
- package/web/dist/assets/NeedsYouView-CKmuYan_.js +1 -0
- package/web/dist/assets/Overview-CKcTqsMM.js +1 -0
- package/web/dist/assets/{Pane-CJYRVkwh.js → Pane-BaM0TdE6.js} +1 -1
- package/web/dist/assets/{Picker-DoxCN6C7.js → Picker-C2ig-0dT.js} +1 -1
- package/web/dist/assets/ProjectView-T2OHTh5j.js +1 -0
- package/web/dist/assets/ProposalView-B5yjTAcn.js +1 -0
- package/web/dist/assets/ProposalsView-p4KjwV9p.js +1 -0
- package/web/dist/assets/Quota-CfFGIql9.js +1 -0
- package/web/dist/assets/RunnersView-DdvwoOur.js +1 -0
- package/web/dist/assets/SkillFileList-CgrCnuaC.js +2 -0
- package/web/dist/assets/SkillView-Di5Z9h2L.js +1 -0
- package/web/dist/assets/SocietyView-BDiVxUC5.js +1 -0
- package/web/dist/assets/StickyList-BaV_KZ8x.js +1 -0
- package/web/dist/assets/SubjectThread-DD_ZCCnr.js +1 -0
- package/web/dist/assets/TaskView-CKcs-MOC.js +3 -0
- package/web/dist/assets/TasksView-oZMjncOU.js +1 -0
- package/web/dist/assets/{ThreadForms-C846TvtS.js → ThreadForms-CG5Y926b.js} +1 -1
- package/web/dist/assets/ThreadItem-D-n2arFA.js +1 -0
- package/web/dist/assets/ThreadView-CT9vd0P4.js +1 -0
- package/web/dist/assets/citizen-BiHfph2A.js +1 -0
- package/web/dist/assets/{compose-BmqQmcZH.js → compose-DY1WL8zB.js} +1 -1
- package/web/dist/assets/format-CENiwkJC.js +55 -0
- package/web/dist/assets/governance-C6ez5MLh.js +1 -0
- package/web/dist/assets/index-CPXALUV6.js +23 -0
- package/web/dist/assets/index-DIa8Dlav.css +2 -0
- package/web/dist/assets/mentions-BEL5U6ra.js +3 -0
- package/web/dist/assets/{tasks-R3YgpYTe.js → tasks-CAI8Eci7.js} +1 -1
- package/web/dist/assets/useMatch-CINzyDWa.js +1 -0
- package/web/dist/index.html +9 -9
- package/web/dist/assets/BackLink-DGYs80bb.js +0 -1
- package/web/dist/assets/ChannelForms-q9zdj4p2.js +0 -1
- package/web/dist/assets/ChannelView-CGs_aWL3.js +0 -2
- package/web/dist/assets/CitizenView-gcIo_sKt.js +0 -2
- package/web/dist/assets/CitizensView-DZ7S8vnJ.js +0 -1
- package/web/dist/assets/CronsView-1wns6Uwv.js +0 -1
- package/web/dist/assets/KnowledgeView-Cxfi2-Pt.js +0 -1
- package/web/dist/assets/Markdown-BhtIFcWc.js +0 -30
- package/web/dist/assets/MetricsView-CTEH6rJn.js +0 -1
- package/web/dist/assets/NeedsYouView-2VNqH2Sb.js +0 -1
- package/web/dist/assets/Overview-CS9HCSmm.js +0 -1
- package/web/dist/assets/ProjectView-CJ3r4UdN.js +0 -1
- package/web/dist/assets/ProposalView-DLn9dNPp.js +0 -2
- package/web/dist/assets/ProposalsView-B-cOxTsQ.js +0 -1
- package/web/dist/assets/Quota-Bo7UdWTV.js +0 -1
- package/web/dist/assets/RunnersView-DlQruhwp.js +0 -1
- package/web/dist/assets/SocietyView-Bl4ErnV8.js +0 -1
- package/web/dist/assets/StickyList-MVP34ytV.js +0 -1
- package/web/dist/assets/SubjectThread-BFFnHxIF.js +0 -1
- package/web/dist/assets/TaskFileView-GZ0eQFUb.js +0 -4
- package/web/dist/assets/TaskView-DppFelLW.js +0 -3
- package/web/dist/assets/TasksView-DDNXzeG4.js +0 -1
- package/web/dist/assets/ThreadItem-C8cou6wD.js +0 -1
- package/web/dist/assets/ThreadView-DvSGDYoD.js +0 -1
- package/web/dist/assets/citizen-B2Dlbcpe.js +0 -1
- package/web/dist/assets/format-DT52SbbR.js +0 -55
- package/web/dist/assets/governance-Cr5OlQnw.js +0 -1
- package/web/dist/assets/index-BTVy_xFF.js +0 -23
- package/web/dist/assets/index-DzV6CCnG.css +0 -2
- package/web/dist/assets/mentions-D5hNgtg6.js +0 -3
- package/web/dist/assets/useMatch-pEF6RJ6Q.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// apps/server/dist/index.js
|
|
4
4
|
import { access as access2 } from "node:fs/promises";
|
|
5
|
-
import
|
|
5
|
+
import path10 from "node:path";
|
|
6
6
|
import { serve } from "@hono/node-server";
|
|
7
7
|
|
|
8
8
|
// packages/board-core/dist/board.js
|
|
9
|
-
import { chmod as chmod2, cp, mkdir as mkdir4, readdir as
|
|
10
|
-
import
|
|
9
|
+
import { chmod as chmod2, cp, mkdir as mkdir4, readdir as readdir4, readFile as readFile4, rename as rename3, rm as rm2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
|
|
10
|
+
import path7 from "node:path";
|
|
11
11
|
import { decodeTime, monotonicFactory } from "ulid";
|
|
12
12
|
import { z as z16 } from "zod";
|
|
13
13
|
|
|
@@ -235,6 +235,7 @@ var CronFrontmatterSchema = z3.object({
|
|
|
235
235
|
scope: NameSchema,
|
|
236
236
|
thread: UlidSchema.optional(),
|
|
237
237
|
channel: NameSchema.optional(),
|
|
238
|
+
place: NameSchema.optional(),
|
|
238
239
|
schedule: CronScheduleSchema,
|
|
239
240
|
createdBy: NameSchema,
|
|
240
241
|
createdAt: IsoDateTimeSchema,
|
|
@@ -384,15 +385,16 @@ function quotaLevel(quota, now) {
|
|
|
384
385
|
var WINDOW_NAMES = {
|
|
385
386
|
five_hour: "5h",
|
|
386
387
|
seven_day: "7d",
|
|
387
|
-
seven_day_opus: "7d
|
|
388
|
-
seven_day_sonnet: "7d
|
|
388
|
+
seven_day_opus: "7d opus",
|
|
389
|
+
seven_day_sonnet: "7d sonnet",
|
|
389
390
|
seven_day_oauth_apps: "7d apps",
|
|
390
391
|
seven_day_overage_included: "7d extra",
|
|
391
392
|
overage: "extra usage"
|
|
392
393
|
};
|
|
393
394
|
function windowName(window) {
|
|
394
395
|
if (window.label !== void 0) {
|
|
395
|
-
|
|
396
|
+
const label = window.label.toLowerCase();
|
|
397
|
+
return window.minutes === 10080 ? `7d ${label}` : label;
|
|
396
398
|
}
|
|
397
399
|
const named = WINDOW_NAMES[window.key];
|
|
398
400
|
if (named !== void 0) {
|
|
@@ -766,7 +768,6 @@ var ReallocationProposalSchema = z5.object({
|
|
|
766
768
|
changes: RoleChangesSchema,
|
|
767
769
|
description: z5.string().min(1)
|
|
768
770
|
});
|
|
769
|
-
var LegacyReallocationSchema = z5.object({ description: z5.string().min(1) });
|
|
770
771
|
function applyRoleChanges(role, changes) {
|
|
771
772
|
return {
|
|
772
773
|
...role,
|
|
@@ -811,8 +812,7 @@ var ArchiveProposalSchema = z5.object({
|
|
|
811
812
|
});
|
|
812
813
|
var SkillProposalSchema = z5.object({
|
|
813
814
|
name: NameSchema,
|
|
814
|
-
summary: z5.string().min(1).max(200)
|
|
815
|
-
body: z5.string().min(1).optional()
|
|
815
|
+
summary: z5.string().min(1).max(200)
|
|
816
816
|
});
|
|
817
817
|
var ProposalCharterSchemas = {
|
|
818
818
|
role: RoleCharterSchema,
|
|
@@ -889,14 +889,10 @@ function describeCharter(kind, charter) {
|
|
|
889
889
|
}
|
|
890
890
|
case "reallocation": {
|
|
891
891
|
const parsed = ReallocationProposalSchema.safeParse(charter);
|
|
892
|
-
if (parsed.success)
|
|
893
|
-
const { role, changes, description } = parsed.data;
|
|
894
|
-
return `reallocation of role ${role} (${describeRoleChanges(changes)}): ${description}`;
|
|
895
|
-
}
|
|
896
|
-
const legacy = LegacyReallocationSchema.safeParse(charter);
|
|
897
|
-
if (!legacy.success)
|
|
892
|
+
if (!parsed.success)
|
|
898
893
|
break;
|
|
899
|
-
|
|
894
|
+
const { role, changes, description } = parsed.data;
|
|
895
|
+
return `reallocation of role ${role} (${describeRoleChanges(changes)}): ${description}`;
|
|
900
896
|
}
|
|
901
897
|
case "skill": {
|
|
902
898
|
const parsed = SkillProposalSchema.safeParse(charter);
|
|
@@ -991,7 +987,8 @@ var DashboardSchema = z5.object({
|
|
|
991
987
|
var ProposalOriginSchema = z5.object({
|
|
992
988
|
scope: NameSchema,
|
|
993
989
|
thread: UlidSchema.optional(),
|
|
994
|
-
channel: NameSchema.optional()
|
|
990
|
+
channel: NameSchema.optional(),
|
|
991
|
+
place: NameSchema.optional()
|
|
995
992
|
});
|
|
996
993
|
var SKILL_FILES_LIMIT = 100;
|
|
997
994
|
var SKILL_BYTES_LIMIT = 10 * 1024 * 1024;
|
|
@@ -1106,12 +1103,10 @@ var VerbInputs = {
|
|
|
1106
1103
|
channel: ChannelRefSchema.optional(),
|
|
1107
1104
|
limit: z7.number().int().positive().max(100).default(20)
|
|
1108
1105
|
}),
|
|
1109
|
-
/** On a
|
|
1106
|
+
/** On a channel with a title; a task's and a proposal's open with them. */
|
|
1110
1107
|
open_thread: z7.object({
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
channel: ChannelRefSchema.optional(),
|
|
1114
|
-
title: z7.string().min(1).max(200).optional()
|
|
1108
|
+
channel: ChannelRefSchema,
|
|
1109
|
+
title: z7.string().min(1).max(200)
|
|
1115
1110
|
}),
|
|
1116
1111
|
close_thread: z7.object({ thread_id: UlidSchema, summary: z7.string().min(1) }),
|
|
1117
1112
|
create_task: z7.object({
|
|
@@ -1385,6 +1380,7 @@ var TurnExitReasonSchema = z8.enum([
|
|
|
1385
1380
|
"blocked",
|
|
1386
1381
|
"stopped"
|
|
1387
1382
|
]);
|
|
1383
|
+
var ResumeFailureSchema = z8.enum(["lost", "retry"]);
|
|
1388
1384
|
var AgentEventSchema = z8.discriminatedUnion("type", [
|
|
1389
1385
|
z8.object({
|
|
1390
1386
|
type: z8.literal("turn_started"),
|
|
@@ -1436,6 +1432,8 @@ var LiveTurnEventSchema = z8.object({
|
|
|
1436
1432
|
thread: UlidSchema.optional(),
|
|
1437
1433
|
/** The channel, never general, whose conversation the turn is in. */
|
|
1438
1434
|
channel: NameSchema.optional(),
|
|
1435
|
+
/** The project whose channels' conversation the turn is in, for a turn in the society scope. */
|
|
1436
|
+
place: NameSchema.optional(),
|
|
1439
1437
|
event: AgentEventSchema
|
|
1440
1438
|
});
|
|
1441
1439
|
|
|
@@ -1538,7 +1536,8 @@ var TurnWorkspaceSchema = z9.discriminatedUnion("kind", [
|
|
|
1538
1536
|
z9.object({
|
|
1539
1537
|
kind: z9.literal("home"),
|
|
1540
1538
|
thread: UlidSchema.optional(),
|
|
1541
|
-
channel: NameSchema.optional()
|
|
1539
|
+
channel: NameSchema.optional(),
|
|
1540
|
+
place: NameSchema.optional()
|
|
1542
1541
|
}),
|
|
1543
1542
|
z9.object({
|
|
1544
1543
|
kind: z9.literal("project"),
|
|
@@ -1547,7 +1546,16 @@ var TurnWorkspaceSchema = z9.discriminatedUnion("kind", [
|
|
|
1547
1546
|
thread: UlidSchema.optional(),
|
|
1548
1547
|
channel: NameSchema.optional()
|
|
1549
1548
|
}),
|
|
1550
|
-
z9.object({
|
|
1549
|
+
z9.object({
|
|
1550
|
+
kind: z9.literal("task"),
|
|
1551
|
+
repo: ProjectRepoSchema,
|
|
1552
|
+
taskId: UlidSchema,
|
|
1553
|
+
/**
|
|
1554
|
+
* The citizen holding the task's current stage, when someone does: a turn of anyone else
|
|
1555
|
+
* works detached at the branch's tip, so the holder's turns find the branch free.
|
|
1556
|
+
*/
|
|
1557
|
+
holder: NameSchema.optional()
|
|
1558
|
+
})
|
|
1551
1559
|
]);
|
|
1552
1560
|
var TurnJobSchema = z9.object({
|
|
1553
1561
|
turnId: UlidSchema,
|
|
@@ -1563,6 +1571,8 @@ var TurnJobSchema = z9.object({
|
|
|
1563
1571
|
thread: UlidSchema.optional(),
|
|
1564
1572
|
/** The channel, never general, whose conversation the turn is in. */
|
|
1565
1573
|
channel: NameSchema.optional(),
|
|
1574
|
+
/** The project whose channels' conversation the turn is in, for a turn in the society scope. */
|
|
1575
|
+
place: NameSchema.optional(),
|
|
1566
1576
|
/**
|
|
1567
1577
|
* The session to resume, or null to start one: the CLI's adapter picks a fresh session's id, and
|
|
1568
1578
|
* the outcome reports it.
|
|
@@ -1583,6 +1593,12 @@ var TurnJobSchema = z9.object({
|
|
|
1583
1593
|
workspace: TurnWorkspaceSchema
|
|
1584
1594
|
});
|
|
1585
1595
|
var TurnWorkSchema = z9.object({ branch: z9.string().min(1), head: z9.string().min(1) });
|
|
1596
|
+
var StuckWorkSchema = z9.object({
|
|
1597
|
+
branch: z9.string().min(1),
|
|
1598
|
+
agent: NameSchema,
|
|
1599
|
+
commits: z9.array(z9.string()),
|
|
1600
|
+
total: z9.number().int().positive()
|
|
1601
|
+
});
|
|
1586
1602
|
var TurnEventsSchema = z9.object({ entries: z9.array(TranscriptEntrySchema) });
|
|
1587
1603
|
var TurnOutcomeSchema = z9.object({
|
|
1588
1604
|
exitReason: TurnExitReasonSchema,
|
|
@@ -1592,8 +1608,24 @@ var TurnOutcomeSchema = z9.object({
|
|
|
1592
1608
|
costUsd: z9.number().nonnegative(),
|
|
1593
1609
|
/** The session's running total after this turn, for CLIs that report one. */
|
|
1594
1610
|
sessionCostUsd: z9.number().nonnegative().optional(),
|
|
1595
|
-
/**
|
|
1596
|
-
|
|
1611
|
+
/**
|
|
1612
|
+
* The session the CLI used, which differs from the job's for CLIs that assign their own ids. A
|
|
1613
|
+
* turn whose CLI never started, because it failed or was stopped first, reports the job's own,
|
|
1614
|
+
* null in a conversation's first turn, so a session never started is not recorded.
|
|
1615
|
+
*/
|
|
1616
|
+
session: z9.string().min(1).nullable(),
|
|
1617
|
+
/**
|
|
1618
|
+
* Set when the CLI could not resume the job's session, which failed the turn before any model
|
|
1619
|
+
* call; the server then drops a `lost` session, and one that failed `retry` too often in a row. A
|
|
1620
|
+
* runner from before never sends it.
|
|
1621
|
+
*/
|
|
1622
|
+
resumeFailed: ResumeFailureSchema.optional(),
|
|
1623
|
+
/**
|
|
1624
|
+
* Set when the model service turned the turn away for now, as when it was overloaded or failed
|
|
1625
|
+
* on its side, which the scheduler answers by sending the same wake again within minutes. A
|
|
1626
|
+
* runner from before never sends it.
|
|
1627
|
+
*/
|
|
1628
|
+
transient: z9.boolean().optional(),
|
|
1597
1629
|
/** The model the CLI reported, when it reported one. */
|
|
1598
1630
|
model: z9.string().nullable(),
|
|
1599
1631
|
work: TurnWorkSchema.nullable(),
|
|
@@ -1602,6 +1634,11 @@ var TurnOutcomeSchema = z9.object({
|
|
|
1602
1634
|
* branch after the hand-back: uncommitted changes, or commits no branch contains.
|
|
1603
1635
|
*/
|
|
1604
1636
|
leftovers: z9.boolean().default(false),
|
|
1637
|
+
/**
|
|
1638
|
+
* Commits the hand-back found its task's worktrees holding that no fast-forward of the task's
|
|
1639
|
+
* branch can take, each told once; a runner from before never sends it.
|
|
1640
|
+
*/
|
|
1641
|
+
stuck: z9.array(StuckWorkSchema).optional(),
|
|
1605
1642
|
/**
|
|
1606
1643
|
* The CLI's plan quota as the turn's stream last reported it, when it reported any; a runner from
|
|
1607
1644
|
* before never sends it.
|
|
@@ -1720,13 +1757,17 @@ var TurnImageSchema = z9.discriminatedUnion("kind", [
|
|
|
1720
1757
|
]);
|
|
1721
1758
|
var WorkspaceConversationSchema = z9.discriminatedUnion("kind", [
|
|
1722
1759
|
z9.object({ kind: z9.literal("thread"), id: UlidSchema }),
|
|
1723
|
-
z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema })
|
|
1760
|
+
z9.object({ kind: z9.literal("channel"), scope: NameSchema, name: NameSchema }),
|
|
1761
|
+
z9.object({ kind: z9.literal("place"), project: NameSchema })
|
|
1724
1762
|
]);
|
|
1725
1763
|
function workspaceConversationOf(turn) {
|
|
1726
1764
|
if (turn.thread !== void 0) {
|
|
1727
1765
|
return { kind: "thread", id: turn.thread };
|
|
1728
1766
|
}
|
|
1729
|
-
|
|
1767
|
+
if (turn.channel !== void 0) {
|
|
1768
|
+
return { kind: "channel", scope: turn.scope, name: turn.channel };
|
|
1769
|
+
}
|
|
1770
|
+
return turn.place === void 0 ? null : { kind: "place", project: turn.place };
|
|
1730
1771
|
}
|
|
1731
1772
|
var HeldWorkspaceSchema = z9.object({
|
|
1732
1773
|
agent: NameSchema,
|
|
@@ -1832,6 +1873,7 @@ var TurnDispatchSchema = z10.object({
|
|
|
1832
1873
|
project: NameSchema,
|
|
1833
1874
|
thread: TurnThreadSchema.optional(),
|
|
1834
1875
|
channel: NameSchema.optional(),
|
|
1876
|
+
place: NameSchema.optional(),
|
|
1835
1877
|
trigger: TriggerSchema,
|
|
1836
1878
|
priority: z10.number().int().min(0).max(2),
|
|
1837
1879
|
onboarding: z10.boolean().default(false),
|
|
@@ -1839,7 +1881,13 @@ var TurnDispatchSchema = z10.object({
|
|
|
1839
1881
|
crons: z10.array(UlidSchema).optional()
|
|
1840
1882
|
});
|
|
1841
1883
|
function conversationPart(conversation) {
|
|
1842
|
-
|
|
1884
|
+
if (conversation.thread !== void 0) {
|
|
1885
|
+
return conversation.thread;
|
|
1886
|
+
}
|
|
1887
|
+
if (conversation.channel !== void 0) {
|
|
1888
|
+
return `#${conversation.channel}`;
|
|
1889
|
+
}
|
|
1890
|
+
return conversation.place === void 0 ? void 0 : `~${conversation.place}`;
|
|
1843
1891
|
}
|
|
1844
1892
|
function sessionKey(agent, scope, part) {
|
|
1845
1893
|
return part === void 0 ? `${agent}/${scope}` : `${agent}/${scope}/${part}`;
|
|
@@ -1852,7 +1900,10 @@ function parseSessionKey(key) {
|
|
|
1852
1900
|
if (part === void 0) {
|
|
1853
1901
|
return { agent, scope };
|
|
1854
1902
|
}
|
|
1855
|
-
|
|
1903
|
+
if (part.startsWith("#")) {
|
|
1904
|
+
return { agent, scope, channel: part.slice(1) };
|
|
1905
|
+
}
|
|
1906
|
+
return part.startsWith("~") ? { agent, scope, place: part.slice(1) } : { agent, scope, thread: part };
|
|
1856
1907
|
}
|
|
1857
1908
|
function channelConversation(scope, channel) {
|
|
1858
1909
|
const at = channel.indexOf("/");
|
|
@@ -1860,6 +1911,10 @@ function channelConversation(scope, channel) {
|
|
|
1860
1911
|
const name = at < 0 ? channel : channel.slice(at + 1);
|
|
1861
1912
|
return place === scope && name !== "general" ? name : void 0;
|
|
1862
1913
|
}
|
|
1914
|
+
function placeConversation(scope, channel) {
|
|
1915
|
+
const at = channel.indexOf("/");
|
|
1916
|
+
return scope === SOCIETY_SCOPE && at >= 0 ? channel.slice(0, at) : void 0;
|
|
1917
|
+
}
|
|
1863
1918
|
var TurnRecordSchema = z10.object({
|
|
1864
1919
|
/** Assigned by the board when the turn begins; the turn's transcript is filed under it. */
|
|
1865
1920
|
id: UlidSchema.optional(),
|
|
@@ -1869,6 +1924,8 @@ var TurnRecordSchema = z10.object({
|
|
|
1869
1924
|
thread: UlidSchema.optional(),
|
|
1870
1925
|
/** The channel, never general, whose conversation the turn was in. */
|
|
1871
1926
|
channel: NameSchema.optional(),
|
|
1927
|
+
/** The project whose channels' conversation the turn was in, for a turn in the society scope. */
|
|
1928
|
+
place: NameSchema.optional(),
|
|
1872
1929
|
runner: NameSchema,
|
|
1873
1930
|
cli: CliKindSchema.nullable(),
|
|
1874
1931
|
session: z10.string().nullable(),
|
|
@@ -1892,6 +1949,14 @@ var TurnRecordSchema = z10.object({
|
|
|
1892
1949
|
work: TurnWorkSchema.optional(),
|
|
1893
1950
|
/** Who stopped the turn, for one that ended `stopped`. */
|
|
1894
1951
|
stoppedBy: NameSchema.optional(),
|
|
1952
|
+
/** Why the CLI could not resume the turn's session, for a turn that failed so. */
|
|
1953
|
+
resumeFailed: ResumeFailureSchema.optional(),
|
|
1954
|
+
/** Whether the model service turned the turn away for now: overloaded, or failing on its side. */
|
|
1955
|
+
transient: z10.boolean().optional(),
|
|
1956
|
+
/** How many turns in a row, this one included, could not resume the session for a `retry`. */
|
|
1957
|
+
resumeRetries: z10.number().int().positive().optional(),
|
|
1958
|
+
/** Whether the server dropped the conversation's session after this turn, which the next starts anew. */
|
|
1959
|
+
sessionDropped: z10.boolean().optional(),
|
|
1895
1960
|
/** The crons whose fire started the turn, which record how it ended. */
|
|
1896
1961
|
crons: z10.array(UlidSchema).optional()
|
|
1897
1962
|
});
|
|
@@ -1901,6 +1966,7 @@ var RunningTurnSchema = z10.object({
|
|
|
1901
1966
|
scope: NameSchema,
|
|
1902
1967
|
thread: UlidSchema.optional(),
|
|
1903
1968
|
channel: NameSchema.optional(),
|
|
1969
|
+
place: NameSchema.optional(),
|
|
1904
1970
|
cli: CliKindSchema,
|
|
1905
1971
|
askedIn: ChannelRefSchema.optional(),
|
|
1906
1972
|
steerable: z10.boolean(),
|
|
@@ -1931,6 +1997,8 @@ var TurnHistoryEntrySchema = z10.object({
|
|
|
1931
1997
|
thread: UlidSchema.optional(),
|
|
1932
1998
|
/** The channel, never general, whose conversation the turn was in. */
|
|
1933
1999
|
channel: NameSchema.optional(),
|
|
2000
|
+
/** The project whose channels' conversation the turn was in, for a turn in the society scope. */
|
|
2001
|
+
place: NameSchema.optional(),
|
|
1934
2002
|
trigger: z10.string(),
|
|
1935
2003
|
exitReason: z10.string().nullable(),
|
|
1936
2004
|
costUsd: z10.number(),
|
|
@@ -1959,7 +2027,8 @@ var OPS_SIGNAL_KINDS = [
|
|
|
1959
2027
|
"runner",
|
|
1960
2028
|
"home_conflict",
|
|
1961
2029
|
"stuck_workspace",
|
|
1962
|
-
"cron_failing"
|
|
2030
|
+
"cron_failing",
|
|
2031
|
+
"turn_refused"
|
|
1963
2032
|
];
|
|
1964
2033
|
var OpsSignalKindSchema = z11.enum(OPS_SIGNAL_KINDS);
|
|
1965
2034
|
var WAKING_SIGNAL_KINDS = [
|
|
@@ -1983,8 +2052,18 @@ var OpsSignalSchema = z11.object({
|
|
|
1983
2052
|
project: NameSchema.optional(),
|
|
1984
2053
|
agent: NameSchema.optional(),
|
|
1985
2054
|
role: NameSchema.optional(),
|
|
1986
|
-
taskId: UlidSchema.optional()
|
|
2055
|
+
taskId: UlidSchema.optional(),
|
|
2056
|
+
/**
|
|
2057
|
+
* Whether this log of the condition wakes the roles charted for `ops_event`, as its publisher
|
|
2058
|
+
* decided: the operations pass wakes them when a condition is new, and again only at the steps
|
|
2059
|
+
* it names, not at every repeat. Absent, the kind decides.
|
|
2060
|
+
*/
|
|
2061
|
+
wakes: z11.boolean().optional()
|
|
1987
2062
|
});
|
|
2063
|
+
var WAKING = new Set(WAKING_SIGNAL_KINDS);
|
|
2064
|
+
function signalWakes(signal) {
|
|
2065
|
+
return signal.wakes ?? WAKING.has(signal.kind);
|
|
2066
|
+
}
|
|
1988
2067
|
|
|
1989
2068
|
// packages/shared/dist/metrics.js
|
|
1990
2069
|
import { z as z12 } from "zod";
|
|
@@ -2178,7 +2257,7 @@ var RunnerConfigSchema = z13.object({
|
|
|
2178
2257
|
});
|
|
2179
2258
|
|
|
2180
2259
|
// packages/shared/dist/version.js
|
|
2181
|
-
var STELLARIS_VERSION = "0.3.
|
|
2260
|
+
var STELLARIS_VERSION = "0.3.25";
|
|
2182
2261
|
|
|
2183
2262
|
// packages/shared/dist/mentions.js
|
|
2184
2263
|
import { parse, postprocess, preprocess } from "micromark";
|
|
@@ -2562,6 +2641,7 @@ var HOME_GITIGNORE = [
|
|
|
2562
2641
|
"/projects/*/last-turn.json",
|
|
2563
2642
|
"/projects/*/threads/",
|
|
2564
2643
|
"/projects/*/channels/",
|
|
2644
|
+
"/projects/*/places/",
|
|
2565
2645
|
`/${HOME_SCRATCH}/`,
|
|
2566
2646
|
"*.tmp",
|
|
2567
2647
|
"node_modules/",
|
|
@@ -2579,7 +2659,7 @@ var HOME_GITIGNORE = [
|
|
|
2579
2659
|
var IGNORE_FILE = `${HOME_GITIGNORE.join("\n")}
|
|
2580
2660
|
`;
|
|
2581
2661
|
var PRE_RECEIVE = `#!/bin/sh
|
|
2582
|
-
forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/|channels/))'
|
|
2662
|
+
forbidden='^(agent\\.json|cursors\\.json|role\\.md|turns/|\\.claude/|\\.codex/|projects/[^/]+/(sessions[^/]*\\.json|last-turn\\.json|threads/|channels/|places/))'
|
|
2583
2663
|
while read old new ref; do
|
|
2584
2664
|
if [ "$ref" != "refs/heads/main" ]; then
|
|
2585
2665
|
echo "a home takes pushes to main only" >&2
|
|
@@ -2905,6 +2985,7 @@ var TurnPayload = z14.object({
|
|
|
2905
2985
|
project: z14.string(),
|
|
2906
2986
|
thread: z14.string().optional(),
|
|
2907
2987
|
channel: z14.string().optional(),
|
|
2988
|
+
place: z14.string().optional(),
|
|
2908
2989
|
trigger: z14.string()
|
|
2909
2990
|
});
|
|
2910
2991
|
var TaskPayload = z14.object({
|
|
@@ -2950,11 +3031,16 @@ function answers(mention, turn) {
|
|
|
2950
3031
|
if (!mention.scopes.includes(turn.project)) {
|
|
2951
3032
|
return false;
|
|
2952
3033
|
}
|
|
2953
|
-
const
|
|
3034
|
+
const topLevel = mention.thread === null;
|
|
3035
|
+
const own = topLevel ? channelConversation(turn.project, mention.channel) : void 0;
|
|
3036
|
+
const place = topLevel ? placeConversation(turn.project, mention.channel) : void 0;
|
|
2954
3037
|
if (turn.channel !== null) {
|
|
2955
|
-
return
|
|
3038
|
+
return topLevel && own === turn.channel;
|
|
3039
|
+
}
|
|
3040
|
+
if (turn.place !== null) {
|
|
3041
|
+
return topLevel && place === turn.place;
|
|
2956
3042
|
}
|
|
2957
|
-
return
|
|
3043
|
+
return topLevel && own === void 0 && place === void 0 || turn.trigger === "mention";
|
|
2958
3044
|
}
|
|
2959
3045
|
function computeMetrics(input) {
|
|
2960
3046
|
const { events, now, window } = input;
|
|
@@ -3020,6 +3106,7 @@ function computeMetrics(input) {
|
|
|
3020
3106
|
at,
|
|
3021
3107
|
thread: turn.data.thread ?? null,
|
|
3022
3108
|
channel: turn.data.channel ?? null,
|
|
3109
|
+
place: turn.data.place ?? null,
|
|
3023
3110
|
project: turn.data.project,
|
|
3024
3111
|
trigger: turn.data.trigger
|
|
3025
3112
|
};
|
|
@@ -3219,6 +3306,7 @@ var TurnHistoryPayloadSchema = z15.object({
|
|
|
3219
3306
|
project: NameSchema,
|
|
3220
3307
|
thread: UlidSchema.optional(),
|
|
3221
3308
|
channel: NameSchema.optional(),
|
|
3309
|
+
place: NameSchema.optional(),
|
|
3222
3310
|
trigger: z15.string().default("manual"),
|
|
3223
3311
|
exitReason: z15.string().nullable().default(null),
|
|
3224
3312
|
costUsd: z15.number().default(0),
|
|
@@ -3457,11 +3545,14 @@ var BoardPaths = class {
|
|
|
3457
3545
|
return path5.join(this.agentProjects(name), slug);
|
|
3458
3546
|
}
|
|
3459
3547
|
/**
|
|
3460
|
-
* A thread's
|
|
3461
|
-
* conversation; `part` is a thread's id or
|
|
3548
|
+
* A thread's, a channel's, or, in the society scope, a project's conversation's session and last
|
|
3549
|
+
* turn, beside the scope's home conversation; `part` is a thread's id, `#channel`, or `~project`.
|
|
3462
3550
|
*/
|
|
3463
3551
|
agentConversation(name, slug, part) {
|
|
3464
|
-
|
|
3552
|
+
if (part.startsWith("#")) {
|
|
3553
|
+
return path5.join(this.agentProject(name, slug), "channels", part.slice(1));
|
|
3554
|
+
}
|
|
3555
|
+
return part.startsWith("~") ? path5.join(this.agentProject(name, slug), "places", part.slice(1)) : path5.join(this.agentProject(name, slug), "threads", part);
|
|
3465
3556
|
}
|
|
3466
3557
|
agentCursors(name) {
|
|
3467
3558
|
return path5.join(this.agent(name), "cursors.json");
|
|
@@ -3495,6 +3586,111 @@ var BoardPaths = class {
|
|
|
3495
3586
|
}
|
|
3496
3587
|
};
|
|
3497
3588
|
|
|
3589
|
+
// packages/board-core/dist/skill-folder.js
|
|
3590
|
+
import { constants } from "node:fs";
|
|
3591
|
+
import { lstat, open as open2, readdir as readdir3, readlink, realpath } from "node:fs/promises";
|
|
3592
|
+
import path6 from "node:path";
|
|
3593
|
+
var SKILL_TEXT_LIMIT_CHARS = 1e5;
|
|
3594
|
+
var SKILL_TEXT_LIMIT_BYTES = SKILL_TEXT_LIMIT_CHARS * 4;
|
|
3595
|
+
function skillFileText(bytes) {
|
|
3596
|
+
const text = bytes.includes(0) ? null : bytes.toString("utf8");
|
|
3597
|
+
return {
|
|
3598
|
+
text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
|
|
3599
|
+
truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
|
|
3600
|
+
};
|
|
3601
|
+
}
|
|
3602
|
+
function inside(root, file) {
|
|
3603
|
+
const relative = path6.relative(root, file);
|
|
3604
|
+
return relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
|
|
3605
|
+
}
|
|
3606
|
+
async function handleInside(root, handle) {
|
|
3607
|
+
if (process.platform !== "linux") {
|
|
3608
|
+
return true;
|
|
3609
|
+
}
|
|
3610
|
+
const file = await readlink(`/proc/self/fd/${handle.fd}`).catch(() => null);
|
|
3611
|
+
return file !== null && inside(root, file);
|
|
3612
|
+
}
|
|
3613
|
+
async function walk(root, prefix, found) {
|
|
3614
|
+
const entries = await readdir3(path6.join(root, ...prefix === "" ? [] : prefix.split("/")), {
|
|
3615
|
+
withFileTypes: true
|
|
3616
|
+
}).catch(() => []);
|
|
3617
|
+
for (const entry of entries.toSorted((a, b) => a.name < b.name ? -1 : 1)) {
|
|
3618
|
+
if (found.length >= SKILL_FILES_LIMIT) {
|
|
3619
|
+
return;
|
|
3620
|
+
}
|
|
3621
|
+
const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
3622
|
+
if (!isSafeRelativePath(relative)) {
|
|
3623
|
+
continue;
|
|
3624
|
+
}
|
|
3625
|
+
if (entry.isDirectory()) {
|
|
3626
|
+
await walk(root, relative, found);
|
|
3627
|
+
} else if (entry.isFile() && !entry.name.endsWith(".tmp")) {
|
|
3628
|
+
found.push(relative);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
async function readEntry(root, relative) {
|
|
3633
|
+
let real;
|
|
3634
|
+
try {
|
|
3635
|
+
real = await realpath(path6.join(root, ...relative.split("/")));
|
|
3636
|
+
} catch {
|
|
3637
|
+
return null;
|
|
3638
|
+
}
|
|
3639
|
+
if (!inside(root, real)) {
|
|
3640
|
+
return null;
|
|
3641
|
+
}
|
|
3642
|
+
const handle = await open2(real, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK).catch(() => null);
|
|
3643
|
+
if (handle === null) {
|
|
3644
|
+
return null;
|
|
3645
|
+
}
|
|
3646
|
+
try {
|
|
3647
|
+
if (!await handleInside(root, handle)) {
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3650
|
+
const stats = await handle.stat();
|
|
3651
|
+
if (!stats.isFile()) {
|
|
3652
|
+
return null;
|
|
3653
|
+
}
|
|
3654
|
+
const buffer = Buffer.alloc(Math.min(stats.size, SKILL_TEXT_LIMIT_BYTES));
|
|
3655
|
+
let size = 0;
|
|
3656
|
+
while (size < buffer.length) {
|
|
3657
|
+
const { bytesRead } = await handle.read(buffer, size, buffer.length - size, size);
|
|
3658
|
+
if (bytesRead === 0) {
|
|
3659
|
+
break;
|
|
3660
|
+
}
|
|
3661
|
+
size += bytesRead;
|
|
3662
|
+
}
|
|
3663
|
+
return {
|
|
3664
|
+
path: relative,
|
|
3665
|
+
size: stats.size,
|
|
3666
|
+
executable: (stats.mode & 73) !== 0,
|
|
3667
|
+
...skillFileText(buffer.subarray(0, size))
|
|
3668
|
+
};
|
|
3669
|
+
} finally {
|
|
3670
|
+
await handle.close();
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
async function readSkillFolder(dir) {
|
|
3674
|
+
const info = await lstat(dir).catch(() => null);
|
|
3675
|
+
if (info === null || !info.isDirectory()) {
|
|
3676
|
+
return null;
|
|
3677
|
+
}
|
|
3678
|
+
const root = await realpath(dir).catch(() => null);
|
|
3679
|
+
if (root === null) {
|
|
3680
|
+
return null;
|
|
3681
|
+
}
|
|
3682
|
+
const found = [];
|
|
3683
|
+
await walk(root, "", found);
|
|
3684
|
+
const files = [];
|
|
3685
|
+
for (const relative of found) {
|
|
3686
|
+
const file = await readEntry(root, relative);
|
|
3687
|
+
if (file !== null) {
|
|
3688
|
+
files.push(file);
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
return files.some((file) => file.path === "SKILL.md") ? files : null;
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3498
3694
|
// packages/board-core/dist/tokens.js
|
|
3499
3695
|
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
3500
3696
|
function mintToken() {
|
|
@@ -3508,7 +3704,7 @@ function hashToken(token) {
|
|
|
3508
3704
|
var SYSTEM_ACTOR = { name: "board", role: USER_ROLE };
|
|
3509
3705
|
var CLOSING_ATTEMPTS = 3;
|
|
3510
3706
|
function ownTurn(actor, turn) {
|
|
3511
|
-
return turn.agent === actor.name && turn.scope === actor.scope && turn.thread === actor.thread && turn.channel === actor.channel;
|
|
3707
|
+
return turn.agent === actor.name && turn.scope === actor.scope && turn.thread === actor.thread && turn.channel === actor.channel && turn.place === actor.place;
|
|
3512
3708
|
}
|
|
3513
3709
|
function namesList(names) {
|
|
3514
3710
|
return names.length <= 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} and ${names.at(-1) ?? ""}`;
|
|
@@ -3521,37 +3717,45 @@ var SignInsSchema = z16.object({
|
|
|
3521
3717
|
signIns: z16.array(z16.object({
|
|
3522
3718
|
tokenHash: z16.string().min(1),
|
|
3523
3719
|
login: z16.string().min(1),
|
|
3524
|
-
/** The account's picture
|
|
3720
|
+
/** The account's picture, when GitHub gave one on its own avatar host. */
|
|
3525
3721
|
avatarUrl: z16.url().optional(),
|
|
3526
3722
|
createdAt: IsoDateTimeSchema,
|
|
3527
3723
|
expiresAt: IsoDateTimeSchema
|
|
3528
3724
|
})).default([])
|
|
3529
3725
|
});
|
|
3530
|
-
var TurnStartPayloadSchema = z16.object({
|
|
3726
|
+
var TurnStartPayloadSchema = z16.object({ turnId: UlidSchema });
|
|
3531
3727
|
var CursorsSchema = z16.object({
|
|
3532
3728
|
digest: z16.string().nullable(),
|
|
3533
3729
|
scopes: z16.record(z16.string(), z16.string()).default({}),
|
|
3534
|
-
threadsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
|
|
3535
3730
|
channelsFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
|
|
3731
|
+
placesFrom: z16.record(z16.string(), z16.string().nullable()).default({}),
|
|
3536
3732
|
/** How far a work role has read news, which goes to whichever of its general conversations reads first. */
|
|
3537
3733
|
news: z16.string().optional()
|
|
3538
3734
|
});
|
|
3539
|
-
var NO_CURSORS = {
|
|
3735
|
+
var NO_CURSORS = {
|
|
3736
|
+
digest: null,
|
|
3737
|
+
scopes: {},
|
|
3738
|
+
channelsFrom: {},
|
|
3739
|
+
placesFrom: {}
|
|
3740
|
+
};
|
|
3540
3741
|
function conversationKey(scope, part) {
|
|
3541
3742
|
return part === null ? scope : `${scope}/${part}`;
|
|
3542
3743
|
}
|
|
3543
3744
|
function channelOfPart(part) {
|
|
3544
3745
|
return part?.startsWith("#") === true ? part.slice(1) : void 0;
|
|
3545
3746
|
}
|
|
3747
|
+
function placeOfPart(part) {
|
|
3748
|
+
return part?.startsWith("~") === true ? part.slice(1) : void 0;
|
|
3749
|
+
}
|
|
3546
3750
|
function threadOfPart(part) {
|
|
3547
|
-
return part === null || part === void 0 || part.startsWith("#") ? void 0 : part;
|
|
3751
|
+
return part === null || part === void 0 || part.startsWith("#") || part.startsWith("~") ? void 0 : part;
|
|
3548
3752
|
}
|
|
3549
3753
|
function channelStart(cursors, scope) {
|
|
3550
3754
|
const from = cursors.channelsFrom[scope];
|
|
3551
3755
|
return from === void 0 ? cursors.digest : from;
|
|
3552
3756
|
}
|
|
3553
|
-
function
|
|
3554
|
-
const from = cursors.
|
|
3757
|
+
function placeStart(cursors, scope) {
|
|
3758
|
+
const from = cursors.placesFrom[scope];
|
|
3555
3759
|
return from === void 0 ? cursors.digest : from;
|
|
3556
3760
|
}
|
|
3557
3761
|
function newsStart(cursors) {
|
|
@@ -3565,7 +3769,7 @@ function cursorOf(cursors, scope, part = null) {
|
|
|
3565
3769
|
if (part === null) {
|
|
3566
3770
|
return cursors.scopes[scope] ?? cursors.digest;
|
|
3567
3771
|
}
|
|
3568
|
-
return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) :
|
|
3772
|
+
return cursors.scopes[conversationKey(scope, part)] ?? (part.startsWith("#") ? channelStart(cursors, scope) : part.startsWith("~") ? placeStart(cursors, scope) : cursors.digest);
|
|
3569
3773
|
}
|
|
3570
3774
|
function byId(a, b) {
|
|
3571
3775
|
return a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0;
|
|
@@ -3573,21 +3777,23 @@ function byId(a, b) {
|
|
|
3573
3777
|
function turnTime(record) {
|
|
3574
3778
|
return record.endedAt ?? record.startedAt;
|
|
3575
3779
|
}
|
|
3780
|
+
function turnConversation(record) {
|
|
3781
|
+
return record.thread !== void 0 ? ` in thread ${record.thread}` : record.channel !== void 0 ? ` in #${record.channel}` : record.place !== void 0 ? ` in ${record.place}'s channels` : "";
|
|
3782
|
+
}
|
|
3576
3783
|
var PausedSchema = z16.object({ paused: z16.boolean() });
|
|
3577
3784
|
var DEFAULT_LEASE_MS = 30 * 60 * 1e3;
|
|
3578
3785
|
function withoutMentions(text) {
|
|
3579
3786
|
return text.replaceAll(MENTION_PATTERN, "$1$2");
|
|
3580
3787
|
}
|
|
3581
|
-
var SKILL_TEXT_LIMIT_CHARS = 1e5;
|
|
3582
3788
|
function skillFolderOf(proposal) {
|
|
3583
3789
|
if (proposal.kind !== "skill") {
|
|
3584
3790
|
return null;
|
|
3585
3791
|
}
|
|
3586
3792
|
const parsed = SkillProposalSchema.safeParse(proposal.charter);
|
|
3587
|
-
return parsed.success
|
|
3793
|
+
return parsed.success ? parsed.data.name : null;
|
|
3588
3794
|
}
|
|
3589
3795
|
function proposedFrom(origin, where) {
|
|
3590
|
-
return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel;
|
|
3796
|
+
return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel && origin.place === where.place;
|
|
3591
3797
|
}
|
|
3592
3798
|
function megabytes(bytes) {
|
|
3593
3799
|
return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
|
|
@@ -3599,27 +3805,6 @@ var FRONT_DESK_TRIGGER = "user_post";
|
|
|
3599
3805
|
var OPS_CHANNELS = ["governance"];
|
|
3600
3806
|
var FRONT_DESK_CHANNELS = [ASK_CHANNEL];
|
|
3601
3807
|
var AlignmentsSchema = z16.object({ applied: z16.array(z16.string()).default([]) });
|
|
3602
|
-
var FOLLOW_DECISIONS = "ops-readers-follow-decisions";
|
|
3603
|
-
var THREAD_RECORDS = "threads-as-records";
|
|
3604
|
-
var DIGEST_CURSORS = "digest-replaces-inbox";
|
|
3605
|
-
var TASK_RETURNS = "task-returns-recorded";
|
|
3606
|
-
var OPS_CHANNEL_RETIRED = "ops-channel-retired";
|
|
3607
|
-
var DECISIONS_CHANNEL_RETIRED = "decisions-channel-retired";
|
|
3608
|
-
var THREAD_CONVERSATIONS = "thread-conversations";
|
|
3609
|
-
var ASKS_CHANNEL_OPENED = "asks-channel-opened";
|
|
3610
|
-
var KNOWLEDGE_REMOVAL = "knowledge-removal-granted";
|
|
3611
|
-
var CHANNEL_CONVERSATIONS = "channel-conversations";
|
|
3612
|
-
var DASHBOARD_UPDATES = "dashboard-updates-granted";
|
|
3613
|
-
var CRON_VERBS = "cron-verbs-granted";
|
|
3614
|
-
var KNOWLEDGE_NOTICES = "knowledge-notices";
|
|
3615
|
-
var IMAGE_UPLOADS = "image-uploads-granted";
|
|
3616
|
-
var KnowledgeEventSchema = z16.object({
|
|
3617
|
-
topic: NameSchema,
|
|
3618
|
-
project: NameSchema.nullable(),
|
|
3619
|
-
replaced: z16.boolean().optional()
|
|
3620
|
-
});
|
|
3621
|
-
var CRON_VERB_NAMES = ["create_cron", "update_cron", "remove_cron"];
|
|
3622
|
-
var OLD_DASHBOARD_LINE = "Agents may edit this file. It is rendered by the board UI.";
|
|
3623
3808
|
var DASHBOARD_LINE = "The project's members keep this page current with update_dashboard.";
|
|
3624
3809
|
var PLANNING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
3625
3810
|
var REALLOCATING_ROLES = [USER_ROLE, "steward", "concierge"];
|
|
@@ -3639,9 +3824,9 @@ function sentence(summary) {
|
|
|
3639
3824
|
return summary.trim().replace(/\.+$/, "");
|
|
3640
3825
|
}
|
|
3641
3826
|
function skillSummary(data, body) {
|
|
3642
|
-
const
|
|
3643
|
-
if (typeof
|
|
3644
|
-
return
|
|
3827
|
+
const description = data["description"];
|
|
3828
|
+
if (typeof description === "string" && description.trim().length > 0) {
|
|
3829
|
+
return description.trim();
|
|
3645
3830
|
}
|
|
3646
3831
|
return body.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#")) ?? "";
|
|
3647
3832
|
}
|
|
@@ -3683,8 +3868,8 @@ function restage(existing, edit) {
|
|
|
3683
3868
|
};
|
|
3684
3869
|
}
|
|
3685
3870
|
function reopenStage(stage) {
|
|
3686
|
-
const { completedBy: _by, completedAt: _at, ...
|
|
3687
|
-
return
|
|
3871
|
+
const { completedBy: _by, completedAt: _at, ...open3 } = stage;
|
|
3872
|
+
return open3;
|
|
3688
3873
|
}
|
|
3689
3874
|
function gateChanges(before, after) {
|
|
3690
3875
|
const changes = [];
|
|
@@ -3765,7 +3950,7 @@ var Board = class _Board {
|
|
|
3765
3950
|
homes;
|
|
3766
3951
|
constructor(dataDir, options) {
|
|
3767
3952
|
this.paths = new BoardPaths(dataDir);
|
|
3768
|
-
this.homes = new HomeRepos(
|
|
3953
|
+
this.homes = new HomeRepos(path7.join(this.paths.state(), "home-hooks"));
|
|
3769
3954
|
this.leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;
|
|
3770
3955
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3771
3956
|
this.events = new EventLog(this.paths.eventLog(), () => this.newId(), this.now);
|
|
@@ -3794,368 +3979,22 @@ var Board = class _Board {
|
|
|
3794
3979
|
return board2;
|
|
3795
3980
|
}
|
|
3796
3981
|
/**
|
|
3797
|
-
* Applies each one-time alignment an existing society has not had yet
|
|
3798
|
-
*
|
|
3982
|
+
* Applies each one-time alignment an existing society has not had yet, and remembers it, so a
|
|
3983
|
+
* member that later chooses otherwise keeps its choice. A new alignment is a name and a method in
|
|
3984
|
+
* this list; once every board has applied it, both are deleted. None is pending.
|
|
3799
3985
|
*/
|
|
3800
3986
|
async applyAlignments() {
|
|
3801
|
-
const
|
|
3987
|
+
const alignments = [];
|
|
3988
|
+
const file = path7.join(this.paths.state(), "alignments.json");
|
|
3802
3989
|
const { applied } = await exists(file) ? await readJson(file, AlignmentsSchema) : { applied: [] };
|
|
3803
|
-
const pending =
|
|
3804
|
-
FOLLOW_DECISIONS,
|
|
3805
|
-
THREAD_RECORDS,
|
|
3806
|
-
DIGEST_CURSORS,
|
|
3807
|
-
TASK_RETURNS,
|
|
3808
|
-
OPS_CHANNEL_RETIRED,
|
|
3809
|
-
DECISIONS_CHANNEL_RETIRED,
|
|
3810
|
-
THREAD_CONVERSATIONS,
|
|
3811
|
-
ASKS_CHANNEL_OPENED,
|
|
3812
|
-
KNOWLEDGE_REMOVAL,
|
|
3813
|
-
CHANNEL_CONVERSATIONS,
|
|
3814
|
-
DASHBOARD_UPDATES,
|
|
3815
|
-
CRON_VERBS,
|
|
3816
|
-
KNOWLEDGE_NOTICES,
|
|
3817
|
-
IMAGE_UPLOADS
|
|
3818
|
-
].filter((name) => !applied.includes(name));
|
|
3990
|
+
const pending = alignments.filter(({ name }) => !applied.includes(name));
|
|
3819
3991
|
if (pending.length === 0) {
|
|
3820
3992
|
return;
|
|
3821
3993
|
}
|
|
3822
|
-
|
|
3823
|
-
await
|
|
3824
|
-
}
|
|
3825
|
-
if (pending.includes(THREAD_RECORDS)) {
|
|
3826
|
-
await this.recordTaskThreads();
|
|
3827
|
-
}
|
|
3828
|
-
if (pending.includes(DIGEST_CURSORS)) {
|
|
3829
|
-
await this.renameInboxCursors();
|
|
3830
|
-
}
|
|
3831
|
-
if (pending.includes(TASK_RETURNS)) {
|
|
3832
|
-
await this.recordTaskReturns();
|
|
3833
|
-
}
|
|
3834
|
-
if (pending.includes(OPS_CHANNEL_RETIRED)) {
|
|
3835
|
-
await this.retireSocietyChannel("ops");
|
|
3836
|
-
}
|
|
3837
|
-
if (pending.includes(DECISIONS_CHANNEL_RETIRED)) {
|
|
3838
|
-
await this.retireSocietyChannel("decisions");
|
|
3839
|
-
}
|
|
3840
|
-
if (pending.includes(THREAD_CONVERSATIONS)) {
|
|
3841
|
-
await this.startThreadCursors();
|
|
3842
|
-
}
|
|
3843
|
-
if (pending.includes(ASKS_CHANNEL_OPENED)) {
|
|
3844
|
-
await this.openAsksChannel();
|
|
3845
|
-
}
|
|
3846
|
-
if (pending.includes(KNOWLEDGE_REMOVAL)) {
|
|
3847
|
-
await this.grantBeside("write_knowledge", "remove_knowledge");
|
|
3848
|
-
}
|
|
3849
|
-
if (pending.includes(CHANNEL_CONVERSATIONS)) {
|
|
3850
|
-
await this.startChannelCursors();
|
|
3851
|
-
}
|
|
3852
|
-
if (pending.includes(DASHBOARD_UPDATES)) {
|
|
3853
|
-
await this.grantBeside("write_knowledge", "update_dashboard");
|
|
3854
|
-
await this.rewordDashboards();
|
|
3855
|
-
}
|
|
3856
|
-
if (pending.includes(CRON_VERBS)) {
|
|
3857
|
-
await this.grantToEveryRole(CRON_VERB_NAMES);
|
|
3858
|
-
}
|
|
3859
|
-
if (pending.includes(KNOWLEDGE_NOTICES)) {
|
|
3860
|
-
await this.noticeKnowledgeNotes();
|
|
3861
|
-
}
|
|
3862
|
-
if (pending.includes(IMAGE_UPLOADS)) {
|
|
3863
|
-
await this.grantBeside("post_message", "upload_image");
|
|
3864
|
-
}
|
|
3865
|
-
await writeJson(file, { applied: [...applied, ...pending] });
|
|
3866
|
-
}
|
|
3867
|
-
/**
|
|
3868
|
-
* Gives each knowledge note from before notes carried their notice the notice it would carry
|
|
3869
|
-
* now. The event log, not the note's words, says which post is one: a knowledge verb appends
|
|
3870
|
-
* its event and then its note under the mutex, so a note is the post right after such an event,
|
|
3871
|
-
* by the same actor, at the top of the general channel of the topic's place. A post written to
|
|
3872
|
-
* look like a note follows no such event and is left as it is.
|
|
3873
|
-
*/
|
|
3874
|
-
async noticeKnowledgeNotes() {
|
|
3875
|
-
let before;
|
|
3876
|
-
for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
|
|
3877
|
-
const verb = before;
|
|
3878
|
-
before = event;
|
|
3879
|
-
if (event.type !== "message.posted" || verb === void 0 || verb.actor !== event.actor || verb.type !== "knowledge.written" && verb.type !== "knowledge.removed") {
|
|
3880
|
-
continue;
|
|
3881
|
-
}
|
|
3882
|
-
const knowledge = KnowledgeEventSchema.safeParse(verb.payload);
|
|
3883
|
-
const id = UlidSchema.safeParse(event.payload["id"]);
|
|
3884
|
-
if (!knowledge.success || !id.success || (event.payload["thread"] ?? null) !== null || event.payload["channel"] !== channelRef(knowledge.data.project, "general")) {
|
|
3885
|
-
continue;
|
|
3886
|
-
}
|
|
3887
|
-
const { topic, project, replaced } = knowledge.data;
|
|
3888
|
-
const file = this.paths.messageFile(this.paths.channelDir(channelRef(project, "general")), id.data, event.actor);
|
|
3889
|
-
if (!await exists(file)) {
|
|
3890
|
-
continue;
|
|
3891
|
-
}
|
|
3892
|
-
const { data, body, error } = await readLooseMarkdown(file);
|
|
3893
|
-
if (error !== null || data["notice"] !== void 0) {
|
|
3894
|
-
continue;
|
|
3895
|
-
}
|
|
3896
|
-
const notice = {
|
|
3897
|
-
kind: "knowledge",
|
|
3898
|
-
action: verb.type === "knowledge.removed" ? "removed" : replaced === true ? "updated" : "written",
|
|
3899
|
-
topic,
|
|
3900
|
-
project
|
|
3901
|
-
};
|
|
3902
|
-
await writeMarkdown(file, { ...data, notice }, body);
|
|
3903
|
-
}
|
|
3904
|
-
}
|
|
3905
|
-
/**
|
|
3906
|
-
* Grants `verb` once to every charter granting `beside`, the work roles' charters in practice;
|
|
3907
|
-
* seed roles have it from their seed.
|
|
3908
|
-
*/
|
|
3909
|
-
async grantBeside(beside, verb) {
|
|
3910
|
-
for (const charter of await this.listRoles()) {
|
|
3911
|
-
if (charter.verbs.includes(beside) && !charter.verbs.includes(verb)) {
|
|
3912
|
-
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, verb] }, { verbsAdded: [verb] });
|
|
3913
|
-
}
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
3916
|
-
/** Grants verbs once to every charter but the user's, which holds every verb already. */
|
|
3917
|
-
async grantToEveryRole(verbs) {
|
|
3918
|
-
for (const charter of await this.listRoles()) {
|
|
3919
|
-
const missing = verbs.filter((verb) => !charter.verbs.includes(verb));
|
|
3920
|
-
if (charter.name !== USER_ROLE && missing.length > 0) {
|
|
3921
|
-
await this.writeRoleUnlocked(USER_NAME, { ...charter, verbs: [...charter.verbs, ...missing] }, { verbsAdded: missing });
|
|
3922
|
-
}
|
|
3923
|
-
}
|
|
3924
|
-
}
|
|
3925
|
-
/** A dashboard still saying agents may edit its file says how it changes now. */
|
|
3926
|
-
async rewordDashboards() {
|
|
3927
|
-
for (const slug of await listDirs(this.paths.projects())) {
|
|
3928
|
-
const file = this.paths.dashboard(slug);
|
|
3929
|
-
if (!await exists(file)) {
|
|
3930
|
-
continue;
|
|
3931
|
-
}
|
|
3932
|
-
const { data, body } = await readLooseMarkdown(file);
|
|
3933
|
-
if (body.includes(OLD_DASHBOARD_LINE)) {
|
|
3934
|
-
await writeMarkdown(file, data, body.replace(OLD_DASHBOARD_LINE, DASHBOARD_LINE));
|
|
3935
|
-
}
|
|
3936
|
-
}
|
|
3937
|
-
}
|
|
3938
|
-
/**
|
|
3939
|
-
* A task sent back before tasks recorded it kept no trace outside the event log. A task still
|
|
3940
|
-
* short of the stage its latest `task.moved` came from is in that rework, and records it.
|
|
3941
|
-
*/
|
|
3942
|
-
async recordTaskReturns() {
|
|
3943
|
-
const moves = /* @__PURE__ */ new Map();
|
|
3944
|
-
for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
|
|
3945
|
-
const taskId = event.payload["taskId"];
|
|
3946
|
-
if (event.type === "task.moved" && typeof taskId === "string") {
|
|
3947
|
-
moves.set(taskId, event);
|
|
3948
|
-
}
|
|
3949
|
-
}
|
|
3950
|
-
for (const slug of await listDirs(this.paths.projects())) {
|
|
3951
|
-
for (const task of await this.listTasks(slug)) {
|
|
3952
|
-
const move = moves.get(task.id);
|
|
3953
|
-
const from = move?.payload["from"];
|
|
3954
|
-
if (move === void 0 || typeof from !== "string" || task.returned !== void 0 || task.completing || task.status !== "open" && task.status !== "claimed") {
|
|
3955
|
-
continue;
|
|
3956
|
-
}
|
|
3957
|
-
const back = stageIndex(task, from);
|
|
3958
|
-
if (back > stageIndex(task, task.stage)) {
|
|
3959
|
-
await this.writeTask(slug, {
|
|
3960
|
-
...task,
|
|
3961
|
-
returned: { from, by: move.actor, at: move.ts }
|
|
3962
|
-
});
|
|
3963
|
-
}
|
|
3964
|
-
}
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
/**
|
|
3968
|
-
* A society channel the board no longer uses leaves the society's list and every member's
|
|
3969
|
-
* subscriptions; its old posts stay on disk. `ops` carried operations signals, which are only
|
|
3970
|
-
* logged now; `decisions` carried proposals' outcomes, which are posts in their threads now, and
|
|
3971
|
-
* requests to the user, which are mentions of the user wherever the question belongs.
|
|
3972
|
-
*/
|
|
3973
|
-
async retireSocietyChannel(name) {
|
|
3974
|
-
const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
|
|
3975
|
-
if (doc.data.channels.includes(name)) {
|
|
3976
|
-
await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: doc.data.channels.filter((channel) => channel !== name) }, doc.body);
|
|
3977
|
-
}
|
|
3978
|
-
for (const agent of await this.listAgents()) {
|
|
3979
|
-
if (agent.subscriptions.includes(name)) {
|
|
3980
|
-
await this.updateAgent(agent.name, (a) => ({
|
|
3981
|
-
...a,
|
|
3982
|
-
subscriptions: a.subscriptions.filter((ref) => ref !== name)
|
|
3983
|
-
}));
|
|
3984
|
-
await this.refreshMember(agent.name);
|
|
3985
|
-
}
|
|
3986
|
-
}
|
|
3987
|
-
}
|
|
3988
|
-
/**
|
|
3989
|
-
* Asks once opened their threads in general, where closing one posted its summary to every
|
|
3990
|
-
* citizen and woke the steward's heartbeat for it. The society gains the asks channel, and the
|
|
3991
|
-
* front desk follows it.
|
|
3992
|
-
*/
|
|
3993
|
-
async openAsksChannel() {
|
|
3994
|
-
const doc = await readMarkdown(this.paths.societyFile(), SocietySchema);
|
|
3995
|
-
if (!doc.data.channels.includes(ASK_CHANNEL)) {
|
|
3996
|
-
await writeMarkdown(this.paths.societyFile(), { ...doc.data, channels: [...doc.data.channels, ASK_CHANNEL] }, doc.body);
|
|
3997
|
-
}
|
|
3998
|
-
await ensureDir(this.paths.societyChannel(ASK_CHANNEL));
|
|
3999
|
-
for (const agent of await this.listAgents()) {
|
|
4000
|
-
if (agent.status !== "active" || agent.subscriptions.includes(ASK_CHANNEL)) {
|
|
4001
|
-
continue;
|
|
4002
|
-
}
|
|
4003
|
-
if (!(await this.readRole(agent.role)).wakeTriggers.includes(FRONT_DESK_TRIGGER)) {
|
|
4004
|
-
continue;
|
|
4005
|
-
}
|
|
4006
|
-
await this.updateAgent(agent.name, (a) => ({
|
|
4007
|
-
...a,
|
|
4008
|
-
subscriptions: [...a.subscriptions, ASK_CHANNEL]
|
|
4009
|
-
}));
|
|
4010
|
-
await this.refreshMember(agent.name);
|
|
4011
|
-
await this.events.append("subscription.changed", agent.name, {
|
|
4012
|
-
channel: ASK_CHANNEL,
|
|
4013
|
-
subscribed: true,
|
|
4014
|
-
aligned: true
|
|
4015
|
-
});
|
|
4016
|
-
}
|
|
4017
|
-
}
|
|
4018
|
-
/** Members that predate `decisions` among the ops channels read proposals without their outcome. */
|
|
4019
|
-
async followDecisions() {
|
|
4020
|
-
if (!(await this.society()).channels.includes("decisions")) {
|
|
4021
|
-
return;
|
|
4022
|
-
}
|
|
4023
|
-
for (const agent of await this.listAgents()) {
|
|
4024
|
-
if (agent.status !== "active" || agent.subscriptions.includes("decisions")) {
|
|
4025
|
-
continue;
|
|
4026
|
-
}
|
|
4027
|
-
if (!(await this.readRole(agent.role)).wakeTriggers.includes(OPS_WAKE_TRIGGER)) {
|
|
4028
|
-
continue;
|
|
4029
|
-
}
|
|
4030
|
-
await this.updateAgent(agent.name, (a) => ({
|
|
4031
|
-
...a,
|
|
4032
|
-
subscriptions: [...a.subscriptions, "decisions"]
|
|
4033
|
-
}));
|
|
4034
|
-
await this.refreshMember(agent.name);
|
|
4035
|
-
await this.events.append("subscription.changed", agent.name, {
|
|
4036
|
-
channel: "decisions",
|
|
4037
|
-
subscribed: true,
|
|
4038
|
-
aligned: true
|
|
4039
|
-
});
|
|
4040
|
-
}
|
|
4041
|
-
}
|
|
4042
|
-
/**
|
|
4043
|
-
* Threads were once a state field on their task. Each becomes a record under the task's project,
|
|
4044
|
-
* on its general channel, closed if its task has ended, and the field leaves the task file.
|
|
4045
|
-
*/
|
|
4046
|
-
async recordTaskThreads() {
|
|
4047
|
-
const opened = /* @__PURE__ */ new Map();
|
|
4048
|
-
const closed = /* @__PURE__ */ new Map();
|
|
4049
|
-
for (const event of await this.events.readSince(null, Number.POSITIVE_INFINITY)) {
|
|
4050
|
-
const taskId = event.payload["taskId"];
|
|
4051
|
-
if (typeof taskId !== "string") {
|
|
4052
|
-
continue;
|
|
4053
|
-
}
|
|
4054
|
-
if (event.type === "thread.opened") {
|
|
4055
|
-
opened.set(taskId, event);
|
|
4056
|
-
} else if (event.type === "thread.closed") {
|
|
4057
|
-
closed.set(taskId, event);
|
|
4058
|
-
}
|
|
4059
|
-
}
|
|
4060
|
-
for (const slug of await listDirs(this.paths.projects())) {
|
|
4061
|
-
const threads = this.paths.threads(slug);
|
|
4062
|
-
const summaries = /* @__PURE__ */ new Map();
|
|
4063
|
-
for (const message of await this.readMessagesIn(this.paths.projectChannel(slug, "general"), null)) {
|
|
4064
|
-
if (message.task !== void 0) {
|
|
4065
|
-
summaries.set(message.task, message.body.replace(/^Thread closed[^\n]*\n\n/, ""));
|
|
4066
|
-
}
|
|
4067
|
-
}
|
|
4068
|
-
for (const file of await listFiles(this.paths.tasks(slug))) {
|
|
4069
|
-
const raw = await readMarkdown(path6.join(this.paths.tasks(slug), file), z16.record(z16.string(), z16.unknown()));
|
|
4070
|
-
const legacy = raw.data["thread"];
|
|
4071
|
-
if (legacy === void 0) {
|
|
4072
|
-
continue;
|
|
4073
|
-
}
|
|
4074
|
-
const task = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
|
|
4075
|
-
if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task.id))) {
|
|
4076
|
-
const ended = task.status === "done" || task.status === "abandoned";
|
|
4077
|
-
const open2 = legacy === "open" && !ended;
|
|
4078
|
-
const openedBy = opened.get(task.id);
|
|
4079
|
-
const closedBy = closed.get(task.id);
|
|
4080
|
-
await this.writeThread(threads, {
|
|
4081
|
-
id: task.id,
|
|
4082
|
-
channel: channelRef(slug, "general"),
|
|
4083
|
-
title: task.title,
|
|
4084
|
-
subject: { kind: "task", id: task.id },
|
|
4085
|
-
state: open2 ? "open" : "closed",
|
|
4086
|
-
openedBy: openedBy?.actor ?? task.createdBy,
|
|
4087
|
-
openedAt: openedBy?.ts ?? task.createdAt,
|
|
4088
|
-
...open2 ? {} : {
|
|
4089
|
-
closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
|
|
4090
|
-
closedAt: closedBy?.ts ?? this.now().toISOString()
|
|
4091
|
-
},
|
|
4092
|
-
body: summaries.get(task.id) ?? ""
|
|
4093
|
-
});
|
|
4094
|
-
}
|
|
4095
|
-
await this.writeTask(slug, task);
|
|
4096
|
-
}
|
|
3994
|
+
for (const { apply } of pending) {
|
|
3995
|
+
await apply();
|
|
4097
3996
|
}
|
|
4098
|
-
|
|
4099
|
-
/**
|
|
4100
|
-
* Every channel of a scope was once read in its home conversation, so a member had read each up
|
|
4101
|
-
* to its scope's cursor. Each channel beside general starts from that cursor now, as each takes a
|
|
4102
|
-
* conversation of its own.
|
|
4103
|
-
*/
|
|
4104
|
-
async startChannelCursors() {
|
|
4105
|
-
for (const agent of await this.listAgents()) {
|
|
4106
|
-
const file = this.paths.agentCursors(agent.name);
|
|
4107
|
-
if (!await exists(file)) {
|
|
4108
|
-
continue;
|
|
4109
|
-
}
|
|
4110
|
-
const cursors = await readJson(file, CursorsSchema);
|
|
4111
|
-
const scopes = /* @__PURE__ */ new Set([
|
|
4112
|
-
...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
|
|
4113
|
-
...agent.memberships,
|
|
4114
|
-
SOCIETY_SCOPE
|
|
4115
|
-
]);
|
|
4116
|
-
const channelsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
|
|
4117
|
-
await writeJson(file, { ...cursors, channelsFrom });
|
|
4118
|
-
}
|
|
4119
|
-
}
|
|
4120
|
-
/**
|
|
4121
|
-
* Threads were once read in the scope's single conversation, so a member had read every thread
|
|
4122
|
-
* up to its scope's cursor. Each scope's threads start from that cursor now, as each thread's
|
|
4123
|
-
* conversation takes a cursor of its own.
|
|
4124
|
-
*/
|
|
4125
|
-
async startThreadCursors() {
|
|
4126
|
-
for (const agent of await this.listAgents()) {
|
|
4127
|
-
const file = this.paths.agentCursors(agent.name);
|
|
4128
|
-
if (!await exists(file)) {
|
|
4129
|
-
continue;
|
|
4130
|
-
}
|
|
4131
|
-
const cursors = await readJson(file, CursorsSchema);
|
|
4132
|
-
const scopes = /* @__PURE__ */ new Set([
|
|
4133
|
-
...Object.keys(cursors.scopes).filter((key) => !key.includes("/")),
|
|
4134
|
-
...agent.memberships,
|
|
4135
|
-
SOCIETY_SCOPE
|
|
4136
|
-
]);
|
|
4137
|
-
const threadsFrom = Object.fromEntries([...scopes].map((scope) => [scope, cursorOf(cursors, scope)]));
|
|
4138
|
-
await writeJson(file, { ...cursors, threadsFrom });
|
|
4139
|
-
}
|
|
4140
|
-
}
|
|
4141
|
-
/**
|
|
4142
|
-
* The digest was once called the inbox: each cursor file keyed its cursor `inbox`, and the user
|
|
4143
|
-
* followed every society channel and project general channel to read one. The user takes no
|
|
4144
|
-
* turns, so it follows nothing now.
|
|
4145
|
-
*/
|
|
4146
|
-
async renameInboxCursors() {
|
|
4147
|
-
const Legacy = z16.object({ inbox: z16.string().nullable() });
|
|
4148
|
-
for (const agent of await this.listAgents()) {
|
|
4149
|
-
const file = this.paths.agentCursors(agent.name);
|
|
4150
|
-
if (await exists(file)) {
|
|
4151
|
-
const legacy = Legacy.safeParse(JSON.parse(await readFile4(file, "utf8")));
|
|
4152
|
-
if (legacy.success) {
|
|
4153
|
-
await writeJson(file, { digest: legacy.data.inbox });
|
|
4154
|
-
}
|
|
4155
|
-
}
|
|
4156
|
-
}
|
|
4157
|
-
await this.updateAgent(USER_NAME, (user) => ({ ...user, subscriptions: [] }));
|
|
4158
|
-
await this.refreshMember(USER_NAME);
|
|
3997
|
+
await writeJson(file, { applied: [...applied, ...pending.map(({ name }) => name)] });
|
|
4159
3998
|
}
|
|
4160
3999
|
/** Writes every missing seed charter and aligns the existing ones with their seed. */
|
|
4161
4000
|
async ensureSeedRoles() {
|
|
@@ -4257,13 +4096,15 @@ ${charter.purpose}
|
|
|
4257
4096
|
const token = mintToken();
|
|
4258
4097
|
const thread = threadOfPart(part);
|
|
4259
4098
|
const channel = channelOfPart(part);
|
|
4099
|
+
const place = placeOfPart(part);
|
|
4260
4100
|
this.turnTokens.set(hashToken(token), {
|
|
4261
4101
|
actor: {
|
|
4262
4102
|
name: agent,
|
|
4263
4103
|
role,
|
|
4264
4104
|
...scope === void 0 ? {} : { scope },
|
|
4265
4105
|
...thread === void 0 ? {} : { thread },
|
|
4266
|
-
...channel === void 0 ? {} : { channel }
|
|
4106
|
+
...channel === void 0 ? {} : { channel },
|
|
4107
|
+
...place === void 0 ? {} : { place }
|
|
4267
4108
|
},
|
|
4268
4109
|
expiresAt: this.now().getTime() + ttlMs
|
|
4269
4110
|
});
|
|
@@ -4352,7 +4193,7 @@ ${charter.purpose}
|
|
|
4352
4193
|
});
|
|
4353
4194
|
}
|
|
4354
4195
|
signInsFile() {
|
|
4355
|
-
return
|
|
4196
|
+
return path7.join(this.paths.state(), "sign-ins.json");
|
|
4356
4197
|
}
|
|
4357
4198
|
async readSignIns() {
|
|
4358
4199
|
const file = this.signInsFile();
|
|
@@ -4449,7 +4290,7 @@ ${charter.purpose}
|
|
|
4449
4290
|
const files = await listFiles(this.paths.tasks(project));
|
|
4450
4291
|
const tasks = [];
|
|
4451
4292
|
for (const file of files) {
|
|
4452
|
-
const doc = await readMarkdown(
|
|
4293
|
+
const doc = await readMarkdown(path7.join(this.paths.tasks(project), file), TaskFrontmatterSchema);
|
|
4453
4294
|
tasks.push({ ...doc.data, body: doc.body });
|
|
4454
4295
|
}
|
|
4455
4296
|
return tasks;
|
|
@@ -4468,7 +4309,7 @@ ${charter.purpose}
|
|
|
4468
4309
|
const files = await listFiles(this.paths.proposals());
|
|
4469
4310
|
const proposals = [];
|
|
4470
4311
|
for (const file of files) {
|
|
4471
|
-
const doc = await readMarkdown(
|
|
4312
|
+
const doc = await readMarkdown(path7.join(this.paths.proposals(), file), ProposalFrontmatterSchema);
|
|
4472
4313
|
proposals.push({ ...doc.data, body: doc.body });
|
|
4473
4314
|
}
|
|
4474
4315
|
return proposals;
|
|
@@ -4495,7 +4336,7 @@ ${charter.purpose}
|
|
|
4495
4336
|
async listMembers() {
|
|
4496
4337
|
const members = [];
|
|
4497
4338
|
for (const file of await listFiles(this.paths.members())) {
|
|
4498
|
-
const doc = await readMarkdown(
|
|
4339
|
+
const doc = await readMarkdown(path7.join(this.paths.members(), file), MemberSchema);
|
|
4499
4340
|
members.push({ ...doc.data, profile: doc.body });
|
|
4500
4341
|
}
|
|
4501
4342
|
return members;
|
|
@@ -4514,7 +4355,7 @@ ${charter.purpose}
|
|
|
4514
4355
|
const dir = project === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project);
|
|
4515
4356
|
const topics = [];
|
|
4516
4357
|
for (const file of await listFiles(dir)) {
|
|
4517
|
-
topics.push(await this.knowledgeAt(
|
|
4358
|
+
topics.push(await this.knowledgeAt(path7.join(dir, file), project, file.replace(/\.md$/, "")));
|
|
4518
4359
|
}
|
|
4519
4360
|
return topics;
|
|
4520
4361
|
}
|
|
@@ -4544,7 +4385,7 @@ ${charter.purpose}
|
|
|
4544
4385
|
const aside = this.paths.removedKnowledge(project ?? SOCIETY_SCOPE);
|
|
4545
4386
|
let highest = 0;
|
|
4546
4387
|
for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
|
|
4547
|
-
const copy = await this.knowledgeAt(
|
|
4388
|
+
const copy = await this.knowledgeAt(path7.join(aside, file), project, topic);
|
|
4548
4389
|
highest = Math.max(highest, copy.revision);
|
|
4549
4390
|
}
|
|
4550
4391
|
return highest;
|
|
@@ -4560,7 +4401,7 @@ ${charter.purpose}
|
|
|
4560
4401
|
async listSkillsIn(dir, scope) {
|
|
4561
4402
|
const skills = [];
|
|
4562
4403
|
for (const entry of await listDirs(dir)) {
|
|
4563
|
-
const file =
|
|
4404
|
+
const file = path7.join(dir, entry, "SKILL.md");
|
|
4564
4405
|
if (!await exists(file)) {
|
|
4565
4406
|
continue;
|
|
4566
4407
|
}
|
|
@@ -4650,7 +4491,7 @@ ${charter.purpose}
|
|
|
4650
4491
|
if (!wanted.delete(file.slice(0, 26))) {
|
|
4651
4492
|
continue;
|
|
4652
4493
|
}
|
|
4653
|
-
const { data } = await readMarkdown(
|
|
4494
|
+
const { data } = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
|
|
4654
4495
|
found.push(MessageRefSchema.parse(data));
|
|
4655
4496
|
}
|
|
4656
4497
|
if (wanted.size === 0) {
|
|
@@ -4682,7 +4523,7 @@ ${charter.purpose}
|
|
|
4682
4523
|
if (!file.endsWith(".md")) {
|
|
4683
4524
|
continue;
|
|
4684
4525
|
}
|
|
4685
|
-
const doc = await readMarkdown(
|
|
4526
|
+
const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
|
|
4686
4527
|
if (doc.data.state !== "open") {
|
|
4687
4528
|
continue;
|
|
4688
4529
|
}
|
|
@@ -4709,7 +4550,7 @@ ${charter.purpose}
|
|
|
4709
4550
|
const all = [];
|
|
4710
4551
|
for (const threads of await this.threadScopes()) {
|
|
4711
4552
|
for (const file of await listFiles(threads)) {
|
|
4712
|
-
const doc = await readMarkdown(
|
|
4553
|
+
const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
|
|
4713
4554
|
const messages = await listFiles(this.paths.threadMessages(threads, doc.data.id));
|
|
4714
4555
|
const last = messages.at(-1);
|
|
4715
4556
|
all.push({
|
|
@@ -4957,7 +4798,7 @@ ${DASHBOARD_LINE}
|
|
|
4957
4798
|
if (event.type === "turn.started") {
|
|
4958
4799
|
const start = TurnStartPayloadSchema.safeParse(event.payload);
|
|
4959
4800
|
if (start.success)
|
|
4960
|
-
starts.set(start.data.turnId
|
|
4801
|
+
starts.set(start.data.turnId, event.ts);
|
|
4961
4802
|
continue;
|
|
4962
4803
|
}
|
|
4963
4804
|
if (event.type !== "turn.completed" && event.type !== "turn.failed") {
|
|
@@ -4965,9 +4806,11 @@ ${DASHBOARD_LINE}
|
|
|
4965
4806
|
}
|
|
4966
4807
|
const parsed = TurnHistoryPayloadSchema.safeParse(event.payload);
|
|
4967
4808
|
if (parsed.success) {
|
|
4968
|
-
const
|
|
4969
|
-
const startedAt = starts.get(
|
|
4970
|
-
|
|
4809
|
+
const { turnId } = parsed.data;
|
|
4810
|
+
const startedAt = turnId === void 0 ? void 0 : starts.get(turnId);
|
|
4811
|
+
if (turnId !== void 0) {
|
|
4812
|
+
starts.delete(turnId);
|
|
4813
|
+
}
|
|
4971
4814
|
entries.push({
|
|
4972
4815
|
id: event.id,
|
|
4973
4816
|
ts: event.ts,
|
|
@@ -5076,16 +4919,20 @@ ${DASHBOARD_LINE}
|
|
|
5076
4919
|
}
|
|
5077
4920
|
/**
|
|
5078
4921
|
* How many unread digest messages a member has in each conversation: a scope's home, for its
|
|
5079
|
-
* general channel, one of its other channels,
|
|
4922
|
+
* general channel, one of its other channels, one of its threads, or, in the society scope, a
|
|
4923
|
+
* project's channels. The heartbeat asks, so that it wakes a member where its unread
|
|
5080
4924
|
* messages are and nowhere else. Society channels count only for a member woken in the society
|
|
5081
4925
|
* scope; a project member reads them at its next turn. Two kinds of post are in the digest but not
|
|
5082
4926
|
* counted, and are read when the member wakes for something else: a task step's, which wakes
|
|
5083
4927
|
* whoever it hands work to, and the board's own, operations signals and landing announcements,
|
|
5084
|
-
* whose signals that call for action wake their readers directly.
|
|
4928
|
+
* whose signals that call for action wake their readers directly. A closed thread counts for
|
|
4929
|
+
* nothing: its conversation has ended, and a turn there could only read its posts and be refused
|
|
4930
|
+
* a reply, while a wake queued before it closed, as for a mention, still runs there.
|
|
5085
4931
|
*/
|
|
5086
4932
|
async unreadByConversation(actor) {
|
|
5087
4933
|
const { cursors, upto } = await this.digestSnapshot(actor.name);
|
|
5088
4934
|
const counts = /* @__PURE__ */ new Map();
|
|
4935
|
+
const closed = /* @__PURE__ */ new Map();
|
|
5089
4936
|
const unscoped = { name: actor.name, role: actor.role };
|
|
5090
4937
|
for (const { scope, part, message, news } of await this.digestSince(unscoped, cursors, upto)) {
|
|
5091
4938
|
if (news === true || message.step !== void 0 || message.author === SYSTEM_ACTOR.name) {
|
|
@@ -5095,11 +4942,23 @@ ${DASHBOARD_LINE}
|
|
|
5095
4942
|
if (posted !== scope && scope !== SOCIETY_SCOPE) {
|
|
5096
4943
|
continue;
|
|
5097
4944
|
}
|
|
4945
|
+
const thread = threadOfPart(part);
|
|
4946
|
+
if (thread !== void 0) {
|
|
4947
|
+
let ended = closed.get(thread);
|
|
4948
|
+
if (ended === void 0) {
|
|
4949
|
+
ended = (await this.tryFindThread(thread))?.thread.state === "closed";
|
|
4950
|
+
closed.set(thread, ended);
|
|
4951
|
+
}
|
|
4952
|
+
if (ended) {
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
}
|
|
5098
4956
|
const key = conversationKey(scope, part);
|
|
5099
4957
|
const entry = counts.get(key) ?? {
|
|
5100
4958
|
scope,
|
|
5101
|
-
thread:
|
|
4959
|
+
thread: thread ?? null,
|
|
5102
4960
|
channel: channelOfPart(part) ?? null,
|
|
4961
|
+
place: placeOfPart(part) ?? null,
|
|
5103
4962
|
count: 0
|
|
5104
4963
|
};
|
|
5105
4964
|
counts.set(key, { ...entry, count: entry.count + 1 });
|
|
@@ -5157,8 +5016,9 @@ ${DASHBOARD_LINE}
|
|
|
5157
5016
|
* thread it takes part in, and for the front desk, which every post by the user wakes, every post
|
|
5158
5017
|
* by the user wherever it is. With a conversation, only what is filed there: for a thread, every
|
|
5159
5018
|
* message of the thread, since the conversation is about it; for a channel's, that channel's
|
|
5160
|
-
* top-level messages; for a
|
|
5161
|
-
*
|
|
5019
|
+
* top-level messages; for a project's in the society scope, the top-level messages of every
|
|
5020
|
+
* channel of a project the member is not in; for a scope's home, general's, since each thread,
|
|
5021
|
+
* each other channel, and each such project is a conversation of its own. Nothing newer than
|
|
5162
5022
|
* `upto` counts (see `digestSnapshot`).
|
|
5163
5023
|
*/
|
|
5164
5024
|
async digestSince(actor, cursors, upto, conversation) {
|
|
@@ -5178,11 +5038,17 @@ ${DASHBOARD_LINE}
|
|
|
5178
5038
|
const frontDesk = charter.wakeTriggers.includes(FRONT_DESK_TRIGGER);
|
|
5179
5039
|
const threadParticipation = /* @__PURE__ */ new Map();
|
|
5180
5040
|
const openChannels = /* @__PURE__ */ new Map();
|
|
5041
|
+
const liveProjects = /* @__PURE__ */ new Map();
|
|
5181
5042
|
const collected = [];
|
|
5182
5043
|
const newsFrom = charter.societyScope ? void 0 : newsStart(cursors);
|
|
5183
5044
|
const scopes = conversation !== void 0 ? [conversation.scope] : [...agent.memberships, SOCIETY_SCOPE];
|
|
5184
5045
|
const starts = [
|
|
5185
|
-
...scopes.flatMap((each) => conversation === void 0 ? [
|
|
5046
|
+
...scopes.flatMap((each) => conversation === void 0 ? [
|
|
5047
|
+
cursorOf(cursors, each),
|
|
5048
|
+
cursors.digest,
|
|
5049
|
+
channelStart(cursors, each),
|
|
5050
|
+
placeStart(cursors, each)
|
|
5051
|
+
] : [cursorOf(cursors, each, conversation.part)]),
|
|
5186
5052
|
...newsFrom === void 0 ? [] : [newsFrom]
|
|
5187
5053
|
];
|
|
5188
5054
|
const known = starts.filter((start) => start !== null);
|
|
@@ -5192,7 +5058,7 @@ ${DASHBOARD_LINE}
|
|
|
5192
5058
|
const thread = message.thread ?? null;
|
|
5193
5059
|
const asked = message.mentions.includes(actor.name) || frontDesk && message.author === USER_NAME;
|
|
5194
5060
|
if (newsFrom !== void 0 && thread === null && filedIn === SOCIETY_SCOPE && !asked) {
|
|
5195
|
-
if ((newsFrom === null || message.id > newsFrom) && subscribed.has(message.channel) && message.author !== actor.name) {
|
|
5061
|
+
if ((conversation === void 0 || conversation.part === null) && (newsFrom === null || message.id > newsFrom) && subscribed.has(message.channel) && message.author !== actor.name) {
|
|
5196
5062
|
collected.push({
|
|
5197
5063
|
scope: conversation?.scope ?? SOCIETY_SCOPE,
|
|
5198
5064
|
part: null,
|
|
@@ -5203,12 +5069,18 @@ ${DASHBOARD_LINE}
|
|
|
5203
5069
|
continue;
|
|
5204
5070
|
}
|
|
5205
5071
|
const named = thread === null ? channelConversation(filedIn, message.channel) : void 0;
|
|
5206
|
-
let
|
|
5207
|
-
if (named !== void 0 &&
|
|
5208
|
-
|
|
5209
|
-
openChannels.set(message.channel,
|
|
5210
|
-
}
|
|
5211
|
-
const
|
|
5072
|
+
let open3 = named === void 0 ? void 0 : openChannels.get(message.channel);
|
|
5073
|
+
if (named !== void 0 && open3 === void 0) {
|
|
5074
|
+
open3 = await this.channelOpen(message.channel);
|
|
5075
|
+
openChannels.set(message.channel, open3);
|
|
5076
|
+
}
|
|
5077
|
+
const project = thread === null ? placeConversation(filedIn, message.channel) : void 0;
|
|
5078
|
+
let live = project === void 0 ? void 0 : liveProjects.get(project);
|
|
5079
|
+
if (project !== void 0 && live === void 0) {
|
|
5080
|
+
live = await this.projectLive(project);
|
|
5081
|
+
liveProjects.set(project, live);
|
|
5082
|
+
}
|
|
5083
|
+
const part = thread ?? (open3 === true ? `#${named}` : live === true ? `~${project}` : null);
|
|
5212
5084
|
if (conversation !== void 0 && (filedIn !== conversation.scope || part !== conversation.part)) {
|
|
5213
5085
|
continue;
|
|
5214
5086
|
}
|
|
@@ -5273,7 +5145,7 @@ ${task.body}`;
|
|
|
5273
5145
|
}
|
|
5274
5146
|
}
|
|
5275
5147
|
for (const file of await listFiles(this.paths.projectKnowledge(project))) {
|
|
5276
|
-
const content = await readFile4(
|
|
5148
|
+
const content = await readFile4(path7.join(this.paths.projectKnowledge(project), file), "utf8");
|
|
5277
5149
|
if (content.toLowerCase().includes(needle)) {
|
|
5278
5150
|
hits.push({
|
|
5279
5151
|
kind: "knowledge",
|
|
@@ -5294,7 +5166,7 @@ ${task.body}`;
|
|
|
5294
5166
|
shared.push({
|
|
5295
5167
|
kind: "knowledge",
|
|
5296
5168
|
ref: `society/knowledge/${file}`,
|
|
5297
|
-
file:
|
|
5169
|
+
file: path7.join(this.paths.societyKnowledge(), file)
|
|
5298
5170
|
});
|
|
5299
5171
|
}
|
|
5300
5172
|
for (const skill of await this.listSocietySkills()) {
|
|
@@ -5307,7 +5179,7 @@ ${task.body}`;
|
|
|
5307
5179
|
shared.push({
|
|
5308
5180
|
kind: "memory",
|
|
5309
5181
|
ref: `memory/${file}`,
|
|
5310
|
-
file:
|
|
5182
|
+
file: path7.join(this.paths.agentMemory(actor.name), file)
|
|
5311
5183
|
});
|
|
5312
5184
|
}
|
|
5313
5185
|
for (const entry of shared) {
|
|
@@ -5397,7 +5269,7 @@ ${current.body}`);
|
|
|
5397
5269
|
});
|
|
5398
5270
|
const aside = this.paths.removedKnowledge(args.project ?? SOCIETY_SCOPE);
|
|
5399
5271
|
await mkdir4(aside, { recursive: true });
|
|
5400
|
-
await rename3(file,
|
|
5272
|
+
await rename3(file, path7.join(aside, `${args.topic}.${removed.removedAt.replaceAll(":", "-")}.md`));
|
|
5401
5273
|
await this.events.append("knowledge.removed", actor.name, {
|
|
5402
5274
|
topic: args.topic,
|
|
5403
5275
|
project: args.project
|
|
@@ -5445,7 +5317,7 @@ ${current.body}`);
|
|
|
5445
5317
|
});
|
|
5446
5318
|
const aside = this.paths.removedSkills();
|
|
5447
5319
|
await mkdir4(aside, { recursive: true });
|
|
5448
|
-
await rename3(this.paths.societySkill(args.name),
|
|
5320
|
+
await rename3(this.paths.societySkill(args.name), path7.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
|
|
5449
5321
|
await this.events.append("skill.removed", actor.name, {
|
|
5450
5322
|
name: args.name,
|
|
5451
5323
|
reason: args.reason
|
|
@@ -5522,7 +5394,7 @@ ${current.body}`);
|
|
|
5522
5394
|
type,
|
|
5523
5395
|
size: bytes.byteLength
|
|
5524
5396
|
});
|
|
5525
|
-
const name =
|
|
5397
|
+
const name = path7.basename(args.path).replaceAll(/[[\]\\\r\n]/g, "");
|
|
5526
5398
|
return {
|
|
5527
5399
|
id,
|
|
5528
5400
|
type,
|
|
@@ -5548,7 +5420,7 @@ ${current.body}`);
|
|
|
5548
5420
|
async listCrons() {
|
|
5549
5421
|
const crons = [];
|
|
5550
5422
|
for (const file of await listFiles(this.paths.crons())) {
|
|
5551
|
-
crons.push(await this.readCronFile(
|
|
5423
|
+
crons.push(await this.readCronFile(path7.join(this.paths.crons(), file)));
|
|
5552
5424
|
}
|
|
5553
5425
|
return crons;
|
|
5554
5426
|
}
|
|
@@ -5670,11 +5542,16 @@ ${current.body}`);
|
|
|
5670
5542
|
}
|
|
5671
5543
|
/**
|
|
5672
5544
|
* Where a cron fires for its citizen: the thread, the channel, or the scope's home it names, else
|
|
5673
|
-
* the conversation of the turn that set it when it wakes its setter,
|
|
5545
|
+
* the conversation of the turn that set it when it wakes its setter, a project's channels' in
|
|
5546
|
+
* the society scope included, else its home outside projects.
|
|
5674
5547
|
*/
|
|
5675
5548
|
async cronConversation(actor, agent, named) {
|
|
5676
5549
|
const own = actor.name === agent.name;
|
|
5677
5550
|
if (named.project === void 0 && named.thread_id === void 0 && named.channel === void 0) {
|
|
5551
|
+
if (own && actor.scope === SOCIETY_SCOPE && actor.place !== void 0) {
|
|
5552
|
+
await this.readActiveProject(actor.place);
|
|
5553
|
+
return { scope: SOCIETY_SCOPE, place: actor.place };
|
|
5554
|
+
}
|
|
5678
5555
|
return own && actor.scope !== void 0 ? this.cronConversation(actor, agent, {
|
|
5679
5556
|
project: actor.scope,
|
|
5680
5557
|
thread_id: actor.thread,
|
|
@@ -5750,7 +5627,8 @@ ${current.body}`);
|
|
|
5750
5627
|
agent: cron.agent,
|
|
5751
5628
|
project: cron.scope,
|
|
5752
5629
|
...cron.thread === void 0 ? {} : { thread: cron.thread },
|
|
5753
|
-
...cron.channel === void 0 ? {} : { channel: cron.channel }
|
|
5630
|
+
...cron.channel === void 0 ? {} : { channel: cron.channel },
|
|
5631
|
+
...cron.place === void 0 ? {} : { place: cron.place }
|
|
5754
5632
|
};
|
|
5755
5633
|
}
|
|
5756
5634
|
async readCronFile(file) {
|
|
@@ -5783,11 +5661,12 @@ ${current.body}`);
|
|
|
5783
5661
|
const args = VerbInputs.open_thread.parse(input);
|
|
5784
5662
|
await this.authorize(actor, "open_thread");
|
|
5785
5663
|
return this.mutex.run(async () => {
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5664
|
+
await this.assertChannelOpen(args.channel);
|
|
5665
|
+
return this.openThreadUnlocked(actor.name, {
|
|
5666
|
+
id: this.newId(),
|
|
5667
|
+
channel: args.channel,
|
|
5668
|
+
title: args.title
|
|
5669
|
+
});
|
|
5791
5670
|
});
|
|
5792
5671
|
}
|
|
5793
5672
|
/** Writes a new open thread and its event. Callers hold the mutex and chose where it hangs. */
|
|
@@ -5822,28 +5701,13 @@ ${current.body}`);
|
|
|
5822
5701
|
subject: { kind: "proposal", id: proposal.id }
|
|
5823
5702
|
};
|
|
5824
5703
|
}
|
|
5825
|
-
/**
|
|
5826
|
-
|
|
5827
|
-
* threads gets one now, opened as its proposer, and a thread closed before the decision opens
|
|
5828
|
-
* again.
|
|
5829
|
-
*/
|
|
5830
|
-
async proposalThreadUnlocked(by, proposal) {
|
|
5704
|
+
/** The open thread of a proposal being decided, which `propose` opened with it. */
|
|
5705
|
+
async proposalThreadUnlocked(proposal) {
|
|
5831
5706
|
const found = await this.tryFindThread(proposal.id);
|
|
5832
|
-
if (found?.thread.state
|
|
5833
|
-
|
|
5707
|
+
if (found?.thread.state !== "open") {
|
|
5708
|
+
throw new BoardError("INVALID_STATE", `proposal ${proposal.id} has no open thread`);
|
|
5834
5709
|
}
|
|
5835
|
-
|
|
5836
|
-
return this.openThreadUnlocked(proposal.proposedBy, this.proposalThreadOpening(proposal));
|
|
5837
|
-
}
|
|
5838
|
-
const { closedBy: _by, closedAt: _at, ...kept } = found.thread;
|
|
5839
|
-
const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
|
|
5840
|
-
await this.events.append("thread.opened", by, {
|
|
5841
|
-
threadId: proposal.id,
|
|
5842
|
-
channel: reopened.channel,
|
|
5843
|
-
subject: reopened.subject ?? null,
|
|
5844
|
-
reopened: true
|
|
5845
|
-
});
|
|
5846
|
-
return reopened;
|
|
5710
|
+
return found.thread;
|
|
5847
5711
|
}
|
|
5848
5712
|
/** Where a task's thread hangs, what it is called, and what it is about. */
|
|
5849
5713
|
taskThreadOpening(task) {
|
|
@@ -5855,79 +5719,21 @@ ${current.body}`);
|
|
|
5855
5719
|
};
|
|
5856
5720
|
}
|
|
5857
5721
|
/**
|
|
5858
|
-
* The open thread of a task in play, for a verb about to post into it
|
|
5859
|
-
*
|
|
5860
|
-
* again; an ended task's thread stays closed.
|
|
5722
|
+
* The open thread of a task in play, for a verb about to post into it, which `createTask` opened
|
|
5723
|
+
* with the task; an ended task's thread closed with it.
|
|
5861
5724
|
*/
|
|
5862
|
-
async taskThreadUnlocked(
|
|
5725
|
+
async taskThreadUnlocked(task) {
|
|
5863
5726
|
const found = await this.tryFindThread(task.id);
|
|
5864
5727
|
if (found?.thread.state === "open") {
|
|
5865
5728
|
return found.thread;
|
|
5866
5729
|
}
|
|
5867
|
-
|
|
5868
|
-
throw new BoardError("INVALID_STATE", `task ${task.id} is ${task.status} and its thread closed with it`);
|
|
5869
|
-
}
|
|
5870
|
-
if (found === null) {
|
|
5871
|
-
return this.openThreadUnlocked(by, this.taskThreadOpening(task));
|
|
5872
|
-
}
|
|
5873
|
-
const { closedBy: _by, closedAt: _at, ...kept } = found.thread;
|
|
5874
|
-
const reopened = await this.writeThread(found.threads, { ...kept, state: "open" });
|
|
5875
|
-
await this.events.append("thread.opened", by, {
|
|
5876
|
-
threadId: task.id,
|
|
5877
|
-
channel: reopened.channel,
|
|
5878
|
-
subject: reopened.subject ?? null,
|
|
5879
|
-
reopened: true
|
|
5880
|
-
});
|
|
5881
|
-
return reopened;
|
|
5730
|
+
throw new BoardError("INVALID_STATE", task.status === "done" || task.status === "abandoned" ? `task ${task.id} is ${task.status} and its thread closed with it` : `task ${task.id} has no open thread`);
|
|
5882
5731
|
}
|
|
5883
5732
|
/** Posts what a task verb was told to say into the task's thread, as its author, with the step it recorded. */
|
|
5884
5733
|
async postTaskNote(author, task, body, step) {
|
|
5885
|
-
const thread = await this.taskThreadUnlocked(
|
|
5734
|
+
const thread = await this.taskThreadUnlocked(task);
|
|
5886
5735
|
return this.appendThreadMessage(author, thread.id, body, step === void 0 ? {} : { step });
|
|
5887
5736
|
}
|
|
5888
|
-
/** Where a new thread hangs, what it is called, and what it is about. */
|
|
5889
|
-
async threadToOpen(args) {
|
|
5890
|
-
if (args.task_id !== void 0 && args.proposal_id !== void 0) {
|
|
5891
|
-
throw new BoardError("VALIDATION", "a thread is about one task or one proposal, not both");
|
|
5892
|
-
}
|
|
5893
|
-
if (args.task_id !== void 0) {
|
|
5894
|
-
const { task, project } = await this.findTask(args.task_id);
|
|
5895
|
-
if (task.status === "done" || task.status === "abandoned") {
|
|
5896
|
-
throw new BoardError("INVALID_STATE", `task ${task.id} is ${task.status}`);
|
|
5897
|
-
}
|
|
5898
|
-
const channel = args.channel ?? channelRef(project, task.channel);
|
|
5899
|
-
if (parseChannelRef(channel).project !== project) {
|
|
5900
|
-
throw new BoardError("VALIDATION", `a task's thread hangs off a channel of its project, ${project}`);
|
|
5901
|
-
}
|
|
5902
|
-
await this.assertChannelOpen(channel);
|
|
5903
|
-
return {
|
|
5904
|
-
id: task.id,
|
|
5905
|
-
channel,
|
|
5906
|
-
title: args.title ?? task.title,
|
|
5907
|
-
subject: { kind: "task", id: task.id }
|
|
5908
|
-
};
|
|
5909
|
-
}
|
|
5910
|
-
if (args.proposal_id !== void 0) {
|
|
5911
|
-
const proposal = await this.readProposal(args.proposal_id);
|
|
5912
|
-
if (proposal.status !== "proposed") {
|
|
5913
|
-
throw new BoardError("INVALID_STATE", `proposal ${proposal.id} is already ${proposal.status}`);
|
|
5914
|
-
}
|
|
5915
|
-
const channel = args.channel ?? "governance";
|
|
5916
|
-
await this.assertChannelOpen(channel);
|
|
5917
|
-
const described = `${proposal.kind} proposal: ${describeCharter(proposal.kind, proposal.charter)}`;
|
|
5918
|
-
return {
|
|
5919
|
-
id: proposal.id,
|
|
5920
|
-
channel,
|
|
5921
|
-
title: args.title ?? described.slice(0, 200),
|
|
5922
|
-
subject: { kind: "proposal", id: proposal.id }
|
|
5923
|
-
};
|
|
5924
|
-
}
|
|
5925
|
-
if (args.channel === void 0 || args.title === void 0) {
|
|
5926
|
-
throw new BoardError("VALIDATION", "a thread needs a task_id, a proposal_id, or a channel and a title");
|
|
5927
|
-
}
|
|
5928
|
-
await this.assertChannelOpen(args.channel);
|
|
5929
|
-
return { id: this.newId(), channel: args.channel, title: args.title };
|
|
5930
|
-
}
|
|
5931
5737
|
/**
|
|
5932
5738
|
* Where the board learns which turns are in flight, from the server that hosts them, so it
|
|
5933
5739
|
* refuses to end a conversation under someone else's turn.
|
|
@@ -5961,17 +5767,20 @@ ${current.body}`);
|
|
|
5961
5767
|
async openThreadIds(dir, keep) {
|
|
5962
5768
|
const ids = /* @__PURE__ */ new Set();
|
|
5963
5769
|
for (const file of await listFiles(dir)) {
|
|
5964
|
-
const doc = await readMarkdown(
|
|
5770
|
+
const doc = await readMarkdown(path7.join(dir, file), ThreadFrontmatterSchema);
|
|
5965
5771
|
if (doc.data.state === "open" && keep({ ...doc.data, body: doc.body })) {
|
|
5966
5772
|
ids.add(doc.data.id);
|
|
5967
5773
|
}
|
|
5968
5774
|
}
|
|
5969
5775
|
return ids;
|
|
5970
5776
|
}
|
|
5971
|
-
/**
|
|
5777
|
+
/**
|
|
5778
|
+
* Refuses to archive a project while a turn runs in it, in a citizen's conversation of its
|
|
5779
|
+
* channels in the society scope, or in a thread its archive closes.
|
|
5780
|
+
*/
|
|
5972
5781
|
async assertProjectQuiet(actor, slug) {
|
|
5973
5782
|
const threads = await this.openThreadIds(this.paths.threads(slug), () => true);
|
|
5974
|
-
this.assertNoTurnCut(actor, `project ${slug} cannot be archived`, (turn) => turn.scope === slug || turn.thread !== void 0 && threads.has(turn.thread));
|
|
5783
|
+
this.assertNoTurnCut(actor, `project ${slug} cannot be archived`, (turn) => turn.scope === slug || turn.place === slug || turn.thread !== void 0 && threads.has(turn.thread));
|
|
5975
5784
|
}
|
|
5976
5785
|
async closeThread(actor, input) {
|
|
5977
5786
|
const args = VerbInputs.close_thread.parse(input);
|
|
@@ -6512,7 +6321,8 @@ ${args.summary}`);
|
|
|
6512
6321
|
filedFrom: {
|
|
6513
6322
|
scope: actor.scope,
|
|
6514
6323
|
...actor.thread === void 0 ? {} : { thread: actor.thread },
|
|
6515
|
-
...actor.channel === void 0 ? {} : { channel: actor.channel }
|
|
6324
|
+
...actor.channel === void 0 ? {} : { channel: actor.channel },
|
|
6325
|
+
...actor.place === void 0 ? {} : { place: actor.place }
|
|
6516
6326
|
}
|
|
6517
6327
|
},
|
|
6518
6328
|
charter: parsed.data
|
|
@@ -6924,19 +6734,20 @@ ${rationale}`}`);
|
|
|
6924
6734
|
project: input.project,
|
|
6925
6735
|
...input.thread === void 0 ? {} : { thread: input.thread },
|
|
6926
6736
|
...input.channel === void 0 ? {} : { channel: input.channel },
|
|
6737
|
+
...input.place === void 0 ? {} : { place: input.place },
|
|
6927
6738
|
messages: [...input.messages]
|
|
6928
6739
|
});
|
|
6929
6740
|
});
|
|
6930
6741
|
}
|
|
6931
6742
|
/**
|
|
6932
6743
|
* Where a conversation's session ids and last turn are kept: the scope's home, or beside it a
|
|
6933
|
-
* thread's or a
|
|
6744
|
+
* thread's, a channel's, or a project's, named by `part` as `conversationPart` gives it.
|
|
6934
6745
|
*/
|
|
6935
6746
|
sessionDir(agent, scope, part) {
|
|
6936
6747
|
return part === void 0 ? this.paths.agentProject(agent, scope) : this.paths.agentConversation(agent, scope, part);
|
|
6937
6748
|
}
|
|
6938
6749
|
async readSessions(agent, project, part) {
|
|
6939
|
-
const file =
|
|
6750
|
+
const file = path7.join(this.sessionDir(agent, project, part), "sessions.json");
|
|
6940
6751
|
return await exists(file) ? readJson(file, SessionsFileSchema) : {};
|
|
6941
6752
|
}
|
|
6942
6753
|
/**
|
|
@@ -6947,15 +6758,34 @@ ${rationale}`}`);
|
|
|
6947
6758
|
await this.mutex.run(async () => {
|
|
6948
6759
|
const dir = this.sessionDir(agent, project, part);
|
|
6949
6760
|
await ensureDir(dir);
|
|
6950
|
-
const file =
|
|
6761
|
+
const file = path7.join(dir, "sessions.json");
|
|
6951
6762
|
const current = await exists(file) ? await readJson(file, SessionsFileSchema) : {};
|
|
6952
6763
|
const kept = current.runner === void 0 || current.runner === runner ? current : {};
|
|
6953
6764
|
await writeJson(file, { ...kept, [cli]: sessionId, runner });
|
|
6954
6765
|
});
|
|
6955
6766
|
}
|
|
6956
|
-
/**
|
|
6767
|
+
/**
|
|
6768
|
+
* Drops a conversation's session for a CLI, so its next turn starts a new one, when the record
|
|
6769
|
+
* still names `sessionId`; returns whether it did.
|
|
6770
|
+
*/
|
|
6771
|
+
async dropSession(agent, project, cli, sessionId, part) {
|
|
6772
|
+
return this.mutex.run(async () => {
|
|
6773
|
+
const file = path7.join(this.sessionDir(agent, project, part), "sessions.json");
|
|
6774
|
+
if (!await exists(file)) {
|
|
6775
|
+
return false;
|
|
6776
|
+
}
|
|
6777
|
+
const current = await readJson(file, SessionsFileSchema);
|
|
6778
|
+
if (current[cli] !== sessionId) {
|
|
6779
|
+
return false;
|
|
6780
|
+
}
|
|
6781
|
+
const { [cli]: _dropped, ...kept } = current;
|
|
6782
|
+
await writeJson(file, kept);
|
|
6783
|
+
return true;
|
|
6784
|
+
});
|
|
6785
|
+
}
|
|
6786
|
+
/** The last turn of one conversation: the scope's home, a channel's, a project's, or a thread's. */
|
|
6957
6787
|
async readLastTurn(agent, project, part) {
|
|
6958
|
-
const file =
|
|
6788
|
+
const file = path7.join(this.sessionDir(agent, project, part), "last-turn.json");
|
|
6959
6789
|
return await exists(file) ? readJson(file, TurnRecordSchema) : null;
|
|
6960
6790
|
}
|
|
6961
6791
|
/** The latest turn a citizen took in a scope, in any of its conversations there. */
|
|
@@ -6963,8 +6793,9 @@ ${rationale}`}`);
|
|
|
6963
6793
|
let latest = await this.readLastTurn(agent, project);
|
|
6964
6794
|
const place = this.paths.agentProject(agent, project);
|
|
6965
6795
|
const parts = [
|
|
6966
|
-
...await listDirs(
|
|
6967
|
-
...(await listDirs(
|
|
6796
|
+
...await listDirs(path7.join(place, "threads")),
|
|
6797
|
+
...(await listDirs(path7.join(place, "channels"))).map((name) => `#${name}`),
|
|
6798
|
+
...(await listDirs(path7.join(place, "places"))).map((name) => `~${name}`)
|
|
6968
6799
|
];
|
|
6969
6800
|
for (const part of parts) {
|
|
6970
6801
|
const turn = await this.readLastTurn(agent, project, part);
|
|
@@ -6985,6 +6816,7 @@ ${rationale}`}`);
|
|
|
6985
6816
|
project: parsed.project,
|
|
6986
6817
|
...parsed.thread === void 0 ? {} : { thread: parsed.thread },
|
|
6987
6818
|
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
6819
|
+
...parsed.place === void 0 ? {} : { place: parsed.place },
|
|
6988
6820
|
trigger: parsed.trigger.kind,
|
|
6989
6821
|
session: parsed.session,
|
|
6990
6822
|
runner: parsed.runner
|
|
@@ -7014,7 +6846,8 @@ ${rationale}`}`);
|
|
|
7014
6846
|
agent: parsed.agent,
|
|
7015
6847
|
project: parsed.project,
|
|
7016
6848
|
...parsed.thread === void 0 ? {} : { thread: parsed.thread },
|
|
7017
|
-
...parsed.channel === void 0 ? {} : { channel: parsed.channel }
|
|
6849
|
+
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
6850
|
+
...parsed.place === void 0 ? {} : { place: parsed.place }
|
|
7018
6851
|
});
|
|
7019
6852
|
}
|
|
7020
6853
|
await this.events.append(failed ? "turn.failed" : "turn.completed", parsed.agent, {
|
|
@@ -7022,6 +6855,7 @@ ${rationale}`}`);
|
|
|
7022
6855
|
project: parsed.project,
|
|
7023
6856
|
...parsed.thread === void 0 ? {} : { thread: parsed.thread },
|
|
7024
6857
|
...parsed.channel === void 0 ? {} : { channel: parsed.channel },
|
|
6858
|
+
...parsed.place === void 0 ? {} : { place: parsed.place },
|
|
7025
6859
|
trigger: parsed.trigger.kind,
|
|
7026
6860
|
session: parsed.session,
|
|
7027
6861
|
exitReason: parsed.exitReason,
|
|
@@ -7031,7 +6865,10 @@ ${rationale}`}`);
|
|
|
7031
6865
|
model: parsed.model,
|
|
7032
6866
|
summary: parsed.status?.summary ?? null,
|
|
7033
6867
|
needsUserDecision: parsed.status?.needsUserDecision ?? false,
|
|
7034
|
-
error: parsed.error
|
|
6868
|
+
error: parsed.error,
|
|
6869
|
+
...parsed.resumeFailed === void 0 ? {} : { resumeFailed: parsed.resumeFailed },
|
|
6870
|
+
...parsed.transient === true ? { transient: true } : {},
|
|
6871
|
+
...parsed.sessionDropped === true ? { sessionDropped: true } : {}
|
|
7035
6872
|
});
|
|
7036
6873
|
});
|
|
7037
6874
|
}
|
|
@@ -7068,6 +6905,20 @@ ${rationale}`}`);
|
|
|
7068
6905
|
return task;
|
|
7069
6906
|
});
|
|
7070
6907
|
}
|
|
6908
|
+
/**
|
|
6909
|
+
* Posts the board's word on a task's work into the task's thread, such as commits a runner
|
|
6910
|
+
* could not put on its branch: as the board, with every `@name` written as a plain name, so it
|
|
6911
|
+
* wakes nobody and counts toward no heartbeat. Null, posting nothing, once the thread is closed.
|
|
6912
|
+
*/
|
|
6913
|
+
async postTaskNotice(taskId, body) {
|
|
6914
|
+
return this.mutex.run(async () => {
|
|
6915
|
+
const found = await this.tryFindThread(taskId);
|
|
6916
|
+
if (found === null || found.thread.state !== "open") {
|
|
6917
|
+
return null;
|
|
6918
|
+
}
|
|
6919
|
+
return this.appendThreadMessage(SYSTEM_ACTOR.name, taskId, withoutMentions(body));
|
|
6920
|
+
});
|
|
6921
|
+
}
|
|
7071
6922
|
/**
|
|
7072
6923
|
* Records that a citizen's workspace in a conversation that has ended holds work on no branch, so
|
|
7073
6924
|
* the scheduler gives the citizen a closing turn there to decide about it. False, recording
|
|
@@ -7078,7 +6929,7 @@ ${rationale}`}`);
|
|
|
7078
6929
|
const payload = {
|
|
7079
6930
|
agent: input.agent,
|
|
7080
6931
|
project: input.scope,
|
|
7081
|
-
...conversation.kind === "thread" ? { thread: conversation.id } : { channel: conversation.name }
|
|
6932
|
+
...conversation.kind === "thread" ? { thread: conversation.id } : conversation.kind === "channel" ? { channel: conversation.name } : { place: conversation.project }
|
|
7082
6933
|
};
|
|
7083
6934
|
return this.mutex.run(async () => {
|
|
7084
6935
|
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;
|
|
@@ -7130,9 +6981,13 @@ ${rationale}`}`);
|
|
|
7130
6981
|
}
|
|
7131
6982
|
/**
|
|
7132
6983
|
* Whether a conversation with a workspace of its own has ended: a thread closed, a task ended
|
|
7133
|
-
* (for one from before tasks had threads), or a
|
|
6984
|
+
* (for one from before tasks had threads), a channel archived, or a project archived, which ends
|
|
6985
|
+
* every citizen's conversation of its channels in the society scope.
|
|
7134
6986
|
*/
|
|
7135
6987
|
async conversationEnded(conversation) {
|
|
6988
|
+
if (conversation.kind === "place") {
|
|
6989
|
+
return !await this.projectLive(conversation.project);
|
|
6990
|
+
}
|
|
7136
6991
|
if (conversation.kind === "channel") {
|
|
7137
6992
|
const place = conversation.scope === SOCIETY_SCOPE ? null : conversation.scope;
|
|
7138
6993
|
return !await this.channelOpen(channelRef(place, conversation.name));
|
|
@@ -7206,11 +7061,11 @@ ${rationale}`}`);
|
|
|
7206
7061
|
}
|
|
7207
7062
|
/** Small named state files under data/state, for cursors the scheduler must keep across restarts. */
|
|
7208
7063
|
async readState(name, schema, fallback) {
|
|
7209
|
-
const file =
|
|
7064
|
+
const file = path7.join(this.paths.state(), `${name}.json`);
|
|
7210
7065
|
return await exists(file) ? readJson(file, schema) : fallback;
|
|
7211
7066
|
}
|
|
7212
7067
|
async writeState(name, value) {
|
|
7213
|
-
await this.mutex.run(() => writeJson(
|
|
7068
|
+
await this.mutex.run(() => writeJson(path7.join(this.paths.state(), `${name}.json`), value));
|
|
7214
7069
|
}
|
|
7215
7070
|
/** The project's dashboard, empty at revision 0 when it has none. */
|
|
7216
7071
|
async readDashboard(slug) {
|
|
@@ -7241,7 +7096,7 @@ ${rationale}`}`);
|
|
|
7241
7096
|
async listRunners() {
|
|
7242
7097
|
const runners2 = [];
|
|
7243
7098
|
for (const file of await listFiles(this.paths.runners())) {
|
|
7244
|
-
runners2.push((await readMarkdown(
|
|
7099
|
+
runners2.push((await readMarkdown(path7.join(this.paths.runners(), file), RunnerSchema)).data);
|
|
7245
7100
|
}
|
|
7246
7101
|
return runners2;
|
|
7247
7102
|
}
|
|
@@ -7293,7 +7148,7 @@ ${rationale}`}`);
|
|
|
7293
7148
|
return this.runnerTokens.get(hashToken(token)) ?? null;
|
|
7294
7149
|
}
|
|
7295
7150
|
runnerTokensFile() {
|
|
7296
|
-
return
|
|
7151
|
+
return path7.join(this.paths.state(), "runners.json");
|
|
7297
7152
|
}
|
|
7298
7153
|
async readRunnerTokens() {
|
|
7299
7154
|
const file = this.runnerTokensFile();
|
|
@@ -7342,26 +7197,26 @@ ${rationale}`}`);
|
|
|
7342
7197
|
async listHomeConflicts(agent) {
|
|
7343
7198
|
const home = this.paths.agent(agent);
|
|
7344
7199
|
const found = [];
|
|
7345
|
-
const
|
|
7346
|
-
for (const entry of await
|
|
7200
|
+
const walk2 = async (dir, prefix) => {
|
|
7201
|
+
for (const entry of await readdir4(dir, { withFileTypes: true }).catch(() => [])) {
|
|
7347
7202
|
if (prefix === "" && SKIPPED_IN_HOME.has(entry.name)) {
|
|
7348
7203
|
continue;
|
|
7349
7204
|
}
|
|
7350
7205
|
const relative = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
7351
7206
|
if (entry.isDirectory()) {
|
|
7352
|
-
await
|
|
7207
|
+
await walk2(path7.join(dir, entry.name), relative);
|
|
7353
7208
|
continue;
|
|
7354
7209
|
}
|
|
7355
7210
|
const file = entry.isFile() ? conflictCopyOf(relative) : null;
|
|
7356
7211
|
if (file !== null) {
|
|
7357
|
-
const since = await this.homes.changedAt(home, relative) ?? (await stat3(
|
|
7212
|
+
const since = await this.homes.changedAt(home, relative) ?? (await stat3(path7.join(home, relative)).catch(() => null))?.mtime.toISOString();
|
|
7358
7213
|
if (since !== void 0) {
|
|
7359
7214
|
found.push({ path: relative, file, since });
|
|
7360
7215
|
}
|
|
7361
7216
|
}
|
|
7362
7217
|
}
|
|
7363
7218
|
};
|
|
7364
|
-
await
|
|
7219
|
+
await walk2(home, "");
|
|
7365
7220
|
return found.toSorted((a, b) => a.path.localeCompare(b.path));
|
|
7366
7221
|
}
|
|
7367
7222
|
/** The newest `limit` changes to an agent's home, newest first; empty while it is no repository. */
|
|
@@ -7438,7 +7293,7 @@ ${rationale}`}`);
|
|
|
7438
7293
|
async writeTurnRecord(record) {
|
|
7439
7294
|
const dir = this.sessionDir(record.agent, record.project, conversationPart(record));
|
|
7440
7295
|
await ensureDir(dir);
|
|
7441
|
-
await writeJson(
|
|
7296
|
+
await writeJson(path7.join(dir, "last-turn.json"), record);
|
|
7442
7297
|
}
|
|
7443
7298
|
// ---------------------------------------------------------------------------------------------
|
|
7444
7299
|
// Internals
|
|
@@ -7557,8 +7412,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7557
7412
|
async ensureAgentProject(name, slug) {
|
|
7558
7413
|
const dir = this.paths.agentProject(name, slug);
|
|
7559
7414
|
await ensureDir(dir);
|
|
7560
|
-
if (!await exists(
|
|
7561
|
-
await writeJson(
|
|
7415
|
+
if (!await exists(path7.join(dir, "sessions.json"))) {
|
|
7416
|
+
await writeJson(path7.join(dir, "sessions.json"), {});
|
|
7562
7417
|
}
|
|
7563
7418
|
}
|
|
7564
7419
|
/**
|
|
@@ -7590,7 +7445,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7590
7445
|
const at = turn?.endedAt ?? turn?.startedAt;
|
|
7591
7446
|
if (turn !== null && at !== void 0 && (lastTurnAt === void 0 || at > lastTurnAt)) {
|
|
7592
7447
|
lastTurnAt = at;
|
|
7593
|
-
lastTurnOutcome = `${turn.trigger.kind} on ${scope}: ${turn.exitReason ?? "running"}${turn.status === null ? "" : `, ${turn.status.summary.slice(0, 160)}`}`;
|
|
7448
|
+
lastTurnOutcome = `${turn.trigger.kind} on ${scope}${turnConversation(turn)}: ${turn.exitReason ?? "running"}${turn.status === null ? "" : `, ${turn.status.summary.slice(0, 160)}`}`;
|
|
7594
7449
|
lastModel = turn.model ?? void 0;
|
|
7595
7450
|
}
|
|
7596
7451
|
}
|
|
@@ -7720,15 +7575,9 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7720
7575
|
case "archive":
|
|
7721
7576
|
await this.validateArchive(ArchiveProposalSchema.parse(charter).project);
|
|
7722
7577
|
return;
|
|
7723
|
-
case "reallocation":
|
|
7724
|
-
|
|
7725
|
-
if (parsed.success) {
|
|
7726
|
-
await this.reallocatedRole(parsed.data);
|
|
7727
|
-
} else {
|
|
7728
|
-
LegacyReallocationSchema.parse(charter);
|
|
7729
|
-
}
|
|
7578
|
+
case "reallocation":
|
|
7579
|
+
await this.reallocatedRole(ReallocationProposalSchema.parse(charter));
|
|
7730
7580
|
return;
|
|
7731
|
-
}
|
|
7732
7581
|
default:
|
|
7733
7582
|
return;
|
|
7734
7583
|
}
|
|
@@ -7788,11 +7637,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7788
7637
|
return { project: input.project, members };
|
|
7789
7638
|
}
|
|
7790
7639
|
case "reallocation": {
|
|
7791
|
-
const
|
|
7792
|
-
if (!parsed.success) {
|
|
7793
|
-
return void 0;
|
|
7794
|
-
}
|
|
7795
|
-
const { next, changed } = await this.reallocatedRole(parsed.data);
|
|
7640
|
+
const { next, changed } = await this.reallocatedRole(ReallocationProposalSchema.parse(proposal.charter));
|
|
7796
7641
|
await this.writeRoleUnlocked(by, next, meta);
|
|
7797
7642
|
return { role: next.name, changed };
|
|
7798
7643
|
}
|
|
@@ -7802,25 +7647,21 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7802
7647
|
}
|
|
7803
7648
|
/**
|
|
7804
7649
|
* Publishes a promoted skill under the society's skills, where every citizen's skills index lists
|
|
7805
|
-
* it: the folder the board took from its proposer's home
|
|
7806
|
-
*
|
|
7650
|
+
* it: the folder the board took from its proposer's home. A replacement is the whole skill, so
|
|
7651
|
+
* files the new version dropped go with the old one.
|
|
7807
7652
|
*/
|
|
7808
7653
|
async promoteSkillUnlocked(by, proposal, skill, meta) {
|
|
7809
7654
|
const dir = this.paths.societySkill(skill.name);
|
|
7810
7655
|
const file = this.paths.societySkillFile(skill.name);
|
|
7811
7656
|
const replaced = await exists(file);
|
|
7812
7657
|
const taken = proposal.skillFiles;
|
|
7813
|
-
const staged =
|
|
7814
|
-
if (
|
|
7658
|
+
const staged = path7.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
|
|
7659
|
+
if (taken === void 0 || !await exists(staged)) {
|
|
7815
7660
|
throw new BoardError("INVALID_STATE", `proposal ${proposal.id} holds no skill files`);
|
|
7816
7661
|
}
|
|
7817
7662
|
await rm2(dir, { recursive: true, force: true });
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
} else {
|
|
7821
|
-
await ensureDir(dir);
|
|
7822
|
-
}
|
|
7823
|
-
const body = skill.body ?? (await readLooseMarkdown(file)).body;
|
|
7663
|
+
await cp(this.paths.proposalFiles(proposal.id), dir, { recursive: true });
|
|
7664
|
+
const { body } = await readLooseMarkdown(file);
|
|
7824
7665
|
await writeMarkdown(file, {
|
|
7825
7666
|
name: skill.name,
|
|
7826
7667
|
description: skill.summary,
|
|
@@ -7828,7 +7669,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7828
7669
|
proposedBy: proposal.proposedBy,
|
|
7829
7670
|
promotedBy: by,
|
|
7830
7671
|
promotedAt: this.now().toISOString(),
|
|
7831
|
-
|
|
7672
|
+
commit: taken.commit
|
|
7832
7673
|
}
|
|
7833
7674
|
}, body);
|
|
7834
7675
|
await this.events.append("skill.promoted", by, {
|
|
@@ -7836,7 +7677,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7836
7677
|
summary: skill.summary,
|
|
7837
7678
|
proposedBy: proposal.proposedBy,
|
|
7838
7679
|
replaced,
|
|
7839
|
-
|
|
7680
|
+
files: taken.files.length,
|
|
7840
7681
|
...meta
|
|
7841
7682
|
});
|
|
7842
7683
|
await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${skill.name} ${replaced ? "updated in" : "promoted to"} the society: ${sentence(skill.summary)}. Every citizen's skills index now lists it.`);
|
|
@@ -7873,8 +7714,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7873
7714
|
const dir = this.paths.proposalFiles(proposal.id);
|
|
7874
7715
|
await rm2(dir, { recursive: true, force: true });
|
|
7875
7716
|
for (const entry of taken.files) {
|
|
7876
|
-
const target =
|
|
7877
|
-
await ensureDir(
|
|
7717
|
+
const target = path7.join(dir, ...entry.path.split("/"));
|
|
7718
|
+
await ensureDir(path7.dirname(target));
|
|
7878
7719
|
await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
|
|
7879
7720
|
await chmod2(target, entry.executable ? 493 : 420);
|
|
7880
7721
|
}
|
|
@@ -7932,7 +7773,8 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7932
7773
|
const where = {
|
|
7933
7774
|
scope: turn.project,
|
|
7934
7775
|
...turn.thread === void 0 ? {} : { thread: turn.thread },
|
|
7935
|
-
...turn.channel === void 0 ? {} : { channel: turn.channel }
|
|
7776
|
+
...turn.channel === void 0 ? {} : { channel: turn.channel },
|
|
7777
|
+
...turn.place === void 0 ? {} : { place: turn.place }
|
|
7936
7778
|
};
|
|
7937
7779
|
await this.mutex.run(async () => {
|
|
7938
7780
|
for (const proposal of await this.listProposals()) {
|
|
@@ -7964,16 +7806,25 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
7964
7806
|
if (!isSafeRelativePath(entry.path)) {
|
|
7965
7807
|
continue;
|
|
7966
7808
|
}
|
|
7967
|
-
const bytes = await readFile4(
|
|
7968
|
-
const text = bytes === null || bytes.includes(0) ? null : bytes.toString("utf8");
|
|
7809
|
+
const bytes = await readFile4(path7.join(dir, ...entry.path.split("/"))).catch(() => null);
|
|
7969
7810
|
files.push({
|
|
7970
7811
|
...entry,
|
|
7971
|
-
|
|
7972
|
-
truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
|
|
7812
|
+
...bytes === null ? { text: null, truncated: false } : skillFileText(bytes)
|
|
7973
7813
|
});
|
|
7974
7814
|
}
|
|
7975
7815
|
return files;
|
|
7976
7816
|
}
|
|
7817
|
+
/**
|
|
7818
|
+
* A society skill's folder as it is published, with each file's text, for the skill's view; an
|
|
7819
|
+
* unknown skill, or a folder without a SKILL.md, is not found.
|
|
7820
|
+
*/
|
|
7821
|
+
async readSocietySkillFiles(name) {
|
|
7822
|
+
const files = await readSkillFolder(this.paths.societySkill(NameSchema.parse(name)));
|
|
7823
|
+
if (files === null) {
|
|
7824
|
+
throw new BoardError("NOT_FOUND", `the society has no skill ${name}`);
|
|
7825
|
+
}
|
|
7826
|
+
return files;
|
|
7827
|
+
}
|
|
7977
7828
|
async validateAddAgent(input) {
|
|
7978
7829
|
if (await exists(this.paths.agentFile(input.name))) {
|
|
7979
7830
|
throw new BoardError("ALREADY_EXISTS", `agent ${input.name} already exists`);
|
|
@@ -8104,7 +7955,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
8104
7955
|
this.assertNoTurnCut(actor, `${ref} cannot be archived`, (turn) => turn.channel === channel && turn.scope === scope || turn.thread !== void 0 && closing.has(turn.thread));
|
|
8105
7956
|
const threadsClosed = [];
|
|
8106
7957
|
for (const file of await listFiles(threads)) {
|
|
8107
|
-
const doc = await readMarkdown(
|
|
7958
|
+
const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
|
|
8108
7959
|
if (doc.data.channel !== ref || doc.data.state !== "open") {
|
|
8109
7960
|
continue;
|
|
8110
7961
|
}
|
|
@@ -8210,7 +8061,8 @@ ${charter.purpose}
|
|
|
8210
8061
|
}
|
|
8211
8062
|
}
|
|
8212
8063
|
/**
|
|
8213
|
-
* Every member leaves, every open thread on its channels closes,
|
|
8064
|
+
* Every member leaves, every open thread on its channels closes, every citizen's conversation of
|
|
8065
|
+
* its channels in the society scope ends with the crons there, and the project is marked; its
|
|
8214
8066
|
* files, repository, and history stay. The notice is the board's, since the reason is free text
|
|
8215
8067
|
* a proposer wrote and a board-authored mention would wake whoever it names.
|
|
8216
8068
|
*/
|
|
@@ -8235,7 +8087,7 @@ ${charter.purpose}
|
|
|
8235
8087
|
const threads = this.paths.threads(slug);
|
|
8236
8088
|
const threadsClosed = [];
|
|
8237
8089
|
for (const file of await listFiles(threads)) {
|
|
8238
|
-
const doc = await readMarkdown(
|
|
8090
|
+
const doc = await readMarkdown(path7.join(threads, file), ThreadFrontmatterSchema);
|
|
8239
8091
|
if (doc.data.state !== "open") {
|
|
8240
8092
|
continue;
|
|
8241
8093
|
}
|
|
@@ -8253,7 +8105,7 @@ ${charter.purpose}
|
|
|
8253
8105
|
});
|
|
8254
8106
|
threadsClosed.push(doc.data.id);
|
|
8255
8107
|
}
|
|
8256
|
-
await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron) => cron.scope === slug || cron.thread !== void 0 && threadsClosed.includes(cron.thread));
|
|
8108
|
+
await this.endCronsUnlocked(by, `project ${slug} was archived`, (cron) => cron.scope === slug || cron.place === slug || cron.thread !== void 0 && threadsClosed.includes(cron.thread));
|
|
8257
8109
|
const project = await this.updateProject(slug, (p) => ({
|
|
8258
8110
|
...p,
|
|
8259
8111
|
members: [],
|
|
@@ -8310,10 +8162,10 @@ ${charter.purpose}
|
|
|
8310
8162
|
...project,
|
|
8311
8163
|
members: project.members.filter((member) => member !== name)
|
|
8312
8164
|
}));
|
|
8313
|
-
const sessions =
|
|
8165
|
+
const sessions = path7.join(this.paths.agentProject(name, slug), "sessions.json");
|
|
8314
8166
|
if (await exists(sessions)) {
|
|
8315
8167
|
const archived = `sessions.archived.${ts.replace(/[:.]/g, "-")}.json`;
|
|
8316
|
-
await rename3(sessions,
|
|
8168
|
+
await rename3(sessions, path7.join(this.paths.agentProject(name, slug), archived));
|
|
8317
8169
|
}
|
|
8318
8170
|
}
|
|
8319
8171
|
this.tokenIndex.delete(current.tokenHash);
|
|
@@ -8393,6 +8245,17 @@ ${charter.purpose}
|
|
|
8393
8245
|
return false;
|
|
8394
8246
|
}
|
|
8395
8247
|
}
|
|
8248
|
+
/**
|
|
8249
|
+
* Whether a project exists and is not archived, so citizens' conversations of its channels in
|
|
8250
|
+
* the society scope are still going.
|
|
8251
|
+
*/
|
|
8252
|
+
async projectLive(slug) {
|
|
8253
|
+
try {
|
|
8254
|
+
return (await this.readProject(slug)).archived === void 0;
|
|
8255
|
+
} catch {
|
|
8256
|
+
return false;
|
|
8257
|
+
}
|
|
8258
|
+
}
|
|
8396
8259
|
/**
|
|
8397
8260
|
* A channel that takes new posts, threads, tasks, and followers: it exists, it is not archived,
|
|
8398
8261
|
* and its project is not archived.
|
|
@@ -8503,6 +8366,7 @@ ${charter.purpose}
|
|
|
8503
8366
|
await this.validateProvision(filed.kind, filed.charter);
|
|
8504
8367
|
}
|
|
8505
8368
|
this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn) => turn.thread === proposalId);
|
|
8369
|
+
const thread = await this.proposalThreadUnlocked(filed);
|
|
8506
8370
|
if (outcome === "approved" && filed.kind === "archive") {
|
|
8507
8371
|
await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(filed.charter).project);
|
|
8508
8372
|
}
|
|
@@ -8544,7 +8408,6 @@ ${charter.purpose}
|
|
|
8544
8408
|
const verdict = outcome === "approved" ? "Approved" : "Rejected";
|
|
8545
8409
|
const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
|
|
8546
8410
|
const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
|
|
8547
|
-
const thread = await this.proposalThreadUnlocked(actor.name, proposal);
|
|
8548
8411
|
const post = `${verdict}: ${describeCharter(proposal.kind, proposal.charter)}.${why}${result}`;
|
|
8549
8412
|
await this.appendThreadMessage(actor.name, thread.id, actor.name === SYSTEM_ACTOR.name ? withoutMentions(post) : post, { step: { action: outcome } });
|
|
8550
8413
|
await this.endThreadOf(proposalId, actor.name, outcome);
|
|
@@ -8643,7 +8506,7 @@ ${charter.purpose}
|
|
|
8643
8506
|
if (since !== null && id <= since || upto !== void 0 && id > upto) {
|
|
8644
8507
|
continue;
|
|
8645
8508
|
}
|
|
8646
|
-
const doc = await readMarkdown(
|
|
8509
|
+
const doc = await readMarkdown(path7.join(dir, file), MessageFrontmatterSchema);
|
|
8647
8510
|
messages.push({ ...doc.data, body: doc.body });
|
|
8648
8511
|
}
|
|
8649
8512
|
return messages;
|
|
@@ -8739,11 +8602,21 @@ function besideFields(beside) {
|
|
|
8739
8602
|
if (beside === void 0) {
|
|
8740
8603
|
return {};
|
|
8741
8604
|
}
|
|
8742
|
-
|
|
8605
|
+
if ("thread" in beside) {
|
|
8606
|
+
return { thread: beside.thread };
|
|
8607
|
+
}
|
|
8608
|
+
return "channel" in beside ? { channel: beside.channel } : { place: beside.place };
|
|
8609
|
+
}
|
|
8610
|
+
function namedIn(dispatch) {
|
|
8611
|
+
return {
|
|
8612
|
+
...dispatch.thread === void 0 ? {} : { thread: dispatch.thread.id },
|
|
8613
|
+
...dispatch.channel === void 0 ? {} : { channel: dispatch.channel },
|
|
8614
|
+
...dispatch.place === void 0 ? {} : { place: dispatch.place }
|
|
8615
|
+
};
|
|
8743
8616
|
}
|
|
8744
8617
|
function conversationKey2(agent, scope, beside) {
|
|
8745
8618
|
const fields = besideFields(beside);
|
|
8746
|
-
return sessionKey(agent, scope, conversationPart({ thread: fields.thread?.id, channel: fields.channel }));
|
|
8619
|
+
return sessionKey(agent, scope, conversationPart({ thread: fields.thread?.id, channel: fields.channel, place: fields.place }));
|
|
8747
8620
|
}
|
|
8748
8621
|
var DEFAULT_TIMINGS = Object.freeze({
|
|
8749
8622
|
pollMs: 1e3,
|
|
@@ -8754,12 +8627,14 @@ var DEFAULT_TIMINGS = Object.freeze({
|
|
|
8754
8627
|
leaseSweepMs: 6e4,
|
|
8755
8628
|
opsIntervalMs: 5 * 6e4,
|
|
8756
8629
|
signalRepeatMs: 6 * 36e5,
|
|
8630
|
+
signalWakeMs: 24 * 36e5,
|
|
8757
8631
|
costReportMs: 36e5,
|
|
8758
8632
|
staleThreadMs: 24 * 36e5,
|
|
8759
8633
|
idleMemberMs: 3 * 24 * 36e5,
|
|
8760
8634
|
homeConflictMs: 24 * 36e5,
|
|
8761
8635
|
scaleCooldownMs: 36e5,
|
|
8762
|
-
reflectionMs: 24 * 36e5
|
|
8636
|
+
reflectionMs: 24 * 36e5,
|
|
8637
|
+
transientRetryMs: [6e4, 3 * 6e4, 10 * 6e4]
|
|
8763
8638
|
});
|
|
8764
8639
|
var SchedulerTimingsSchema = z17.object({
|
|
8765
8640
|
pollMs: z17.number().positive(),
|
|
@@ -8770,18 +8645,20 @@ var SchedulerTimingsSchema = z17.object({
|
|
|
8770
8645
|
leaseSweepMs: z17.number().positive(),
|
|
8771
8646
|
opsIntervalMs: z17.number().positive(),
|
|
8772
8647
|
signalRepeatMs: z17.number().positive(),
|
|
8648
|
+
signalWakeMs: z17.number().positive(),
|
|
8773
8649
|
costReportMs: z17.number().positive(),
|
|
8774
8650
|
staleThreadMs: z17.number().positive(),
|
|
8775
8651
|
idleMemberMs: z17.number().positive(),
|
|
8776
8652
|
homeConflictMs: z17.number().positive(),
|
|
8777
8653
|
scaleCooldownMs: z17.number().positive(),
|
|
8778
|
-
reflectionMs: z17.number().positive()
|
|
8654
|
+
reflectionMs: z17.number().positive(),
|
|
8655
|
+
transientRetryMs: z17.array(z17.number().positive())
|
|
8779
8656
|
}).partial();
|
|
8780
8657
|
function parseTimings(input) {
|
|
8781
8658
|
const parsed = SchedulerTimingsSchema.parse(input);
|
|
8782
8659
|
const overrides = {};
|
|
8783
8660
|
for (const [key, value] of Object.entries(parsed)) {
|
|
8784
|
-
if (
|
|
8661
|
+
if (value !== void 0) {
|
|
8785
8662
|
Object.assign(overrides, { [key]: value });
|
|
8786
8663
|
}
|
|
8787
8664
|
}
|
|
@@ -8805,6 +8682,11 @@ var StateSchema = z17.object({
|
|
|
8805
8682
|
/** Queued dispatches survive a restart; the cursor has already moved past the events that made them. */
|
|
8806
8683
|
pending: z17.record(z17.string(), PendingSchema).default({}),
|
|
8807
8684
|
opsReported: z17.record(z17.string(), z17.string()).default({}),
|
|
8685
|
+
/**
|
|
8686
|
+
* When each condition of a waking kind that still holds last woke the readers of signals, and
|
|
8687
|
+
* the level it woke at for the kinds that have one (`wakeLevel`); forgotten with the condition.
|
|
8688
|
+
*/
|
|
8689
|
+
opsWoken: z17.record(z17.string(), z17.object({ at: z17.string(), level: z17.number().optional() })).default({}),
|
|
8808
8690
|
releaseCounts: z17.record(z17.string(), z17.number().int().nonnegative()).default({}),
|
|
8809
8691
|
lastOps: z17.string().nullable().default(null),
|
|
8810
8692
|
lastScaled: z17.record(z17.string(), z17.string()).default({}),
|
|
@@ -8829,6 +8711,22 @@ var SILENT_LOG = { info() {
|
|
|
8829
8711
|
}, error() {
|
|
8830
8712
|
} };
|
|
8831
8713
|
var WAKING_SIGNALS = new Set(WAKING_SIGNAL_KINDS);
|
|
8714
|
+
var DAY_MS = 24 * 36e5;
|
|
8715
|
+
var IDLE_WAKE_STEPS_MS = [7 * DAY_MS, 14 * DAY_MS, 30 * DAY_MS];
|
|
8716
|
+
function wakeLevel(signal) {
|
|
8717
|
+
switch (signal.kind) {
|
|
8718
|
+
case "idle_member":
|
|
8719
|
+
return IDLE_WAKE_STEPS_MS.filter((step) => signal.value >= step).length;
|
|
8720
|
+
case "churn":
|
|
8721
|
+
return signal.value;
|
|
8722
|
+
default:
|
|
8723
|
+
return void 0;
|
|
8724
|
+
}
|
|
8725
|
+
}
|
|
8726
|
+
function wokenAt(signal, now) {
|
|
8727
|
+
const level = wakeLevel(signal);
|
|
8728
|
+
return { at: iso(now), ...level === void 0 ? {} : { level } };
|
|
8729
|
+
}
|
|
8832
8730
|
var HEARTBEAT_TRIGGER = "heartbeat";
|
|
8833
8731
|
var OPS_TRIGGER = "ops_event";
|
|
8834
8732
|
var USER_POST_TRIGGER = "user_post";
|
|
@@ -8885,6 +8783,8 @@ var Scheduler = class {
|
|
|
8885
8783
|
sweeping = /* @__PURE__ */ new Set();
|
|
8886
8784
|
/** Crons whose fire's turn is running; a cron's next time passes while one is. */
|
|
8887
8785
|
cronsInFlight = /* @__PURE__ */ new Set();
|
|
8786
|
+
/** Turns in a row the model service turned away, by session key, which `retryRefused` counts. */
|
|
8787
|
+
refused = /* @__PURE__ */ new Map();
|
|
8888
8788
|
state = StateSchema.parse({
|
|
8889
8789
|
cursor: null,
|
|
8890
8790
|
lastHeartbeat: {}
|
|
@@ -9097,10 +8997,6 @@ var Scheduler = class {
|
|
|
9097
8997
|
await this.wakeCreator(taskId, project, creator, event.actor, now);
|
|
9098
8998
|
}
|
|
9099
8999
|
await this.wakeDependents(taskId, event.actor, now);
|
|
9100
|
-
const threaded = await this.board.readThread(taskId).then(() => true, () => false);
|
|
9101
|
-
if (!threaded) {
|
|
9102
|
-
this.sweep({ kind: "thread", id: taskId });
|
|
9103
|
-
}
|
|
9104
9000
|
return;
|
|
9105
9001
|
}
|
|
9106
9002
|
case "thread.closed": {
|
|
@@ -9215,6 +9111,21 @@ var Scheduler = class {
|
|
|
9215
9111
|
}
|
|
9216
9112
|
return;
|
|
9217
9113
|
}
|
|
9114
|
+
case "project.archived": {
|
|
9115
|
+
const slug = stringOf(payload["slug"]);
|
|
9116
|
+
if (slug === null) {
|
|
9117
|
+
return;
|
|
9118
|
+
}
|
|
9119
|
+
this.sweep({ kind: "place", project: slug });
|
|
9120
|
+
for (const [key, item] of this.pending) {
|
|
9121
|
+
const { dispatch } = item;
|
|
9122
|
+
if (dispatch.place === slug) {
|
|
9123
|
+
this.pending.delete(key);
|
|
9124
|
+
this.log.info({ agent: dispatch.agent, project: dispatch.project, place: slug }, "dropping project wake, the project is archived");
|
|
9125
|
+
}
|
|
9126
|
+
}
|
|
9127
|
+
return;
|
|
9128
|
+
}
|
|
9218
9129
|
case "agent.left": {
|
|
9219
9130
|
const name = stringOf(payload["name"]);
|
|
9220
9131
|
const project = stringOf(payload["project"]);
|
|
@@ -9245,7 +9156,7 @@ var Scheduler = class {
|
|
|
9245
9156
|
}
|
|
9246
9157
|
case "ops.signal": {
|
|
9247
9158
|
const parsed = OpsSignalSchema.safeParse(payload);
|
|
9248
|
-
if (!parsed.success || !
|
|
9159
|
+
if (!parsed.success || !signalWakes(parsed.data)) {
|
|
9249
9160
|
return;
|
|
9250
9161
|
}
|
|
9251
9162
|
await this.wakeSignalReaders(parsed.data, event.actor, now);
|
|
@@ -9516,11 +9427,12 @@ var Scheduler = class {
|
|
|
9516
9427
|
const scope = stringOf(payload["project"]);
|
|
9517
9428
|
const thread = stringOf(payload["thread"]);
|
|
9518
9429
|
const channel = stringOf(payload["channel"]);
|
|
9430
|
+
const place = stringOf(payload["place"]);
|
|
9519
9431
|
const agent = name === null ? null : await this.tryReadAgent(name);
|
|
9520
9432
|
if (agent === null || scope === null) {
|
|
9521
9433
|
return;
|
|
9522
9434
|
}
|
|
9523
|
-
const beside = thread !== null ? { thread: await this.conversationOf(thread, scope) } : channel !== null ? { channel } : null;
|
|
9435
|
+
const beside = thread !== null ? { thread: await this.conversationOf(thread, scope) } : channel !== null ? { channel } : place !== null ? { place } : null;
|
|
9524
9436
|
if (beside === null) {
|
|
9525
9437
|
return;
|
|
9526
9438
|
}
|
|
@@ -9565,7 +9477,7 @@ var Scheduler = class {
|
|
|
9565
9477
|
}
|
|
9566
9478
|
return {
|
|
9567
9479
|
scope,
|
|
9568
|
-
beside: conversation.kind === "thread" ? { thread: await this.conversationOf(conversation.id, scope) } : { channel: conversation.name }
|
|
9480
|
+
beside: conversation.kind === "thread" ? { thread: await this.conversationOf(conversation.id, scope) } : conversation.kind === "channel" ? { channel: conversation.name } : { place: conversation.project }
|
|
9569
9481
|
};
|
|
9570
9482
|
}
|
|
9571
9483
|
/**
|
|
@@ -9583,14 +9495,23 @@ var Scheduler = class {
|
|
|
9583
9495
|
}
|
|
9584
9496
|
/**
|
|
9585
9497
|
* The conversation a post wakes a citizen in, for a turn in `scope`: its thread's, else its
|
|
9586
|
-
* channel's when that is a channel of the scope other than general, else
|
|
9498
|
+
* channel's when that is a channel of the scope other than general, else, for a turn in the
|
|
9499
|
+
* society scope, its project's when it is a project's channel, general included, else the
|
|
9500
|
+
* scope's home.
|
|
9587
9501
|
*/
|
|
9588
9502
|
async postedIn(scope, channel, threadId) {
|
|
9589
9503
|
if (threadId !== void 0) {
|
|
9590
9504
|
return { thread: await this.conversationOf(threadId, scope) };
|
|
9591
9505
|
}
|
|
9592
|
-
|
|
9593
|
-
|
|
9506
|
+
if (channel === null) {
|
|
9507
|
+
return void 0;
|
|
9508
|
+
}
|
|
9509
|
+
const name = channelConversation(scope, channel);
|
|
9510
|
+
if (name !== void 0) {
|
|
9511
|
+
return { channel: name };
|
|
9512
|
+
}
|
|
9513
|
+
const place = placeConversation(scope, channel);
|
|
9514
|
+
return place === void 0 ? void 0 : { place };
|
|
9594
9515
|
}
|
|
9595
9516
|
async tryReadAgent(name) {
|
|
9596
9517
|
try {
|
|
@@ -9732,7 +9653,7 @@ var Scheduler = class {
|
|
|
9732
9653
|
};
|
|
9733
9654
|
for (const entry of unread) {
|
|
9734
9655
|
if (entry.scope === project) {
|
|
9735
|
-
const beside = entry.thread !== null ? { thread: await this.conversationOf(entry.thread, project) } : entry.channel !== null ? { channel: entry.channel } : void 0;
|
|
9656
|
+
const beside = entry.thread !== null ? { thread: await this.conversationOf(entry.thread, project) } : entry.channel !== null ? { channel: entry.channel } : entry.place !== null ? { place: entry.place } : void 0;
|
|
9736
9657
|
reasonsIn(beside).digestSize += entry.count;
|
|
9737
9658
|
}
|
|
9738
9659
|
}
|
|
@@ -9819,7 +9740,10 @@ var Scheduler = class {
|
|
|
9819
9740
|
if (cron.thread !== void 0) {
|
|
9820
9741
|
return { thread: await this.conversationOf(cron.thread, cron.scope) };
|
|
9821
9742
|
}
|
|
9822
|
-
|
|
9743
|
+
if (cron.channel !== void 0) {
|
|
9744
|
+
return { channel: cron.channel };
|
|
9745
|
+
}
|
|
9746
|
+
return cron.place === void 0 ? void 0 : { place: cron.place };
|
|
9823
9747
|
}
|
|
9824
9748
|
/** Whether a cron's wake waits in the queue or its turn is running. */
|
|
9825
9749
|
cronQueuedOrRunning(id) {
|
|
@@ -9943,25 +9867,56 @@ var Scheduler = class {
|
|
|
9943
9867
|
// ---------------------------------------------------------------------------------------------
|
|
9944
9868
|
// Operations: signals from state, the scaling rule, and the spend report
|
|
9945
9869
|
// ---------------------------------------------------------------------------------------------
|
|
9870
|
+
/**
|
|
9871
|
+
* Logs each condition when it is new and again every `signalRepeatMs` while it holds, and marks
|
|
9872
|
+
* each log with whether it wakes the readers of signals (`wakesNow`), so a repeat only logs.
|
|
9873
|
+
*/
|
|
9946
9874
|
async runOperations(now) {
|
|
9947
9875
|
const signals = await this.collectSignals(now);
|
|
9948
9876
|
const current = new Set(signals.map((signal) => signal.key));
|
|
9949
|
-
for (const
|
|
9950
|
-
|
|
9951
|
-
|
|
9877
|
+
for (const record of [this.state.opsReported, this.state.opsWoken]) {
|
|
9878
|
+
for (const key of Object.keys(record)) {
|
|
9879
|
+
if (!current.has(key)) {
|
|
9880
|
+
delete record[key];
|
|
9881
|
+
}
|
|
9952
9882
|
}
|
|
9953
9883
|
}
|
|
9954
9884
|
for (const signal of signals) {
|
|
9955
9885
|
const reported = this.state.opsReported[signal.key];
|
|
9956
|
-
|
|
9886
|
+
const wakes = this.wakesNow(signal, reported !== void 0, now);
|
|
9887
|
+
if (!wakes && reported !== void 0 && now - Date.parse(reported) < this.timings.signalRepeatMs) {
|
|
9957
9888
|
continue;
|
|
9958
9889
|
}
|
|
9959
|
-
await this.board.publishSignal(signal);
|
|
9890
|
+
await this.board.publishSignal({ ...signal, wakes });
|
|
9960
9891
|
this.state.opsReported[signal.key] = iso(now);
|
|
9892
|
+
if (wakes) {
|
|
9893
|
+
this.state.opsWoken[signal.key] = wokenAt(signal, now);
|
|
9894
|
+
}
|
|
9961
9895
|
}
|
|
9962
9896
|
await this.scaleRoles(now);
|
|
9963
9897
|
await this.reportCost(now);
|
|
9964
9898
|
}
|
|
9899
|
+
/**
|
|
9900
|
+
* Whether a condition wakes the readers of signals at this pass, which only a waking kind does:
|
|
9901
|
+
* when it is new; for an idle member or churn, when it has risen past the level it last woke at
|
|
9902
|
+
* (`wakeLevel`); for the other kinds, once its last wake is `signalWakeMs` old. Checked on every
|
|
9903
|
+
* pass, so a step wakes when it is crossed, between repeats.
|
|
9904
|
+
*/
|
|
9905
|
+
wakesNow(signal, held, now) {
|
|
9906
|
+
if (!WAKING_SIGNALS.has(signal.kind)) {
|
|
9907
|
+
return false;
|
|
9908
|
+
}
|
|
9909
|
+
const last = this.state.opsWoken[signal.key];
|
|
9910
|
+
if (last === void 0) {
|
|
9911
|
+
if (held) {
|
|
9912
|
+
this.state.opsWoken[signal.key] = wokenAt(signal, now);
|
|
9913
|
+
return false;
|
|
9914
|
+
}
|
|
9915
|
+
return true;
|
|
9916
|
+
}
|
|
9917
|
+
const level = wakeLevel(signal);
|
|
9918
|
+
return level === void 0 ? now - Date.parse(last.at) >= this.timings.signalWakeMs : level > (last.level ?? 0);
|
|
9919
|
+
}
|
|
9965
9920
|
/** Every operations condition that holds right now, from counters and timers only. */
|
|
9966
9921
|
async collectSignals(now) {
|
|
9967
9922
|
const signals = [];
|
|
@@ -10204,8 +10159,7 @@ var Scheduler = class {
|
|
|
10204
10159
|
const attempt = steer({
|
|
10205
10160
|
agent: dispatch.agent,
|
|
10206
10161
|
project: dispatch.project,
|
|
10207
|
-
...dispatch
|
|
10208
|
-
...dispatch.channel === void 0 ? {} : { channel: dispatch.channel }
|
|
10162
|
+
...namedIn(dispatch)
|
|
10209
10163
|
}).then((outcome) => {
|
|
10210
10164
|
const current = this.pending.get(key);
|
|
10211
10165
|
if (outcome !== "busy" && current !== void 0) {
|
|
@@ -10226,7 +10180,7 @@ var Scheduler = class {
|
|
|
10226
10180
|
return false;
|
|
10227
10181
|
}
|
|
10228
10182
|
const { dispatch } = item;
|
|
10229
|
-
const cursor = await this.board.digestCursor(dispatch.agent, dispatch.project, conversationPart(
|
|
10183
|
+
const cursor = await this.board.digestCursor(dispatch.agent, dispatch.project, conversationPart(namedIn(dispatch)));
|
|
10230
10184
|
return cursor !== null && cursor >= item.newestMessage;
|
|
10231
10185
|
}
|
|
10232
10186
|
async dispatchReady(now) {
|
|
@@ -10263,6 +10217,7 @@ var Scheduler = class {
|
|
|
10263
10217
|
project: dispatch.project,
|
|
10264
10218
|
thread: dispatch.thread?.id,
|
|
10265
10219
|
channel: dispatch.channel,
|
|
10220
|
+
place: dispatch.place,
|
|
10266
10221
|
trigger: dispatch.trigger.kind
|
|
10267
10222
|
}, "dispatching turn");
|
|
10268
10223
|
const crons = dispatch.crons ?? [];
|
|
@@ -10278,6 +10233,7 @@ var Scheduler = class {
|
|
|
10278
10233
|
exitReason: record.exitReason,
|
|
10279
10234
|
costUsd: record.costUsd
|
|
10280
10235
|
}, "turn finished");
|
|
10236
|
+
await this.retryRefused(key, { ...item, dispatch }, record);
|
|
10281
10237
|
} catch (error) {
|
|
10282
10238
|
this.log.error({ agent: dispatch.agent, project: dispatch.project, error: String(error) }, "turn crashed");
|
|
10283
10239
|
} finally {
|
|
@@ -10296,6 +10252,48 @@ var Scheduler = class {
|
|
|
10296
10252
|
}
|
|
10297
10253
|
}
|
|
10298
10254
|
}
|
|
10255
|
+
/**
|
|
10256
|
+
* A turn the model service turned away for now, overloaded or failing on its side, is sent again:
|
|
10257
|
+
* the same wake in the same conversation, after the next delay of `transientRetryMs`, with every
|
|
10258
|
+
* check a queued wake gets at dispatch. A wake already queued there, as a heartbeat's or a new
|
|
10259
|
+
* post's, runs the conversation instead. Once the delays are spent the heartbeat takes over, and
|
|
10260
|
+
* a `turn_refused` signal, which wakes nobody, says so. A turn that ends any other way ends the run.
|
|
10261
|
+
*/
|
|
10262
|
+
async retryRefused(key, item, record) {
|
|
10263
|
+
if (record.transient !== true || record.exitReason !== "error") {
|
|
10264
|
+
this.refused.delete(key);
|
|
10265
|
+
return;
|
|
10266
|
+
}
|
|
10267
|
+
const refused = (this.refused.get(key) ?? 0) + 1;
|
|
10268
|
+
this.refused.set(key, refused);
|
|
10269
|
+
const { dispatch } = item;
|
|
10270
|
+
const delay = this.timings.transientRetryMs[refused - 1];
|
|
10271
|
+
if (delay !== void 0) {
|
|
10272
|
+
if (this.pending.has(key)) {
|
|
10273
|
+
return;
|
|
10274
|
+
}
|
|
10275
|
+
const { steeredThrough: _steered, ...retry } = item;
|
|
10276
|
+
this.pending.set(key, { ...retry, readyAt: this.now().getTime() + delay });
|
|
10277
|
+
this.log.info({ agent: dispatch.agent, project: dispatch.project, refused, delayMs: delay }, "the model service turned the turn away, sending its wake again");
|
|
10278
|
+
return;
|
|
10279
|
+
}
|
|
10280
|
+
if (refused !== this.timings.transientRetryMs.length + 1) {
|
|
10281
|
+
return;
|
|
10282
|
+
}
|
|
10283
|
+
const agent = await this.tryReadAgent(dispatch.agent);
|
|
10284
|
+
const where = dispatch.thread !== void 0 ? `thread ${dispatch.thread.id}` : dispatch.channel !== void 0 ? `channel ${dispatch.channel}` : dispatch.place !== void 0 ? `project ${dispatch.place}'s channels` : "its home";
|
|
10285
|
+
await this.board.publishSignal({
|
|
10286
|
+
kind: "turn_refused",
|
|
10287
|
+
key: `turn_refused:${key}`,
|
|
10288
|
+
summary: `${dispatch.agent}'s turns in ${where} were turned away by the model service ${refused} times in a row${record.model === null ? "" : ` on ${record.model}`}, the last with "${record.error ?? "no error"}"; its heartbeat takes over`,
|
|
10289
|
+
value: refused,
|
|
10290
|
+
threshold: refused,
|
|
10291
|
+
agent: dispatch.agent,
|
|
10292
|
+
...agent === null ? {} : { role: agent.role },
|
|
10293
|
+
...dispatch.project === SOCIETY_SCOPE ? {} : { project: dispatch.project },
|
|
10294
|
+
...dispatch.thread?.task === true ? { taskId: dispatch.thread.id } : {}
|
|
10295
|
+
});
|
|
10296
|
+
}
|
|
10299
10297
|
/**
|
|
10300
10298
|
* Whether a ready turn waits for another of its citizen's turns that must run alone: its
|
|
10301
10299
|
* onboarding in the project, queued or running, which comes before any other work of its there;
|
|
@@ -10379,9 +10377,9 @@ import { randomUUID } from "node:crypto";
|
|
|
10379
10377
|
|
|
10380
10378
|
// packages/turn-host/dist/render.js
|
|
10381
10379
|
var TURN_CONTRACT = [
|
|
10382
|
-
"- Every turn belongs to one conversation, named at the top of your prompt: your home conversation in a project or the society, for its general channel and for work tied to no other channel and no thread; the conversation of one other channel of the place, a workstream such as a release; or the conversation of one thread you take part in, a task's, a proposal's, or a topic's. Each has its own session and its own turns, which may run while this one does. The prompt carries this conversation only: in a thread, its new messages, or the whole thread when the conversation is new, and in a task's thread the task and its stage; in a channel's or at home, that channel's or general's messages that concern you. Read anything else you need with get_task, search, and the board's files, and leave other channels', other threads', and other projects' work to their own turns.",
|
|
10380
|
+
"- Every turn belongs to one conversation, named at the top of your prompt: your home conversation in a project or the society, for its general channel and for work tied to no other channel and no thread; the conversation of one other channel of the place, a workstream such as a release; outside projects, the conversation of one project's channels, general and its workstreams together, for posts there that ask something of you while you are not a member; or the conversation of one thread you take part in, a task's, a proposal's, or a topic's. Each has its own session and its own turns, which may run while this one does. The prompt carries this conversation only: in a thread, its new messages, or the whole thread when the conversation is new, and in a task's thread the task and its stage; in a channel's, a project's, or at home, that channel's, that project's channels', or general's messages that concern you. Read anything else you need with get_task, search, and the board's files, and leave other channels', other threads', and other projects' work to their own turns.",
|
|
10383
10381
|
"- Act through the board tools (the `board` MCP server). Holding a stage is a lease; every turn that touches the task renews it.",
|
|
10384
|
-
"- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task; nothing lands from it by itself. In a proposal's, a topic's, or a channel's conversation it is a worktree of its own, detached at the project's default branch, which goes when the thread closes or the channel is archived, so work that should last goes on a task's branch. Before each turn the runner fetches the project's remote and brings what holds no work of yours up to the default branch: your own branch when it has no commits of its own, a task's branch nobody has worked on, and a detached worktree with nothing uncommitted; what it leaves where it is, and why, is under Your workspace in the prompt. It never merges or commits for you.",
|
|
10382
|
+
"- Work only inside your worktree. In a task's conversation it is a worktree of its own on the task's branch `task/<id>`: commit your work there. The branch is checked out in one worktree at a time and kept for the turns of whoever holds the task's stage, so a turn of anyone else there, or one that starts while another turn has the branch, works detached at its tip, which Your workspace then says; its commits go on the branch by a fast-forward when a turn in the task ends with the branch free, and the task's thread is told of commits that no longer build on its tip. After each turn the runner commits what you left on the branch and lets it go for the next holder. Your home worktree is on your own branch, `agent/<name>`, for work tied to no task; nothing lands from it by itself. In a proposal's, a topic's, or a channel's conversation it is a worktree of its own, detached at the project's default branch, which goes when the thread closes or the channel is archived, so work that should last goes on a task's branch. Before each turn the runner fetches the project's remote and brings what holds no work of yours up to the default branch: your own branch when it has no commits of its own, a task's branch nobody has worked on, and a detached worktree with nothing uncommitted; what it leaves where it is, and why, is under Your workspace in the prompt. It never merges or commits for you.",
|
|
10385
10383
|
"- Posts that would wake this conversation may arrive while you work, delivered into the turn under a heading that says so and formatted like the digest. They are part of this turn: act on them now, after the step in hand, or not at all, as with the digest. The user may also stop a turn; the next turn is told.",
|
|
10386
10384
|
"- Silence is allowed. If the digest needs no reply, post nothing.",
|
|
10387
10385
|
"- 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.",
|
|
@@ -10414,7 +10412,7 @@ var GOVERNANCE = [
|
|
|
10414
10412
|
' - retirement: {"agent", "reason"}',
|
|
10415
10413
|
' - 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',
|
|
10416
10414
|
` - 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`,
|
|
10417
|
-
' - 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.
|
|
10415
|
+
' - 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.',
|
|
10418
10416
|
"- 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.",
|
|
10419
10417
|
"- 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.",
|
|
10420
10418
|
"- 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."
|
|
@@ -10454,7 +10452,7 @@ function renderInstructions(input) {
|
|
|
10454
10452
|
if (norms.length > 0) {
|
|
10455
10453
|
sections.push("", "## Society norms", "", norms);
|
|
10456
10454
|
}
|
|
10457
|
-
sections.push("", "## Core memory", "", memory.length === 0 ? "(empty)" : memory, "", "## Skills", "", ...renderSkillsIndex(input.skills ?? []), "", "## Where things are", "", `- Your home directory: ${input.homeDir}. memory/core.md is loaded every turn; memory/<topic>.md is your archive; skills/<name>/SKILL.md are your skills; projects/<slug>/notes.md are your notes. You may read and write all of it, and the board's search covers your archive and skills for you alone. The runner keeps it in git and shares it with the board after every turn, so whatever machine your next turn runs on has it; leave git there to the runner. Files over ${HOME_FILE_LIMIT_BYTES / (1024 * 1024)} MB are not kept.`, `- ${HOME_SCRATCH}/ in your home is your working folder in turns outside any project, and a thread's or a
|
|
10455
|
+
sections.push("", "## Core memory", "", memory.length === 0 ? "(empty)" : memory, "", "## Skills", "", ...renderSkillsIndex(input.skills ?? []), "", "## Where things are", "", `- Your home directory: ${input.homeDir}. memory/core.md is loaded every turn; memory/<topic>.md is your archive; skills/<name>/SKILL.md are your skills; projects/<slug>/notes.md are your notes. You may read and write all of it, and the board's search covers your archive and skills for you alone. The runner keeps it in git and shares it with the board after every turn, so whatever machine your next turn runs on has it; leave git there to the runner. Files over ${HOME_FILE_LIMIT_BYTES / (1024 * 1024)} MB are not kept.`, `- ${HOME_SCRATCH}/ in your home is your working folder in turns outside any project, and a thread's, a channel's, or a project's channels' conversation there works in a folder of its own inside it, which goes when the thread closes or the channel or the project is archived: none of it is kept or shared, so put downloads, environments, and other working files there, and keep the rest of your home for what you mean to remember.`, `- The board projection: ${input.boardDir} (read-only markdown: society and project channels, tasks, threads; shared knowledge under projects/<slug>/knowledge/ and society/knowledge/; the society's skills under society/skills/; each project's dashboard, the status page the user reads on its view, at projects/<slug>/dashboard.md, which its members change with update_dashboard). Search it with your file tools; act through the board tools.`, "", "## Turn contract", "", TURN_CONTRACT, "", "## Planning", "", PLANNING, "", "## Governance", "", GOVERNANCE);
|
|
10458
10456
|
if (input.onboarding !== void 0) {
|
|
10459
10457
|
sections.push("", "## First turn", "", renderOnboardingPreamble(input.onboarding));
|
|
10460
10458
|
}
|
|
@@ -10608,7 +10606,7 @@ function buildTurnPrompt(input) {
|
|
|
10608
10606
|
lines.push("Operations signals arrived; they are listed under Operations signals below. Decide whether a proposal is warranted, and stay silent if not.");
|
|
10609
10607
|
}
|
|
10610
10608
|
if (dispatch.trigger.kind === "stage") {
|
|
10611
|
-
lines.push("A stage is waiting for you: claim it with claim_task unless you hold it already, do the work in your worktree, which is on the task's branch, commit there, then advance_task. If the plan no longer fits, reshape it with plan_task.");
|
|
10609
|
+
lines.push("A stage is waiting for you: claim it with claim_task unless you hold it already, do the work in your worktree, which is on the task's branch unless Your workspace says otherwise, commit there, then advance_task. If the plan no longer fits, reshape it with plan_task.");
|
|
10612
10610
|
}
|
|
10613
10611
|
if (dispatch.trigger.kind === "task_done") {
|
|
10614
10612
|
lines.push("A task you created is done. Tell whoever asked for it, if anyone did.");
|
|
@@ -10627,8 +10625,11 @@ function buildTurnPrompt(input) {
|
|
|
10627
10625
|
if (conversation === null && dispatch.channel !== void 0) {
|
|
10628
10626
|
const ref = channelRef(dispatch.project === SOCIETY_SCOPE ? null : dispatch.project, dispatch.channel);
|
|
10629
10627
|
lines.push(`This turn is in your conversation of ${ref}, a workstream with turns of its own: its posts, and the work it asks for. Your home conversation for ${dispatch.project} carries its general channel, and each other channel and each thread you take part in is a conversation of its own; read one when you need it, and leave its work to its turn. Post here with post_message and channel ${ref}.`);
|
|
10628
|
+
} else if (conversation === null && dispatch.place !== void 0) {
|
|
10629
|
+
lines.push(`This turn is in your conversation of project ${dispatch.place}'s channels, from outside the project: the posts in its general and its other channels that concern you, and the work they ask for. Your home conversation for ${dispatch.project} carries its general channel, and each other project's channels, each other channel, and each thread you take part in is a conversation of its own with turns of its own; read one when you need it, and leave its work to its turn. Answer in the channel a post came from with post_message.`);
|
|
10630
10630
|
} else if (conversation === null) {
|
|
10631
|
-
|
|
10631
|
+
const besides = dispatch.project === SOCIETY_SCOPE ? "Each other channel, each project's channels you are asked in from outside the project, and each thread you take part in, a task's, a proposal's, or a topic's," : "Each other channel, a workstream, and each thread you take part in, a task's, a proposal's, or a topic's,";
|
|
10632
|
+
lines.push(`This turn is in your home conversation for ${dispatch.project}: its general channel, and work tied to no other channel and no thread. ${besides} is a conversation of its own with turns of its own; read one when you need it, and leave its work to its turn.`);
|
|
10632
10633
|
} else {
|
|
10633
10634
|
const { thread, task } = conversation;
|
|
10634
10635
|
const about = thread.subject === void 0 ? "a topic" : `${thread.subject.kind} ${thread.subject.id}`;
|
|
@@ -10663,7 +10664,9 @@ function buildTurnPrompt(input) {
|
|
|
10663
10664
|
lines.push("", "## The user stopped your previous turn", "", `It was ended on purpose at ${last.endedAt ?? "its end"}, not by a failure. Do not pick its work up again unless this turn asks for it. The worktree may hold uncommitted changes; run git status before doing anything else.`);
|
|
10664
10665
|
}
|
|
10665
10666
|
if (last !== null && (last.exitReason === "error" || last.exitReason === "timeout")) {
|
|
10666
|
-
lines.push("", "## Your previous turn did not finish", "", `It ended with ${last.exitReason}${last.error === null ? "" : `: ${last.error}`}.`,
|
|
10667
|
+
lines.push("", "## Your previous turn did not finish", "", `It ended with ${last.exitReason}${last.error === null ? "" : `: ${last.error}`}.`, ...last.sessionDropped === true ? [
|
|
10668
|
+
"Its CLI could not resume this conversation's session, so the session was dropped and this turn starts a new one: nothing of your earlier turns here carries over but what is on the board and in your home."
|
|
10669
|
+
] : [], "The worktree may hold uncommitted changes. Run git status before doing anything else.");
|
|
10667
10670
|
}
|
|
10668
10671
|
const society = input.societyView;
|
|
10669
10672
|
if (society !== void 0 && society !== null) {
|
|
@@ -10719,7 +10722,7 @@ function buildTurnPrompt(input) {
|
|
|
10719
10722
|
if (signals.length === 0) {
|
|
10720
10723
|
lines.push("None since your last turn here.");
|
|
10721
10724
|
} else {
|
|
10722
|
-
lines.push("Counters and timers the board logged since your last turn here, oldest first. A condition that persists is logged again every few hours; one that clears is not logged.", "");
|
|
10725
|
+
lines.push("Counters and timers the board logged since your last turn here, each condition once at its latest log, oldest first, with the hourly spend reports summed into one line. A condition that persists is logged again every few hours; one that clears is not logged.", "");
|
|
10723
10726
|
for (const { ts, signal } of signals) {
|
|
10724
10727
|
lines.push(signalLine(ts, signal));
|
|
10725
10728
|
}
|
|
@@ -10759,6 +10762,13 @@ function capitalized(text) {
|
|
|
10759
10762
|
}
|
|
10760
10763
|
|
|
10761
10764
|
// packages/turn-host/dist/host.js
|
|
10765
|
+
function conversationOfJob(job) {
|
|
10766
|
+
return {
|
|
10767
|
+
...job.thread === void 0 ? {} : { thread: job.thread },
|
|
10768
|
+
...job.channel === void 0 ? {} : { channel: job.channel },
|
|
10769
|
+
...job.place === void 0 ? {} : { place: job.place }
|
|
10770
|
+
};
|
|
10771
|
+
}
|
|
10762
10772
|
var SILENT = { info() {
|
|
10763
10773
|
}, warn() {
|
|
10764
10774
|
}, error() {
|
|
@@ -10769,7 +10779,50 @@ var DEFAULT_RESIDENT_IDLE_MS = 10 * 6e4;
|
|
|
10769
10779
|
var LEASE_RENEW_MS = 10 * 6e4;
|
|
10770
10780
|
var USER_POST_TRIGGER2 = "user_post";
|
|
10771
10781
|
var OPS_TRIGGER2 = "ops_event";
|
|
10782
|
+
var RESUME_RETRIES = 3;
|
|
10783
|
+
function stuckNotice(stuck) {
|
|
10784
|
+
const more = stuck.total - stuck.commits.length;
|
|
10785
|
+
const commits = `${stuck.commits.map((commit) => `\`${commit}\``).join(", ")}${more > 0 ? `, and ${more} more` : ""}`;
|
|
10786
|
+
return `${stuck.agent}'s worktree for this task holds commits that \`${stuck.branch}\` lacks and that the runner cannot fast-forward it to, because they no longer build on its tip or another worktree's commits build on it too: ${commits}. Until someone puts them on the branch, it lacks them, and so does what lands from it; any worktree of the project's clone can cherry-pick or merge them by id.`;
|
|
10787
|
+
}
|
|
10772
10788
|
var MAX_SIGNALS = 30;
|
|
10789
|
+
var SIGNAL_READ = 500;
|
|
10790
|
+
var REPORT_TURNS = /^(\d+) turn\(s\) completed /;
|
|
10791
|
+
function sumSpend(records, partial) {
|
|
10792
|
+
const reports = records.filter((record) => record.signal.kind === "turn_cost");
|
|
10793
|
+
const last = reports.at(-1);
|
|
10794
|
+
if (last === void 0 || reports.length === 1 && !partial) {
|
|
10795
|
+
return [...records];
|
|
10796
|
+
}
|
|
10797
|
+
let cost = 0;
|
|
10798
|
+
let turns2 = 0;
|
|
10799
|
+
let dearest = last;
|
|
10800
|
+
for (const report of reports) {
|
|
10801
|
+
cost += report.signal.value;
|
|
10802
|
+
const counted = REPORT_TURNS.exec(report.signal.summary)?.[1];
|
|
10803
|
+
turns2 = turns2 === null || counted === void 0 ? null : turns2 + Number(counted);
|
|
10804
|
+
if (report.signal.value > dearest.signal.value) {
|
|
10805
|
+
dearest = report;
|
|
10806
|
+
}
|
|
10807
|
+
}
|
|
10808
|
+
const first = reports[0] ?? last;
|
|
10809
|
+
const one = reports.length === 1;
|
|
10810
|
+
const summary = [
|
|
10811
|
+
`${turns2 === null ? "turns" : `${turns2} turn(s)`} completed for $${cost.toFixed(2)} in ${reports.length} hourly report(s) logged ${one ? `at ${last.ts}` : `from ${first.ts} to ${last.ts}`}`,
|
|
10812
|
+
...one ? [] : [`the dearest, logged at ${dearest.ts}, was $${dearest.signal.value.toFixed(2)}`],
|
|
10813
|
+
...partial ? [`the read holds the newest ${SIGNAL_READ} signals, so these are lower bounds`] : []
|
|
10814
|
+
].join("; ");
|
|
10815
|
+
const summed = { ...last, signal: { ...last.signal, summary, value: cost } };
|
|
10816
|
+
return records.flatMap((record) => record === last ? [summed] : record.signal.kind === "turn_cost" ? [] : [record]);
|
|
10817
|
+
}
|
|
10818
|
+
function latestPerKey(records) {
|
|
10819
|
+
const latest = /* @__PURE__ */ new Map();
|
|
10820
|
+
for (const record of records) {
|
|
10821
|
+
latest.delete(record.signal.key);
|
|
10822
|
+
latest.set(record.signal.key, record);
|
|
10823
|
+
}
|
|
10824
|
+
return [...latest.values()];
|
|
10825
|
+
}
|
|
10773
10826
|
var TurnHost = class {
|
|
10774
10827
|
board;
|
|
10775
10828
|
mcpUrl;
|
|
@@ -10813,13 +10866,15 @@ var TurnHost = class {
|
|
|
10813
10866
|
const actor = { name: agent.name, role: agent.role };
|
|
10814
10867
|
const thread = dispatch.thread;
|
|
10815
10868
|
const channel = thread === void 0 ? dispatch.channel : void 0;
|
|
10816
|
-
const
|
|
10869
|
+
const place = thread === void 0 && channel === void 0 && dispatch.project === SOCIETY_SCOPE ? dispatch.place : void 0;
|
|
10870
|
+
const part = conversationPart({ thread: thread?.id, channel, place });
|
|
10817
10871
|
const startedAt = this.now().toISOString();
|
|
10818
10872
|
const base = {
|
|
10819
10873
|
agent: agent.name,
|
|
10820
10874
|
project: dispatch.project,
|
|
10821
10875
|
...thread === void 0 ? {} : { thread: thread.id },
|
|
10822
10876
|
...channel === void 0 ? {} : { channel },
|
|
10877
|
+
...place === void 0 ? {} : { place },
|
|
10823
10878
|
runner: runner.name,
|
|
10824
10879
|
cli: agent.cli,
|
|
10825
10880
|
session: null,
|
|
@@ -10844,23 +10899,25 @@ var TurnHost = class {
|
|
|
10844
10899
|
const charter = await this.board.readRole(agent.role);
|
|
10845
10900
|
const societyScope = dispatch.project === SOCIETY_SCOPE;
|
|
10846
10901
|
const taskId = thread?.task === true ? thread.id : void 0;
|
|
10902
|
+
const task = taskId === void 0 ? null : await this.board.findTask(taskId).then((found) => found.task).catch(() => null);
|
|
10847
10903
|
const project = societyScope ? null : await this.board.readProject(dispatch.project);
|
|
10848
|
-
const workspace = await this.workspaceOf(project, thread, channel);
|
|
10904
|
+
const workspace = await this.workspaceOf(project, thread, channel, place, task);
|
|
10849
10905
|
const sessions = await this.board.readSessions(agent.name, dispatch.project, part);
|
|
10850
10906
|
const recorded = sessions.runner === runner.name ? sessions[agent.cli] : void 0;
|
|
10851
10907
|
const newSession = recorded === void 0;
|
|
10852
10908
|
const lastTurn = await this.board.readLastTurn(agent.name, dispatch.project, part);
|
|
10853
10909
|
const costSoFarUsd = !newSession && lastTurn?.session === recorded ? lastTurn.sessionCostUsd ?? lastTurn.costUsd : 0;
|
|
10910
|
+
const resumeRetries = !newSession && lastTurn?.session === recorded && lastTurn.resumeFailed === "retry" ? lastTurn.resumeRetries ?? 1 : 0;
|
|
10854
10911
|
const digest = await this.board.readDigest({
|
|
10855
10912
|
...actor,
|
|
10856
10913
|
scope: dispatch.project,
|
|
10857
10914
|
...thread === void 0 ? {} : { thread: thread.id },
|
|
10858
|
-
...channel === void 0 ? {} : { channel }
|
|
10915
|
+
...channel === void 0 ? {} : { channel },
|
|
10916
|
+
...place === void 0 ? {} : { place }
|
|
10859
10917
|
}, { advance: false, limit: thread === void 0 ? 50 : 100 });
|
|
10860
10918
|
const threadSoFar = thread !== void 0 && newSession ? (await this.board.listThread(thread.id).catch(() => [])).slice(-100) : null;
|
|
10861
10919
|
const messages = threadSoFar ?? digest.messages;
|
|
10862
10920
|
const cursorAfter = threadSoFar?.at(-1)?.id ?? digest.cursor;
|
|
10863
|
-
const task = taskId === void 0 ? null : await this.board.findTask(taskId).then((found) => found.task).catch(() => null);
|
|
10864
10921
|
const held = task === null ? [] : (await this.board.heldClaims(agent.name)).filter((each) => each.id === task.id);
|
|
10865
10922
|
const onboarding = dispatch.onboarding || newSession && part === void 0 ? {
|
|
10866
10923
|
agentName: agent.name,
|
|
@@ -10903,7 +10960,7 @@ var TurnHost = class {
|
|
|
10903
10960
|
dir: `${PATH_TOKENS.board}/projects/${dispatch.project}/knowledge`,
|
|
10904
10961
|
topics: await this.board.listKnowledge(dispatch.project)
|
|
10905
10962
|
};
|
|
10906
|
-
const signals = part === void 0 && charter.wakeTriggers.includes(OPS_TRIGGER2) ?
|
|
10963
|
+
const signals = part === void 0 && charter.wakeTriggers.includes(OPS_TRIGGER2) ? await this.signalsSince(agent, dispatch.project, lastTurn) : null;
|
|
10907
10964
|
const waiting = task !== null && task.status === "open" && !task.completing && mayHoldStage(actor, task) ? [task] : [];
|
|
10908
10965
|
const threads = /* @__PURE__ */ new Map();
|
|
10909
10966
|
for (const id of new Set(messages.flatMap((message) => message.thread ?? []))) {
|
|
@@ -10919,7 +10976,7 @@ var TurnHost = class {
|
|
|
10919
10976
|
fresh: threadSoFar !== null,
|
|
10920
10977
|
...threadRecord.state === "closed" ? { replyIn: await this.replyIn(threadRecord) } : {}
|
|
10921
10978
|
};
|
|
10922
|
-
const ending = dispatch.trigger.kind === "closing" ? await this.endingOf(dispatch.project, threadRecord, task, channel) : null;
|
|
10979
|
+
const ending = dispatch.trigger.kind === "closing" ? await this.endingOf(dispatch.project, threadRecord, task, channel, place) : null;
|
|
10923
10980
|
const society = await this.board.society();
|
|
10924
10981
|
const crons = [];
|
|
10925
10982
|
for (const id of dispatch.crons ?? []) {
|
|
@@ -10968,6 +11025,7 @@ var TurnHost = class {
|
|
|
10968
11025
|
scope: dispatch.project,
|
|
10969
11026
|
...thread === void 0 ? {} : { thread: thread.id },
|
|
10970
11027
|
...channel === void 0 ? {} : { channel },
|
|
11028
|
+
...place === void 0 ? {} : { place },
|
|
10971
11029
|
session: recorded ?? null,
|
|
10972
11030
|
costSoFarUsd,
|
|
10973
11031
|
instructions,
|
|
@@ -10994,6 +11052,7 @@ var TurnHost = class {
|
|
|
10994
11052
|
stopRequestedBy: null,
|
|
10995
11053
|
held: held.map((each) => each.id),
|
|
10996
11054
|
costSoFarUsd,
|
|
11055
|
+
resumeRetries,
|
|
10997
11056
|
configuredModel: agent.model ?? null,
|
|
10998
11057
|
transcript: [],
|
|
10999
11058
|
coldToken: residentKey === null ? token : null,
|
|
@@ -11007,6 +11066,7 @@ var TurnHost = class {
|
|
|
11007
11066
|
project: dispatch.project,
|
|
11008
11067
|
thread: thread?.id,
|
|
11009
11068
|
channel,
|
|
11069
|
+
place,
|
|
11010
11070
|
runner: runner.name,
|
|
11011
11071
|
session: recorded,
|
|
11012
11072
|
newSession,
|
|
@@ -11043,8 +11103,7 @@ var TurnHost = class {
|
|
|
11043
11103
|
turnId,
|
|
11044
11104
|
agent: turn.job.agent,
|
|
11045
11105
|
project: turn.job.scope,
|
|
11046
|
-
|
|
11047
|
-
channel: turn.job.channel,
|
|
11106
|
+
...conversationOfJob(turn.job),
|
|
11048
11107
|
messages: sent.messages
|
|
11049
11108
|
}).catch((error) => {
|
|
11050
11109
|
this.log.warn({ turnId, error: String(error) }, "could not record a steer");
|
|
@@ -11052,17 +11111,14 @@ var TurnHost = class {
|
|
|
11052
11111
|
}
|
|
11053
11112
|
}
|
|
11054
11113
|
turn.transcript.push(entry);
|
|
11055
|
-
this.onEvent?.(turn.job.agent, turn.job.scope, entry.event,
|
|
11056
|
-
thread: turn.job.thread,
|
|
11057
|
-
channel: turn.job.channel
|
|
11058
|
-
});
|
|
11114
|
+
this.onEvent?.(turn.job.agent, turn.job.scope, entry.event, conversationOfJob(turn.job));
|
|
11059
11115
|
}
|
|
11060
11116
|
}
|
|
11061
11117
|
/** The turn in flight in a conversation, if any. */
|
|
11062
11118
|
turnIn(agent, scope, conversation = {}) {
|
|
11063
11119
|
for (const turn of this.open.values()) {
|
|
11064
11120
|
const { job } = turn;
|
|
11065
|
-
if (job.agent === agent && job.scope === scope && job.thread === conversation.thread && job.channel === conversation.channel) {
|
|
11121
|
+
if (job.agent === agent && job.scope === scope && job.thread === conversation.thread && job.channel === conversation.channel && job.place === conversation.place) {
|
|
11066
11122
|
return { turnId: job.turnId, runner: turn.runner, cli: job.cli };
|
|
11067
11123
|
}
|
|
11068
11124
|
}
|
|
@@ -11073,8 +11129,7 @@ var TurnHost = class {
|
|
|
11073
11129
|
return [...this.open.values()].map(({ job }) => ({
|
|
11074
11130
|
agent: job.agent,
|
|
11075
11131
|
scope: job.scope,
|
|
11076
|
-
...job
|
|
11077
|
-
...job.channel === void 0 ? {} : { channel: job.channel }
|
|
11132
|
+
...conversationOfJob(job)
|
|
11078
11133
|
}));
|
|
11079
11134
|
}
|
|
11080
11135
|
/** The turns in flight, for the interface: their conversations and where a mention reaches them. */
|
|
@@ -11083,8 +11138,7 @@ var TurnHost = class {
|
|
|
11083
11138
|
turnId: turn.job.turnId,
|
|
11084
11139
|
agent: turn.job.agent,
|
|
11085
11140
|
scope: turn.job.scope,
|
|
11086
|
-
...turn.job
|
|
11087
|
-
...turn.job.channel === void 0 ? {} : { channel: turn.job.channel },
|
|
11141
|
+
...conversationOfJob(turn.job),
|
|
11088
11142
|
cli: turn.job.cli,
|
|
11089
11143
|
...turn.record.trigger.channel === void 0 ? {} : { askedIn: turn.record.trigger.channel },
|
|
11090
11144
|
runner: turn.runner
|
|
@@ -11104,12 +11158,7 @@ var TurnHost = class {
|
|
|
11104
11158
|
return "busy";
|
|
11105
11159
|
}
|
|
11106
11160
|
const { job } = turn;
|
|
11107
|
-
const digest = await this.board.readDigest({
|
|
11108
|
-
...turn.actor,
|
|
11109
|
-
scope: job.scope,
|
|
11110
|
-
...job.thread === void 0 ? {} : { thread: job.thread },
|
|
11111
|
-
...job.channel === void 0 ? {} : { channel: job.channel }
|
|
11112
|
-
}, {
|
|
11161
|
+
const digest = await this.board.readDigest({ ...turn.actor, scope: job.scope, ...conversationOfJob(job) }, {
|
|
11113
11162
|
...turn.delivered === null ? {} : { since_cursor: turn.delivered },
|
|
11114
11163
|
advance: false,
|
|
11115
11164
|
limit: 50
|
|
@@ -11169,6 +11218,8 @@ var TurnHost = class {
|
|
|
11169
11218
|
}
|
|
11170
11219
|
const { job } = turn;
|
|
11171
11220
|
const part = conversationPart(job);
|
|
11221
|
+
const resumeRetries = outcome.resumeFailed === "retry" ? turn.resumeRetries + 1 : void 0;
|
|
11222
|
+
const dropSession = job.session !== null && (outcome.resumeFailed === "lost" || (resumeRetries ?? 0) >= RESUME_RETRIES);
|
|
11172
11223
|
const finished = {
|
|
11173
11224
|
...turn.record,
|
|
11174
11225
|
session: outcome.session,
|
|
@@ -11182,20 +11233,36 @@ var TurnHost = class {
|
|
|
11182
11233
|
toolCalls: turn.transcript.filter((entry) => entry.event.type === "tool_call").length,
|
|
11183
11234
|
model: outcome.model ?? turn.configuredModel,
|
|
11184
11235
|
...outcome.work === null ? {} : { work: outcome.work },
|
|
11185
|
-
...outcome.exitReason === "stopped" ? { stoppedBy: turn.stopRequestedBy ?? USER_NAME } : {}
|
|
11236
|
+
...outcome.exitReason === "stopped" ? { stoppedBy: turn.stopRequestedBy ?? USER_NAME } : {},
|
|
11237
|
+
...outcome.resumeFailed === void 0 ? {} : { resumeFailed: outcome.resumeFailed },
|
|
11238
|
+
...outcome.transient === true ? { transient: true } : {},
|
|
11239
|
+
...resumeRetries === void 0 ? {} : { resumeRetries },
|
|
11240
|
+
...dropSession ? { sessionDropped: true } : {}
|
|
11186
11241
|
};
|
|
11187
11242
|
const ended = await this.conversationEnded(job);
|
|
11188
11243
|
const closing = turn.record.trigger.kind === "closing";
|
|
11189
11244
|
const decided = closing && outcome.exitReason !== "error" && outcome.exitReason !== "timeout";
|
|
11190
11245
|
const recorded = await this.recorded(turn, async () => {
|
|
11191
|
-
if (
|
|
11246
|
+
if (dropSession && job.session !== null) {
|
|
11247
|
+
await this.board.dropSession(job.agent, job.scope, job.cli, job.session, part);
|
|
11248
|
+
this.log.warn({
|
|
11249
|
+
agent: job.agent,
|
|
11250
|
+
project: job.scope,
|
|
11251
|
+
thread: job.thread,
|
|
11252
|
+
channel: job.channel,
|
|
11253
|
+
place: job.place,
|
|
11254
|
+
session: job.session,
|
|
11255
|
+
resumeFailed: outcome.resumeFailed,
|
|
11256
|
+
resumeRetries
|
|
11257
|
+
}, "dropped a session its CLI could not resume");
|
|
11258
|
+
} else if (outcome.session !== null && outcome.session !== job.session) {
|
|
11192
11259
|
await this.board.writeSession(job.agent, job.scope, job.cli, outcome.session, turn.runner, part);
|
|
11193
11260
|
}
|
|
11194
11261
|
if (outcome.exitReason === "completed" || outcome.exitReason === "blocked" || outcome.exitReason === "stopped") {
|
|
11195
11262
|
await this.board.setDigestCursor(job.agent, job.scope, turn.delivered, part);
|
|
11196
11263
|
await this.renewLeases(turn.actor, turn.held);
|
|
11197
11264
|
if (outcome.status?.needsUserDecision === true) {
|
|
11198
|
-
await this.askUser(turn.actor, job, turn.record.startedAt, outcome.status.summary);
|
|
11265
|
+
await this.askUser(turn.actor, job, turn.record.trigger.channel, turn.record.startedAt, outcome.status.summary);
|
|
11199
11266
|
}
|
|
11200
11267
|
}
|
|
11201
11268
|
await this.board.finishTurn(finished, turn.transcript);
|
|
@@ -11207,7 +11274,7 @@ var TurnHost = class {
|
|
|
11207
11274
|
conversation
|
|
11208
11275
|
});
|
|
11209
11276
|
if (!again) {
|
|
11210
|
-
const where = conversation.kind === "thread" ? `thread ${conversation.id}` : `channel ${conversation.name}`;
|
|
11277
|
+
const where = conversation.kind === "thread" ? `thread ${conversation.id}` : conversation.kind === "channel" ? `channel ${conversation.name}` : `project ${conversation.project}'s channels`;
|
|
11211
11278
|
await this.board.publishSignal({
|
|
11212
11279
|
kind: "stuck_workspace",
|
|
11213
11280
|
key: `stuck_workspace:${job.agent}:${job.scope}:${conversationPart(job) ?? ""}`,
|
|
@@ -11224,12 +11291,18 @@ var TurnHost = class {
|
|
|
11224
11291
|
this.log.warn({ runner: turn.runner, cli: job.cli, error: String(error) }, "could not record a quota");
|
|
11225
11292
|
});
|
|
11226
11293
|
}
|
|
11294
|
+
for (const stuck of outcome.stuck ?? []) {
|
|
11295
|
+
await this.board.postTaskNotice(stuck.branch.slice("task/".length), stuckNotice(stuck)).catch((error) => {
|
|
11296
|
+
this.log.warn({ branch: stuck.branch, agent: stuck.agent, error: String(error) }, "could not tell a task's thread of commits its branch cannot take");
|
|
11297
|
+
});
|
|
11298
|
+
}
|
|
11227
11299
|
if (recorded) {
|
|
11228
11300
|
this.log.info({
|
|
11229
11301
|
agent: job.agent,
|
|
11230
11302
|
project: job.scope,
|
|
11231
11303
|
thread: job.thread,
|
|
11232
11304
|
channel: job.channel,
|
|
11305
|
+
place: job.place,
|
|
11233
11306
|
exitReason: finished.exitReason
|
|
11234
11307
|
}, "turn finished");
|
|
11235
11308
|
await this.takeSkillFiles(finished);
|
|
@@ -11296,6 +11369,7 @@ var TurnHost = class {
|
|
|
11296
11369
|
project: dispatch.project,
|
|
11297
11370
|
...dispatch.thread === void 0 ? {} : { thread: dispatch.thread.id },
|
|
11298
11371
|
...dispatch.thread !== void 0 || dispatch.channel === void 0 ? {} : { channel: dispatch.channel },
|
|
11372
|
+
...dispatch.thread !== void 0 || dispatch.channel !== void 0 || dispatch.project !== SOCIETY_SCOPE || dispatch.place === void 0 ? {} : { place: dispatch.place },
|
|
11299
11373
|
runner,
|
|
11300
11374
|
cli: agent?.cli ?? null,
|
|
11301
11375
|
session: null,
|
|
@@ -11342,24 +11416,37 @@ var TurnHost = class {
|
|
|
11342
11416
|
return token;
|
|
11343
11417
|
}
|
|
11344
11418
|
/**
|
|
11345
|
-
*
|
|
11419
|
+
* The signals a reader's prompt lists: those logged for its scope since its last turn there,
|
|
11420
|
+
* the spend reports summed, then each condition once at its latest log, the newest kept.
|
|
11421
|
+
*/
|
|
11422
|
+
async signalsSince(agent, scope, lastTurn) {
|
|
11423
|
+
const read2 = await this.board.listSignals(SIGNAL_READ + 1);
|
|
11424
|
+
const past = read2.length > SIGNAL_READ ? read2[0] : void 0;
|
|
11425
|
+
const partial = past !== void 0 && (lastTurn === null || past.ts > lastTurn.startedAt);
|
|
11426
|
+
const since = read2.slice(-SIGNAL_READ).filter((record) => wakeScope(agent, record.signal.project ?? null) === scope && (lastTurn === null || record.ts > lastTurn.startedAt));
|
|
11427
|
+
return latestPerKey(sumSpend(since, partial)).slice(-MAX_SIGNALS);
|
|
11428
|
+
}
|
|
11429
|
+
/**
|
|
11430
|
+
* Where a conversation works: a task's on its branch, which its stage's holder has to itself,
|
|
11431
|
+
* any other thread's, a channel's, and a project's channels' seen from the society scope in a
|
|
11346
11432
|
* place of its own, and the home in the citizen's own.
|
|
11347
11433
|
*/
|
|
11348
|
-
async workspaceOf(project, thread, channel) {
|
|
11434
|
+
async workspaceOf(project, thread, channel, place, task) {
|
|
11349
11435
|
const own = thread !== void 0 ? thread.task ? {} : { thread: thread.id } : channel === void 0 ? {} : { channel };
|
|
11350
11436
|
if (project === null) {
|
|
11351
|
-
return { kind: "home", ...own };
|
|
11437
|
+
return { kind: "home", ...own, ...place === void 0 ? {} : { place } };
|
|
11352
11438
|
}
|
|
11353
11439
|
const repo = projectRepo(project);
|
|
11354
11440
|
if (thread?.task === true) {
|
|
11355
|
-
|
|
11441
|
+
const holder = task?.status === "claimed" && task.claimedBy !== void 0 && task.claimedBy !== USER_NAME && (task.leaseExpiresAt === void 0 || new Date(task.leaseExpiresAt).getTime() > this.now().getTime()) ? task.claimedBy : void 0;
|
|
11442
|
+
return { kind: "task", repo, taskId: thread.id, ...holder === void 0 ? {} : { holder } };
|
|
11356
11443
|
}
|
|
11357
|
-
const branches = (await this.board.listTasks(project.slug)).filter((
|
|
11444
|
+
const branches = (await this.board.listTasks(project.slug)).filter((each) => each.status === "open" || each.status === "claimed").map((each) => `task/${each.id}`);
|
|
11358
11445
|
return { kind: "project", repo, branches, ...own };
|
|
11359
11446
|
}
|
|
11360
11447
|
/**
|
|
11361
|
-
* Whether a thread conversation's task or thread has ended,
|
|
11362
|
-
*
|
|
11448
|
+
* Whether a thread conversation's task or thread has ended, a channel conversation's channel was
|
|
11449
|
+
* archived, or a project conversation's project was, which frees its workspace.
|
|
11363
11450
|
*/
|
|
11364
11451
|
async conversationEnded(job) {
|
|
11365
11452
|
const conversation = workspaceConversationOf(job);
|
|
@@ -11373,14 +11460,14 @@ var TurnHost = class {
|
|
|
11373
11460
|
* How a closing turn's conversation ended, and where its citizen asks someone else to take over
|
|
11374
11461
|
* work that is not its own: the thread's channel while it is open, else the place's general.
|
|
11375
11462
|
*/
|
|
11376
|
-
async endingOf(scope, thread, task, channel) {
|
|
11463
|
+
async endingOf(scope, thread, task, channel, project) {
|
|
11377
11464
|
const place = scope === SOCIETY_SCOPE ? null : scope;
|
|
11378
11465
|
if (thread !== void 0) {
|
|
11379
11466
|
const closed = `the thread "${thread.title}" on ${thread.channel} was closed${thread.closedBy === void 0 ? "" : ` by ${thread.closedBy}`}${task === null ? "" : `, its task ${task.status}`}`;
|
|
11380
11467
|
return { how: closed, askIn: await this.replyIn(thread) };
|
|
11381
11468
|
}
|
|
11382
11469
|
if (channel === void 0) {
|
|
11383
|
-
return null;
|
|
11470
|
+
return project === void 0 ? null : { how: `project ${project} was archived`, askIn: channelRef(place, "general") };
|
|
11384
11471
|
}
|
|
11385
11472
|
const ref = channelRef(place, channel);
|
|
11386
11473
|
const archived = (await this.board.listChannels()).find((each) => each.ref === ref)?.archived;
|
|
@@ -11394,13 +11481,15 @@ var TurnHost = class {
|
|
|
11394
11481
|
* the user, which is what puts it before them. One that mentioned the user nowhere asks in its
|
|
11395
11482
|
* own thread when it was in one that is still open, so the answer comes back to the same
|
|
11396
11483
|
* conversation, and otherwise in a thread of its own on its conversation's channel, or its scope's
|
|
11397
|
-
* general for the home, opened as the citizen with its summary
|
|
11398
|
-
*
|
|
11399
|
-
*
|
|
11484
|
+
* general for the home, opened as the citizen with its summary; a project's conversation in the
|
|
11485
|
+
* society scope asks on the channel of the project its wake came from, else the project's general.
|
|
11486
|
+
* One that proposed something the user decides has put its decision before the user already,
|
|
11487
|
+
* where deciding it clears it; a mention would stay until the user posted beside it.
|
|
11400
11488
|
*/
|
|
11401
|
-
async askUser(actor, job, since, summary) {
|
|
11489
|
+
async askUser(actor, job, askedIn, since, summary) {
|
|
11402
11490
|
const threadId = job.thread;
|
|
11403
11491
|
const place = job.scope === SOCIETY_SCOPE ? null : job.scope;
|
|
11492
|
+
const channel = job.place === void 0 ? channelRef(place, job.channel ?? "general") : askedIn !== void 0 && parseChannelRef(askedIn).project === job.place ? askedIn : channelRef(job.place, "general");
|
|
11404
11493
|
try {
|
|
11405
11494
|
if (await this.board.hasMentioned(actor.name, USER_NAME, since)) {
|
|
11406
11495
|
return;
|
|
@@ -11418,7 +11507,7 @@ var TurnHost = class {
|
|
|
11418
11507
|
return;
|
|
11419
11508
|
}
|
|
11420
11509
|
const thread = await this.board.openThread(actor, {
|
|
11421
|
-
channel
|
|
11510
|
+
channel,
|
|
11422
11511
|
title: `${actor.name} asks for a decision: ${summary}`.slice(0, 200)
|
|
11423
11512
|
});
|
|
11424
11513
|
await this.board.postMessage(actor, {
|
|
@@ -11734,7 +11823,8 @@ var RunnerHub = class {
|
|
|
11734
11823
|
async steer(conversation) {
|
|
11735
11824
|
const turn = this.host.turnIn(conversation.agent, conversation.project, {
|
|
11736
11825
|
thread: conversation.thread,
|
|
11737
|
-
channel: conversation.channel
|
|
11826
|
+
channel: conversation.channel,
|
|
11827
|
+
place: conversation.place
|
|
11738
11828
|
});
|
|
11739
11829
|
const seat = turn === null ? void 0 : this.seats.get(turn.runner);
|
|
11740
11830
|
if (turn === null || seat === void 0 || seat.send === null) {
|
|
@@ -11951,11 +12041,11 @@ var RunnerHub = class {
|
|
|
11951
12041
|
* NOT_FOUND when the project has no runner yet or the branch or the path does not exist, and
|
|
11952
12042
|
* `RunnerAwayError` while its runner is away.
|
|
11953
12043
|
*/
|
|
11954
|
-
async readBranch(project, branch,
|
|
12044
|
+
async readBranch(project, branch, path11) {
|
|
11955
12045
|
const { seat, repo } = await this.projectSeat(project, "branchReads", "read files");
|
|
11956
|
-
const file = BranchFileSchema.nullable().parse(await this.request(seat, (request) => ({ type: "file", request, read: { repo, branch, path:
|
|
12046
|
+
const file = BranchFileSchema.nullable().parse(await this.request(seat, (request) => ({ type: "file", request, read: { repo, branch, path: path11 } }), FILE_TIMEOUT_MS));
|
|
11957
12047
|
if (file === null) {
|
|
11958
|
-
throw new BoardError("NOT_FOUND", `${branch} has no ${
|
|
12048
|
+
throw new BoardError("NOT_FOUND", `${branch} has no ${path11 === "" ? "commit" : path11}`);
|
|
11959
12049
|
}
|
|
11960
12050
|
return file;
|
|
11961
12051
|
}
|
|
@@ -11975,11 +12065,11 @@ var RunnerHub = class {
|
|
|
11975
12065
|
* What a branch of a project's repository did to one file since it left the default branch, read
|
|
11976
12066
|
* on the runner the project lives on, with the same refusals as `branchChanges`.
|
|
11977
12067
|
*/
|
|
11978
|
-
async branchDiff(project, branch,
|
|
12068
|
+
async branchDiff(project, branch, path11) {
|
|
11979
12069
|
const { seat, repo } = await this.projectSeat(project, "branchChanges", "list changes");
|
|
11980
|
-
const diff = BranchFileDiffSchema.nullable().parse(await this.request(seat, (request) => ({ type: "diff", request, read: { repo, branch, path:
|
|
12070
|
+
const diff = BranchFileDiffSchema.nullable().parse(await this.request(seat, (request) => ({ type: "diff", request, read: { repo, branch, path: path11 } }), FILE_TIMEOUT_MS));
|
|
11981
12071
|
if (diff === null) {
|
|
11982
|
-
throw new BoardError("NOT_FOUND", `${branch} did not change ${
|
|
12072
|
+
throw new BoardError("NOT_FOUND", `${branch} did not change ${path11}`);
|
|
11983
12073
|
}
|
|
11984
12074
|
return diff;
|
|
11985
12075
|
}
|
|
@@ -11990,7 +12080,7 @@ var RunnerHub = class {
|
|
|
11990
12080
|
* back as a `BoardError` in the runner's words.
|
|
11991
12081
|
*/
|
|
11992
12082
|
async readTurnImage(actor, file) {
|
|
11993
|
-
const turn = this.host.runningTurns().find((each) => each.agent === actor.name && each.scope === actor.scope && each.thread === actor.thread && each.channel === actor.channel);
|
|
12083
|
+
const turn = this.host.runningTurns().find((each) => each.agent === actor.name && each.scope === actor.scope && each.thread === actor.thread && each.channel === actor.channel && each.place === actor.place);
|
|
11994
12084
|
if (turn === void 0) {
|
|
11995
12085
|
throw new BoardError("INVALID_STATE", `${actor.name} has no turn running in this conversation to read ${file} from`);
|
|
11996
12086
|
}
|
|
@@ -12223,6 +12313,8 @@ import pino from "pino";
|
|
|
12223
12313
|
// packages/board-mcp/dist/index.js
|
|
12224
12314
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12225
12315
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
12316
|
+
var MAX_TOOL_INPUT_ELEMENTS = 1e3;
|
|
12317
|
+
var MAX_REQUEST_BODY_SIZE = 1024 * 1024;
|
|
12226
12318
|
var BOARD_TOOLS = VERB_NAMES.map((name) => ({
|
|
12227
12319
|
name,
|
|
12228
12320
|
description: VERB_DESCRIPTIONS[name],
|
|
@@ -12245,7 +12337,7 @@ function describeError(error) {
|
|
|
12245
12337
|
return String(error);
|
|
12246
12338
|
}
|
|
12247
12339
|
async function createBoardMcpServer(board2, actor, version) {
|
|
12248
|
-
const server2 = new McpServer({ name: "stellaris-board", version });
|
|
12340
|
+
const server2 = new McpServer({ name: "stellaris-board", version }, { maxToolInputElements: MAX_TOOL_INPUT_ELEMENTS });
|
|
12249
12341
|
const charter = await board2.readRole(actor.role);
|
|
12250
12342
|
for (const tool of toolsForRole(charter)) {
|
|
12251
12343
|
server2.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, async (args) => {
|
|
@@ -12266,7 +12358,10 @@ async function createBoardMcpServer(board2, actor, version) {
|
|
|
12266
12358
|
}
|
|
12267
12359
|
async function handleMcpRequest(board2, actor, request, version) {
|
|
12268
12360
|
const server2 = await createBoardMcpServer(board2, actor, version);
|
|
12269
|
-
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
12361
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
12362
|
+
enableJsonResponse: true,
|
|
12363
|
+
maxRequestBodySize: MAX_REQUEST_BODY_SIZE
|
|
12364
|
+
});
|
|
12270
12365
|
await server2.connect(transport);
|
|
12271
12366
|
try {
|
|
12272
12367
|
return await transport.handleRequest(request, {
|
|
@@ -12283,6 +12378,25 @@ import { compress } from "hono/compress";
|
|
|
12283
12378
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
12284
12379
|
import { z as z19, ZodError } from "zod";
|
|
12285
12380
|
|
|
12381
|
+
// apps/server/dist/body-limit.js
|
|
12382
|
+
import { bodyLimit } from "hono/body-limit";
|
|
12383
|
+
var ENROLL_BODY_LIMIT_BYTES = 16 * 1024;
|
|
12384
|
+
var API_BODY_LIMIT_BYTES = 1024 * 1024;
|
|
12385
|
+
var RUNNER_BODY_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
12386
|
+
var RUNNER_ANSWER_LIMIT_BYTES = 16 * 1024 * 1024;
|
|
12387
|
+
function limitBody(maxSize) {
|
|
12388
|
+
return bodyLimit({
|
|
12389
|
+
maxSize,
|
|
12390
|
+
onError: (c) => {
|
|
12391
|
+
c.header("connection", "close");
|
|
12392
|
+
return c.json({
|
|
12393
|
+
error: "PAYLOAD_TOO_LARGE",
|
|
12394
|
+
message: `the request body is over the ${maxSize} bytes this route takes`
|
|
12395
|
+
}, 413);
|
|
12396
|
+
}
|
|
12397
|
+
});
|
|
12398
|
+
}
|
|
12399
|
+
|
|
12286
12400
|
// apps/server/dist/github-auth.js
|
|
12287
12401
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
12288
12402
|
import { Hono } from "hono";
|
|
@@ -12490,7 +12604,7 @@ function rateLimit(options) {
|
|
|
12490
12604
|
}
|
|
12491
12605
|
|
|
12492
12606
|
// apps/server/dist/runner-routes.js
|
|
12493
|
-
import
|
|
12607
|
+
import path8 from "node:path";
|
|
12494
12608
|
import { Hono as Hono2 } from "hono";
|
|
12495
12609
|
import { streamSSE } from "hono/streaming";
|
|
12496
12610
|
|
|
@@ -12590,8 +12704,9 @@ function bearer(c) {
|
|
|
12590
12704
|
function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
12591
12705
|
const routes = new Hono2();
|
|
12592
12706
|
if (enrollments !== void 0) {
|
|
12593
|
-
|
|
12594
|
-
routes.post("/enroll
|
|
12707
|
+
const enrollBody = limitBody(ENROLL_BODY_LIMIT_BYTES);
|
|
12708
|
+
routes.post("/enroll", enrollLimit ?? ((_c, next) => next()), enrollBody, async (c) => c.json(enrollments.open(RunnerEnrollRequestSchema.parse(await c.req.json()))));
|
|
12709
|
+
routes.post("/enroll/poll", enrollBody, async (c) => {
|
|
12595
12710
|
const { deviceCode } = RunnerEnrollPollSchema.parse(await c.req.json());
|
|
12596
12711
|
const status = enrollments.poll(deviceCode);
|
|
12597
12712
|
return status === null ? c.json({ error: "NOT_FOUND", message: "no such enrollment; it may have expired" }, 404) : c.json(status);
|
|
@@ -12606,7 +12721,8 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
12606
12721
|
c.set("runner", runner);
|
|
12607
12722
|
await next();
|
|
12608
12723
|
});
|
|
12609
|
-
|
|
12724
|
+
const body = limitBody(RUNNER_BODY_LIMIT_BYTES);
|
|
12725
|
+
routes.post("/hello", body, async (c) => c.json(await hub.register(c.get("runner"), RunnerHelloSchema.parse(await c.req.json()))));
|
|
12610
12726
|
routes.get("/stream", (c) => {
|
|
12611
12727
|
const runner = c.get("runner");
|
|
12612
12728
|
return streamSSE(c, async (stream) => {
|
|
@@ -12644,7 +12760,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
12644
12760
|
}
|
|
12645
12761
|
});
|
|
12646
12762
|
});
|
|
12647
|
-
routes.post("/turns/:id/events", async (c) => {
|
|
12763
|
+
routes.post("/turns/:id/events", body, async (c) => {
|
|
12648
12764
|
const { entries } = TurnEventsSchema.parse(await c.req.json());
|
|
12649
12765
|
await hub.turnEvents(c.get("runner"), UlidSchema.parse(c.req.param("id")), entries);
|
|
12650
12766
|
return c.json({ ok: true });
|
|
@@ -12653,21 +12769,21 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
12653
12769
|
hub.received(c.get("runner"), UlidSchema.parse(c.req.param("id")));
|
|
12654
12770
|
return c.json({ ok: true });
|
|
12655
12771
|
});
|
|
12656
|
-
routes.post("/turns/:id/outcome", async (c) => c.json(await hub.turnOutcome(c.get("runner"), UlidSchema.parse(c.req.param("id")), TurnOutcomeSchema.parse(await c.req.json()))));
|
|
12657
|
-
routes.post("/requests/:id", async (c) => {
|
|
12772
|
+
routes.post("/turns/:id/outcome", body, async (c) => c.json(await hub.turnOutcome(c.get("runner"), UlidSchema.parse(c.req.param("id")), TurnOutcomeSchema.parse(await c.req.json()))));
|
|
12773
|
+
routes.post("/requests/:id", limitBody(RUNNER_ANSWER_LIMIT_BYTES), async (c) => {
|
|
12658
12774
|
hub.answer(c.get("runner"), c.req.param("id"), RunnerAnswerSchema.parse(await c.req.json()));
|
|
12659
12775
|
return c.json({ ok: true });
|
|
12660
12776
|
});
|
|
12661
|
-
routes.post("/workspaces", async (c) => {
|
|
12777
|
+
routes.post("/workspaces", body, async (c) => {
|
|
12662
12778
|
hub.heldWorkspaces(c.get("runner"), HeldWorkspacesSchema.parse(await c.req.json()).workspaces);
|
|
12663
12779
|
return c.json({ ok: true });
|
|
12664
12780
|
});
|
|
12665
|
-
routes.post("/warm", async (c) => {
|
|
12781
|
+
routes.post("/warm", body, async (c) => {
|
|
12666
12782
|
hub.warmChanged(c.get("runner"), WarmSessionsSchema.parse(await c.req.json()).warm);
|
|
12667
12783
|
return c.json({ ok: true });
|
|
12668
12784
|
});
|
|
12669
12785
|
routes.get("/board/manifest", async (c) => c.json({ files: await board2.boardManifest() }));
|
|
12670
|
-
routes.post("/board/read", async (c) => c.json({ files: await board2.readBoardFiles(FileReadSchema.parse(await c.req.json()).paths) }));
|
|
12786
|
+
routes.post("/board/read", body, async (c) => c.json({ files: await board2.readBoardFiles(FileReadSchema.parse(await c.req.json()).paths) }));
|
|
12671
12787
|
const home = new Hono2();
|
|
12672
12788
|
home.use("*", async (c, next) => {
|
|
12673
12789
|
const agent = NameSchema.parse(c.req.param("agent"));
|
|
@@ -12683,7 +12799,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
12683
12799
|
const marker = `/homes/${agent}/git`;
|
|
12684
12800
|
const at = c.req.path.indexOf(marker);
|
|
12685
12801
|
return homeGit.serve({
|
|
12686
|
-
root:
|
|
12802
|
+
root: path8.dirname(repository),
|
|
12687
12803
|
agent,
|
|
12688
12804
|
rest: at === -1 ? "" : c.req.path.slice(at + marker.length),
|
|
12689
12805
|
request: c.req.raw,
|
|
@@ -12696,7 +12812,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
12696
12812
|
|
|
12697
12813
|
// apps/server/dist/static.js
|
|
12698
12814
|
import { readFile as readFile5, stat as stat4 } from "node:fs/promises";
|
|
12699
|
-
import
|
|
12815
|
+
import path9 from "node:path";
|
|
12700
12816
|
var CONTENT_TYPES = {
|
|
12701
12817
|
".html": "text/html; charset=utf-8",
|
|
12702
12818
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -12724,11 +12840,11 @@ function within2(root, pathname) {
|
|
|
12724
12840
|
} catch {
|
|
12725
12841
|
return null;
|
|
12726
12842
|
}
|
|
12727
|
-
const candidate =
|
|
12728
|
-
return candidate === root || candidate.startsWith(root +
|
|
12843
|
+
const candidate = path9.resolve(root, `.${decoded}`);
|
|
12844
|
+
return candidate === root || candidate.startsWith(root + path9.sep) ? candidate : null;
|
|
12729
12845
|
}
|
|
12730
12846
|
function spaHandler(staticDir) {
|
|
12731
|
-
const root =
|
|
12847
|
+
const root = path9.resolve(staticDir);
|
|
12732
12848
|
return async (c) => {
|
|
12733
12849
|
const { pathname } = new URL(c.req.url);
|
|
12734
12850
|
if (pathname.startsWith("/api/")) {
|
|
@@ -12739,14 +12855,14 @@ function spaHandler(staticDir) {
|
|
|
12739
12855
|
if (file !== null && await isFile(file)) {
|
|
12740
12856
|
return c.body(await readFile5(file), 200, {
|
|
12741
12857
|
...headers,
|
|
12742
|
-
"content-type": CONTENT_TYPES[
|
|
12858
|
+
"content-type": CONTENT_TYPES[path9.extname(file)] ?? "application/octet-stream",
|
|
12743
12859
|
"cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
|
|
12744
12860
|
});
|
|
12745
12861
|
}
|
|
12746
12862
|
if (pathname.startsWith("/assets/")) {
|
|
12747
12863
|
return c.text("not found", 404, headers);
|
|
12748
12864
|
}
|
|
12749
|
-
const index =
|
|
12865
|
+
const index = path9.join(root, "index.html");
|
|
12750
12866
|
if (!await isFile(index)) {
|
|
12751
12867
|
return c.text("The interface is not built: run pnpm build:web.", 404);
|
|
12752
12868
|
}
|
|
@@ -12819,6 +12935,7 @@ function createApp(deps) {
|
|
|
12819
12935
|
};
|
|
12820
12936
|
const api = new Hono3();
|
|
12821
12937
|
api.use("*", authenticate);
|
|
12938
|
+
api.on(["POST", "PUT", "PATCH", "DELETE"], "*", limitBody(API_BODY_LIMIT_BYTES));
|
|
12822
12939
|
api.get("/me", (c) => {
|
|
12823
12940
|
const signIn = board2.signInOf(bearer2(c) ?? "");
|
|
12824
12941
|
return c.json(signIn === null ? c.get("actor") : {
|
|
@@ -12912,6 +13029,7 @@ function createApp(deps) {
|
|
|
12912
13029
|
});
|
|
12913
13030
|
api.get("/society/knowledge", async (c) => c.json(await board2.listKnowledge(null)));
|
|
12914
13031
|
api.get("/skills", async (c) => c.json(await board2.listSocietySkills()));
|
|
13032
|
+
api.get("/skills/:name/files", async (c) => c.json(await board2.readSocietySkillFiles(NameSchema.parse(c.req.param("name")))));
|
|
12915
13033
|
api.post("/agents/:name/retire", async (c) => {
|
|
12916
13034
|
const body = RetireBodySchema.parse(await c.req.json());
|
|
12917
13035
|
const { tokenHash: _hash, ...agent } = await board2.retireAgent(c.get("actor"), {
|
|
@@ -12967,14 +13085,23 @@ function createApp(deps) {
|
|
|
12967
13085
|
api.get("/projects/:slug/tasks", async (c) => c.json(await board2.listTasks(c.req.param("slug"))));
|
|
12968
13086
|
api.get("/projects/:slug/dashboard", async (c) => c.json(await board2.readDashboard(c.req.param("slug"))));
|
|
12969
13087
|
api.get("/projects/:slug/knowledge", async (c) => c.json(await board2.listKnowledge(c.req.param("slug"))));
|
|
13088
|
+
const branchFile = async (c, project, branch, file) => {
|
|
13089
|
+
if (deps.runners === void 0) {
|
|
13090
|
+
throw new BoardError("NOT_FOUND", "this board has no runners to read files from");
|
|
13091
|
+
}
|
|
13092
|
+
return c.json(await deps.runners.readBranch(project, branch, BranchPathSchema.parse(file)));
|
|
13093
|
+
};
|
|
13094
|
+
const projectFile = async (c, file) => {
|
|
13095
|
+
const project = await board2.readProject(NameSchema.parse(c.req.param("slug")));
|
|
13096
|
+
return branchFile(c, project.slug, project.defaultBranch, file);
|
|
13097
|
+
};
|
|
13098
|
+
api.get("/projects/:slug/files", (c) => projectFile(c, ""));
|
|
13099
|
+
api.get("/projects/:slug/files/:path{.+}", (c) => projectFile(c, c.req.param("path")));
|
|
12970
13100
|
api.get("/tasks/:id", async (c) => c.json((await board2.findTask(c.req.param("id"))).task));
|
|
12971
13101
|
api.get("/tasks/:id/thread", async (c) => c.json(await board2.listThread(c.req.param("id"))));
|
|
12972
13102
|
const taskFile = async (c, file) => {
|
|
12973
13103
|
const { task } = await board2.findTask(UlidSchema.parse(c.req.param("id")));
|
|
12974
|
-
|
|
12975
|
-
throw new BoardError("NOT_FOUND", "this board has no runners to read a task's files from");
|
|
12976
|
-
}
|
|
12977
|
-
return c.json(await deps.runners.readBranch(task.project, `task/${task.id}`, BranchPathSchema.parse(file)));
|
|
13104
|
+
return branchFile(c, task.project, `task/${task.id}`, file);
|
|
12978
13105
|
};
|
|
12979
13106
|
api.get("/tasks/:id/files", (c) => taskFile(c, ""));
|
|
12980
13107
|
api.get("/tasks/:id/changes", async (c) => {
|
|
@@ -13355,7 +13482,10 @@ var TurnHub = class {
|
|
|
13355
13482
|
get lastSeq() {
|
|
13356
13483
|
return this.seq;
|
|
13357
13484
|
}
|
|
13358
|
-
/**
|
|
13485
|
+
/**
|
|
13486
|
+
* Records one event of a turn, in its scope and its conversation there: a thread's, a channel's,
|
|
13487
|
+
* a project's channels' in the society scope, or home.
|
|
13488
|
+
*/
|
|
13359
13489
|
push(agent, project, event, conversation = {}) {
|
|
13360
13490
|
this.seq += 1;
|
|
13361
13491
|
const item = {
|
|
@@ -13365,6 +13495,7 @@ var TurnHub = class {
|
|
|
13365
13495
|
project,
|
|
13366
13496
|
...conversation.thread === void 0 ? {} : { thread: conversation.thread },
|
|
13367
13497
|
...conversation.channel === void 0 ? {} : { channel: conversation.channel },
|
|
13498
|
+
...conversation.place === void 0 ? {} : { place: conversation.place },
|
|
13368
13499
|
event
|
|
13369
13500
|
};
|
|
13370
13501
|
this.buffer.push(item);
|
|
@@ -13441,8 +13572,8 @@ var view = {
|
|
|
13441
13572
|
return scheduler.activeSignals;
|
|
13442
13573
|
}
|
|
13443
13574
|
};
|
|
13444
|
-
var webDir = process.env["STELLARIS_WEB_DIR"] ??
|
|
13445
|
-
var webBuilt = await access2(
|
|
13575
|
+
var webDir = process.env["STELLARIS_WEB_DIR"] ?? path10.resolve(import.meta.dirname, "../../web/dist");
|
|
13576
|
+
var webBuilt = await access2(path10.join(webDir, "index.html")).then(() => true, () => false);
|
|
13446
13577
|
var app = createApp({
|
|
13447
13578
|
board,
|
|
13448
13579
|
version: VERSION,
|