@kubitnodes/stellaris 0.3.18 → 0.3.20
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 +602 -196
- package/dist/server/index.js +606 -156
- package/package.json +1 -1
- package/web/dist/assets/{BackLink-DUOMS3fv.js → BackLink-BjfX2KZl.js} +1 -1
- package/web/dist/assets/{ChannelForms-DfG4vYvk.js → ChannelForms-BYrcUldk.js} +1 -1
- package/web/dist/assets/ChannelView-GJ_DusLQ.js +2 -0
- package/web/dist/assets/CitizenView-Cx7IYSRr.js +2 -0
- package/web/dist/assets/{CitizensView-bKbUaCab.js → CitizensView-IKH7_yNQ.js} +1 -1
- package/web/dist/assets/{CliIcon-DTVk0pGU.js → CliIcon-vSuwLv2K.js} +1 -1
- package/web/dist/assets/CronsView-DJbi8Qhx.js +1 -0
- package/web/dist/assets/{KnowledgeView-Dt7wE3FT.js → KnowledgeView-CoYOG9tI.js} +1 -1
- package/web/dist/assets/{Markdown-DeF9PKNm.js → Markdown-YC-fbHiE.js} +4 -4
- package/web/dist/assets/MessageItem-D7MUCA_T.js +1 -0
- package/web/dist/assets/{MetricsView-Bw7nL6Y9.js → MetricsView-CE94VW0I.js} +1 -1
- package/web/dist/assets/{NeedsYouView-DbESNtQT.js → NeedsYouView-Otnm3lcG.js} +1 -1
- package/web/dist/assets/{Overview-Dv7SS4yW.js → Overview-B8f6rLb2.js} +1 -1
- package/web/dist/assets/{Pane-v044FaP5.js → Pane-_D7n5uKH.js} +1 -1
- package/web/dist/assets/{Picker-wWsO86nu.js → Picker-DUiiQMFN.js} +1 -1
- package/web/dist/assets/ProjectView-D_ACOmEd.js +1 -0
- package/web/dist/assets/ProposalView-4d0U3nEb.js +2 -0
- package/web/dist/assets/{ProposalsView-dVA50yG1.js → ProposalsView-BovF6CMD.js} +1 -1
- package/web/dist/assets/{RunnersView-C-l5QYIa.js → RunnersView-B2_uCVHC.js} +1 -1
- package/web/dist/assets/{SocietyView--mWRXRj_.js → SocietyView-Bf0iX8AY.js} +1 -1
- package/web/dist/assets/SubjectThread-B_Vmgs3d.js +1 -0
- package/web/dist/assets/TaskFileView-BzO0gjE6.js +4 -0
- package/web/dist/assets/{TaskView-BgJcOmGj.js → TaskView-DB_Nji99.js} +2 -2
- package/web/dist/assets/{TasksView-f8KIsif_.js → TasksView-Bz36YYWq.js} +1 -1
- package/web/dist/assets/{ThreadForms-CXsunS-N.js → ThreadForms-1mbMZsbv.js} +1 -1
- package/web/dist/assets/ThreadItem-QJSA92C4.js +1 -0
- package/web/dist/assets/ThreadView-BECLqt5B.js +1 -0
- package/web/dist/assets/{citizen-VtLNxjtf.js → citizen-DVFr5T6R.js} +1 -1
- package/web/dist/assets/{compose-D-SO-HE-.js → compose-_6feqP1t.js} +1 -1
- package/web/dist/assets/{format-CkySAdH1.js → format-DxXtbdNM.js} +1 -1
- package/web/dist/assets/governance-C9xH4QTm.js +1 -0
- package/web/dist/assets/index-BEVCPm4C.js +23 -0
- package/web/dist/assets/index-BkOq4PSU.css +2 -0
- package/web/dist/assets/link-CTjFMNoY.js +1 -0
- package/web/dist/assets/{tasks-CD70eybR.js → tasks-B6I9DJ8O.js} +1 -1
- package/web/dist/assets/useMatch-B3RzAe8y.js +1 -0
- package/web/dist/assets/useStickyScroll-Ca7sWbGH.js +1 -0
- package/web/dist/index.html +9 -9
- package/web/dist/assets/ChannelView-DTLhLXy4.js +0 -2
- package/web/dist/assets/CitizenView-q24CExsI.js +0 -2
- package/web/dist/assets/CronsView-DaG1xhQu.js +0 -1
- package/web/dist/assets/MessageItem-DDs7m3-4.js +0 -1
- package/web/dist/assets/ProjectView--nzlqSDA.js +0 -1
- package/web/dist/assets/ProposalView-Cdw_xzG1.js +0 -1
- package/web/dist/assets/SubjectThread-DvvsxHN9.js +0 -1
- package/web/dist/assets/TaskFileView-DYuPrCdk.js +0 -4
- package/web/dist/assets/ThreadCard-BRLvjdz3.js +0 -1
- package/web/dist/assets/ThreadView-DzpaXkik.js +0 -1
- package/web/dist/assets/governance-1UeTD2K_.js +0 -1
- package/web/dist/assets/index-CwkK8XtJ.js +0 -23
- package/web/dist/assets/index-DaES0xVj.css +0 -2
- package/web/dist/assets/link-BN04dcBX.js +0 -1
- package/web/dist/assets/useMatch-i33QT_GI.js +0 -1
- package/web/dist/assets/useStickyScroll-WSVpAMrA.js +0 -1
package/dist/server/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import path9 from "node:path";
|
|
|
6
6
|
import { serve } from "@hono/node-server";
|
|
7
7
|
|
|
8
8
|
// packages/board-core/dist/board.js
|
|
9
|
-
import { mkdir as mkdir4, readdir as readdir3, readFile as
|
|
9
|
+
import { chmod as chmod2, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rename as rename3, rm as rm2, stat as stat3, writeFile as writeFile4 } from "node:fs/promises";
|
|
10
10
|
import path6 from "node:path";
|
|
11
11
|
import { decodeTime, monotonicFactory } from "ulid";
|
|
12
12
|
import { z as z13 } from "zod";
|
|
@@ -437,6 +437,14 @@ var MessageFrontmatterSchema = z4.object({
|
|
|
437
437
|
ts: IsoDateTimeSchema,
|
|
438
438
|
mentions: z4.array(NameSchema)
|
|
439
439
|
});
|
|
440
|
+
var MessageRefSchema = MessageFrontmatterSchema.pick({
|
|
441
|
+
id: true,
|
|
442
|
+
author: true,
|
|
443
|
+
channel: true,
|
|
444
|
+
thread: true,
|
|
445
|
+
ts: true
|
|
446
|
+
});
|
|
447
|
+
var MESSAGE_LOOKUP_LIMIT = 50;
|
|
440
448
|
var TaskStatusSchema = z4.enum(["open", "claimed", "done", "abandoned"]);
|
|
441
449
|
var ThreadStateSchema = z4.enum(["open", "closed"]);
|
|
442
450
|
var ThreadSubjectSchema = z4.object({
|
|
@@ -637,7 +645,7 @@ var ArchiveProposalSchema = z4.object({
|
|
|
637
645
|
var SkillProposalSchema = z4.object({
|
|
638
646
|
name: NameSchema,
|
|
639
647
|
summary: z4.string().min(1).max(200),
|
|
640
|
-
body: z4.string().min(1)
|
|
648
|
+
body: z4.string().min(1).optional()
|
|
641
649
|
});
|
|
642
650
|
var ProposalCharterSchemas = {
|
|
643
651
|
role: RoleCharterSchema,
|
|
@@ -764,6 +772,7 @@ var HomeFileDiffSchema = HomeFileChangeSchema.pick({ path: true, status: true })
|
|
|
764
772
|
var KnowledgeSchema = z4.object({
|
|
765
773
|
topic: NameSchema,
|
|
766
774
|
project: NameSchema.nullable(),
|
|
775
|
+
revision: z4.number().int().min(0),
|
|
767
776
|
updatedBy: NameSchema,
|
|
768
777
|
updatedAt: IsoDateTimeSchema
|
|
769
778
|
});
|
|
@@ -786,6 +795,24 @@ var ProposalOriginSchema = z4.object({
|
|
|
786
795
|
thread: UlidSchema.optional(),
|
|
787
796
|
channel: NameSchema.optional()
|
|
788
797
|
});
|
|
798
|
+
var SKILL_FILES_LIMIT = 100;
|
|
799
|
+
var SKILL_BYTES_LIMIT = 10 * 1024 * 1024;
|
|
800
|
+
var SkillFileSchema = z4.object({
|
|
801
|
+
path: z4.string().min(1),
|
|
802
|
+
size: z4.number().int().nonnegative(),
|
|
803
|
+
executable: z4.boolean()
|
|
804
|
+
});
|
|
805
|
+
var SkillFilesSchema = z4.object({
|
|
806
|
+
/** The commit of the proposer's home the files came from. */
|
|
807
|
+
commit: CommitIdSchema,
|
|
808
|
+
takenAt: IsoDateTimeSchema,
|
|
809
|
+
files: z4.array(SkillFileSchema)
|
|
810
|
+
});
|
|
811
|
+
var SkillFileContentSchema = SkillFileSchema.extend({
|
|
812
|
+
text: z4.string().nullable(),
|
|
813
|
+
/** The text was cut at its size limit. */
|
|
814
|
+
truncated: z4.boolean()
|
|
815
|
+
});
|
|
789
816
|
var ProposalFrontmatterSchema = z4.object({
|
|
790
817
|
id: UlidSchema,
|
|
791
818
|
kind: ProposalKindSchema,
|
|
@@ -794,6 +821,8 @@ var ProposalFrontmatterSchema = z4.object({
|
|
|
794
821
|
createdAt: IsoDateTimeSchema,
|
|
795
822
|
/** Absent for a proposal filed outside a turn, or before proposals recorded it. */
|
|
796
823
|
filedFrom: ProposalOriginSchema.optional(),
|
|
824
|
+
/** A skill proposal's folder, once taken; absent for one proposed as text alone. */
|
|
825
|
+
skillFiles: SkillFilesSchema.optional(),
|
|
797
826
|
decidedBy: NameSchema.optional(),
|
|
798
827
|
decidedAt: IsoDateTimeSchema.optional(),
|
|
799
828
|
reason: z4.string().optional(),
|
|
@@ -896,7 +925,9 @@ var VerbInputs = {
|
|
|
896
925
|
/** A project you belong to, or null for society knowledge, which the steward and the user curate. */
|
|
897
926
|
project: NameSchema.nullable().default(null),
|
|
898
927
|
topic: NameSchema,
|
|
899
|
-
body: z5.string().min(1)
|
|
928
|
+
body: z5.string().min(1),
|
|
929
|
+
/** The `revision` in the frontmatter of the topic you read, 0 when it names none or for a new topic. */
|
|
930
|
+
revision: z5.number().int().min(0)
|
|
900
931
|
}),
|
|
901
932
|
remove_knowledge: z5.object({
|
|
902
933
|
/** As for write_knowledge: a project you belong to, or null for society knowledge. */
|
|
@@ -986,7 +1017,7 @@ var VERB_DESCRIPTIONS = {
|
|
|
986
1017
|
create_project: "Create a project with its general channel: a slug, a display name, a git remote when one exists, and optionally a completion effect (none, merge, or ghpr for a GitHub remote).",
|
|
987
1018
|
join_project: "Join a project, or add another citizen to one when your role allows it. Membership gives the pair a worktree and an onboarding turn, and follows the project's general; the result lists its other open channels, to subscribe to when the work needs them.",
|
|
988
1019
|
leave_project: "Leave a project, or remove another citizen from one when your role allows it.",
|
|
989
|
-
write_knowledge: "Write or replace a knowledge topic: durable facts every member of a project should know, or with project null the society's shared knowledge (steward and user). Not a message; use post_message for those.",
|
|
1020
|
+
write_knowledge: "Write or replace a knowledge topic: durable facts every member of a project should know, or with project null the society's shared knowledge (steward and user). Not a message; use post_message for those. Pass the revision from the frontmatter of the topic you read, or 0 for a new topic; if it was written since, the call is refused with the current text to merge yours into.",
|
|
990
1021
|
remove_knowledge: "Remove a knowledge topic that is wrong, superseded, or folded into another, so no turn or search finds it again. Whoever may write the topic may remove it; the board keeps its text aside for the user to restore.",
|
|
991
1022
|
plan_task: "Reshape a task's plan from the current stage onward, or after it while someone holds it: keep a stage by passing its id, drop it by leaving it out, add one without an id. Gated stages and on_done: user, steward, concierge.",
|
|
992
1023
|
advance_task: "Finish the stage you hold, with a note for the task's thread on what you did. The next stage becomes current; past the last one the task is done.",
|
|
@@ -1030,6 +1061,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
1030
1061
|
"proposal.created",
|
|
1031
1062
|
"proposal.decided",
|
|
1032
1063
|
"proposal.provisioned",
|
|
1064
|
+
"proposal.files_taken",
|
|
1033
1065
|
"channel.added",
|
|
1034
1066
|
"channel.archived",
|
|
1035
1067
|
"role.added",
|
|
@@ -1458,6 +1490,9 @@ var RunnerAnswerSchema = z7.discriminatedUnion("ok", [
|
|
|
1458
1490
|
]);
|
|
1459
1491
|
var WarmSessionsSchema = z7.object({ warm: z7.array(z7.string()) });
|
|
1460
1492
|
var FileManifestSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
|
|
1493
|
+
function fileVersion(sha, executable) {
|
|
1494
|
+
return executable ? `${sha}+x` : sha;
|
|
1495
|
+
}
|
|
1461
1496
|
var FileReadSchema = z7.object({ paths: z7.array(z7.string()) });
|
|
1462
1497
|
var FileContentsSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
|
|
1463
1498
|
function isSafeRelativePath(file) {
|
|
@@ -1834,7 +1869,7 @@ var RunnerConfigSchema = z11.object({
|
|
|
1834
1869
|
});
|
|
1835
1870
|
|
|
1836
1871
|
// packages/shared/dist/version.js
|
|
1837
|
-
var STELLARIS_VERSION = "0.3.
|
|
1872
|
+
var STELLARIS_VERSION = "0.3.20";
|
|
1838
1873
|
|
|
1839
1874
|
// packages/board-core/dist/errors.js
|
|
1840
1875
|
var BoardError = class extends Error {
|
|
@@ -1850,7 +1885,7 @@ function isBoardError(error, code) {
|
|
|
1850
1885
|
}
|
|
1851
1886
|
|
|
1852
1887
|
// packages/board-core/dist/events.js
|
|
1853
|
-
import { appendFile,
|
|
1888
|
+
import { appendFile, open, stat } from "node:fs/promises";
|
|
1854
1889
|
import path2 from "node:path";
|
|
1855
1890
|
|
|
1856
1891
|
// packages/board-core/dist/fs.js
|
|
@@ -1891,7 +1926,9 @@ async function readMarkdown(file, schema) {
|
|
|
1891
1926
|
return { data: schema.parse(parsed.data), body: parsed.content.replace(/^\n/, "") };
|
|
1892
1927
|
}
|
|
1893
1928
|
async function readLooseMarkdown(file) {
|
|
1894
|
-
|
|
1929
|
+
return parseLooseMarkdown(await readFile(file, "utf8"));
|
|
1930
|
+
}
|
|
1931
|
+
function parseLooseMarkdown(raw) {
|
|
1895
1932
|
try {
|
|
1896
1933
|
const parsed = parseMatter(raw);
|
|
1897
1934
|
return { data: parsed.data, body: parsed.content.replace(/^\n/, ""), error: null };
|
|
@@ -1933,10 +1970,19 @@ async function listDirs(dir) {
|
|
|
1933
1970
|
|
|
1934
1971
|
// packages/board-core/dist/events.js
|
|
1935
1972
|
var LEADING_ID = /^\{"id":"([0-9A-HJKMNP-TV-Z]{26})"/;
|
|
1973
|
+
var ID_PREFIX_BYTES = 64;
|
|
1974
|
+
var NEWLINE = 10;
|
|
1936
1975
|
var EventLog = class {
|
|
1937
1976
|
file;
|
|
1938
1977
|
newId;
|
|
1939
1978
|
now;
|
|
1979
|
+
lines = [];
|
|
1980
|
+
/** Bytes the index covers: always the end of a complete line. */
|
|
1981
|
+
indexed = 0;
|
|
1982
|
+
/** The indexed file, to tell a replaced log from a grown one. */
|
|
1983
|
+
inode = null;
|
|
1984
|
+
scanning = Promise.resolve();
|
|
1985
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1940
1986
|
constructor(file, newId, now) {
|
|
1941
1987
|
this.file = file;
|
|
1942
1988
|
this.newId = newId;
|
|
@@ -1953,48 +1999,97 @@ var EventLog = class {
|
|
|
1953
1999
|
await ensureDir(path2.dirname(this.file));
|
|
1954
2000
|
await appendFile(this.file, `${JSON.stringify(event)}
|
|
1955
2001
|
`, "utf8");
|
|
2002
|
+
for (const listener of this.listeners) {
|
|
2003
|
+
try {
|
|
2004
|
+
listener(event);
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
1956
2008
|
return event;
|
|
1957
2009
|
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Calls `listener` after each event this process appends, until the returned function is
|
|
2012
|
+
* called. Another process's appends, such as the admin CLI's, are seen only by the next read.
|
|
2013
|
+
*/
|
|
2014
|
+
onAppend(listener) {
|
|
2015
|
+
this.listeners.add(listener);
|
|
2016
|
+
return () => {
|
|
2017
|
+
this.listeners.delete(listener);
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
1958
2020
|
/** Events with an id greater than `since`, oldest first. `null` reads from the beginning. */
|
|
1959
2021
|
async readSince(since, limit2 = 1e3) {
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
const events = [];
|
|
1965
|
-
for (const line of raw.split("\n")) {
|
|
1966
|
-
if (line.trim().length === 0) {
|
|
2022
|
+
await this.refresh();
|
|
2023
|
+
const chosen = [];
|
|
2024
|
+
for (const line of this.lines) {
|
|
2025
|
+
if (since !== null && line.id <= since) {
|
|
1967
2026
|
continue;
|
|
1968
2027
|
}
|
|
1969
|
-
|
|
1970
|
-
if (
|
|
1971
|
-
continue;
|
|
1972
|
-
}
|
|
1973
|
-
const event = BoardEventSchema.parse(JSON.parse(line));
|
|
1974
|
-
if (since !== null && event.id <= since) {
|
|
1975
|
-
continue;
|
|
1976
|
-
}
|
|
1977
|
-
events.push(event);
|
|
1978
|
-
if (events.length >= limit2) {
|
|
2028
|
+
chosen.push(line);
|
|
2029
|
+
if (chosen.length >= limit2) {
|
|
1979
2030
|
break;
|
|
1980
2031
|
}
|
|
1981
2032
|
}
|
|
1982
|
-
|
|
2033
|
+
const first = chosen[0];
|
|
2034
|
+
const last = chosen.at(-1);
|
|
2035
|
+
if (first === void 0 || last === void 0) {
|
|
2036
|
+
return [];
|
|
2037
|
+
}
|
|
2038
|
+
const span = await this.readBytes(first.start, last.end);
|
|
2039
|
+
return chosen.map((line) => BoardEventSchema.parse(JSON.parse(span.toString("utf8", line.start - first.start, line.end - first.start))));
|
|
1983
2040
|
}
|
|
1984
2041
|
/** The id of the newest event, or null for an empty log. */
|
|
1985
2042
|
async lastId() {
|
|
1986
|
-
|
|
1987
|
-
|
|
2043
|
+
await this.refresh();
|
|
2044
|
+
return this.lines.at(-1)?.id ?? null;
|
|
2045
|
+
}
|
|
2046
|
+
/** Brings the index up to the file's end, one scan at a time. */
|
|
2047
|
+
refresh() {
|
|
2048
|
+
this.scanning = this.scanning.then(() => this.scan(), () => this.scan());
|
|
2049
|
+
return this.scanning;
|
|
2050
|
+
}
|
|
2051
|
+
async scan() {
|
|
2052
|
+
const info = await stat(this.file).catch((error) => {
|
|
2053
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
2054
|
+
return null;
|
|
2055
|
+
}
|
|
2056
|
+
throw error;
|
|
2057
|
+
});
|
|
2058
|
+
if (info === null || info.ino !== this.inode || info.size < this.indexed) {
|
|
2059
|
+
this.lines = [];
|
|
2060
|
+
this.indexed = 0;
|
|
2061
|
+
this.inode = info?.ino ?? null;
|
|
2062
|
+
}
|
|
2063
|
+
if (info === null || info.size === this.indexed) {
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
const added = await this.readBytes(this.indexed, info.size);
|
|
2067
|
+
let start = 0;
|
|
2068
|
+
for (let at = added.indexOf(NEWLINE); at >= 0; at = added.indexOf(NEWLINE, start)) {
|
|
2069
|
+
const prefix = added.toString("utf8", start, Math.min(at, start + ID_PREFIX_BYTES));
|
|
2070
|
+
if (prefix.trim().length > 0) {
|
|
2071
|
+
const id = LEADING_ID.exec(prefix)?.[1] ?? BoardEventSchema.parse(JSON.parse(added.toString("utf8", start, at))).id;
|
|
2072
|
+
this.lines.push({ id, start: this.indexed + start, end: this.indexed + at });
|
|
2073
|
+
}
|
|
2074
|
+
start = at + 1;
|
|
2075
|
+
}
|
|
2076
|
+
this.indexed += start;
|
|
2077
|
+
}
|
|
2078
|
+
async readBytes(from, to) {
|
|
2079
|
+
const handle = await open(this.file, "r");
|
|
2080
|
+
try {
|
|
2081
|
+
const buffer = Buffer.alloc(to - from);
|
|
2082
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, from);
|
|
2083
|
+
return buffer.subarray(0, bytesRead);
|
|
2084
|
+
} finally {
|
|
2085
|
+
await handle.close();
|
|
1988
2086
|
}
|
|
1989
|
-
const lines = (await readFile2(this.file, "utf8")).trimEnd().split("\n");
|
|
1990
|
-
const last = lines.at(-1) ?? "";
|
|
1991
|
-
return last.length === 0 ? null : BoardEventSchema.parse(JSON.parse(last)).id;
|
|
1992
2087
|
}
|
|
1993
2088
|
};
|
|
1994
2089
|
|
|
1995
2090
|
// packages/board-core/dist/files.js
|
|
1996
2091
|
import { createHash } from "node:crypto";
|
|
1997
|
-
import { mkdir as mkdir2, readdir as readdir2, readFile as
|
|
2092
|
+
import { mkdir as mkdir2, readdir as readdir2, readFile as readFile2, rename as rename2, rm, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1998
2093
|
import path3 from "node:path";
|
|
1999
2094
|
var MAX_FILE_BYTES = 8 * 1024 * 1024;
|
|
2000
2095
|
function isMissing2(error) {
|
|
@@ -2016,9 +2111,9 @@ var FileTree = class {
|
|
|
2016
2111
|
}
|
|
2017
2112
|
try {
|
|
2018
2113
|
const absolute = path3.join(root, ...relative.split("/"));
|
|
2019
|
-
const info = await
|
|
2114
|
+
const info = await stat2(absolute);
|
|
2020
2115
|
if (info.isFile() && info.size <= MAX_FILE_BYTES) {
|
|
2021
|
-
files[relative] = (await
|
|
2116
|
+
files[relative] = (await readFile2(absolute)).toString("base64");
|
|
2022
2117
|
}
|
|
2023
2118
|
} catch (error) {
|
|
2024
2119
|
if (!isMissing2(error)) {
|
|
@@ -2074,17 +2169,18 @@ var FileTree = class {
|
|
|
2074
2169
|
}
|
|
2075
2170
|
async hash(absolute) {
|
|
2076
2171
|
try {
|
|
2077
|
-
const info = await
|
|
2172
|
+
const info = await stat2(absolute);
|
|
2078
2173
|
if (info.size > MAX_FILE_BYTES) {
|
|
2079
2174
|
return null;
|
|
2080
2175
|
}
|
|
2176
|
+
const executable = (info.mode & 73) !== 0;
|
|
2081
2177
|
const known = this.hashes.get(absolute);
|
|
2082
2178
|
if (known !== void 0 && known.mtimeMs === info.mtimeMs && known.size === info.size) {
|
|
2083
|
-
return known.sha;
|
|
2179
|
+
return fileVersion(known.sha, executable);
|
|
2084
2180
|
}
|
|
2085
|
-
const sha = createHash("sha256").update(await
|
|
2181
|
+
const sha = createHash("sha256").update(await readFile2(absolute)).digest("hex");
|
|
2086
2182
|
this.hashes.set(absolute, { mtimeMs: info.mtimeMs, size: info.size, sha });
|
|
2087
|
-
return sha;
|
|
2183
|
+
return fileVersion(sha, executable);
|
|
2088
2184
|
} catch (error) {
|
|
2089
2185
|
if (isMissing2(error)) {
|
|
2090
2186
|
return null;
|
|
@@ -2095,7 +2191,7 @@ var FileTree = class {
|
|
|
2095
2191
|
};
|
|
2096
2192
|
|
|
2097
2193
|
// packages/board-core/dist/homes.js
|
|
2098
|
-
import { chmod, mkdir as mkdir3, readFile as
|
|
2194
|
+
import { chmod, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
2099
2195
|
import path4 from "node:path";
|
|
2100
2196
|
import { execa } from "execa";
|
|
2101
2197
|
var HOME_GITIGNORE = [
|
|
@@ -2239,7 +2335,7 @@ var HomeRepos = class {
|
|
|
2239
2335
|
await writeFile3(ignore, IGNORE_FILE, "utf8");
|
|
2240
2336
|
await git(["add", "--all"], home);
|
|
2241
2337
|
await git(["commit", "--quiet", "--allow-empty", "-m", "home: created by the board"], home);
|
|
2242
|
-
} else if (await
|
|
2338
|
+
} else if (await readFile3(ignore, "utf8").catch(() => "") !== IGNORE_FILE) {
|
|
2243
2339
|
await writeFile3(ignore, IGNORE_FILE, "utf8");
|
|
2244
2340
|
await git(["add", ".gitignore"], home);
|
|
2245
2341
|
await git(["commit", "--quiet", "-m", "home: the board's ignore file"], home);
|
|
@@ -2335,6 +2431,55 @@ var HomeRepos = class {
|
|
|
2335
2431
|
}
|
|
2336
2432
|
return files;
|
|
2337
2433
|
}
|
|
2434
|
+
/**
|
|
2435
|
+
* The files of a folder in a home's newest commit, by path within the folder, with the commit;
|
|
2436
|
+
* conflict copies and anything but a regular file are left out. Null when the home is not a
|
|
2437
|
+
* repository yet.
|
|
2438
|
+
*/
|
|
2439
|
+
async folder(home, folder) {
|
|
2440
|
+
if (!await exists(path4.join(home, ".git"))) {
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
const commit = (await read(home, ["rev-parse", "--verify", "HEAD^{commit}"])).trim();
|
|
2444
|
+
const prefix = `${folder}/`;
|
|
2445
|
+
const listed = await read(home, [
|
|
2446
|
+
"ls-tree",
|
|
2447
|
+
"-r",
|
|
2448
|
+
"-z",
|
|
2449
|
+
"--long",
|
|
2450
|
+
"--full-tree",
|
|
2451
|
+
commit,
|
|
2452
|
+
prefix
|
|
2453
|
+
]);
|
|
2454
|
+
const files = [];
|
|
2455
|
+
for (const entry of listed.split("\0")) {
|
|
2456
|
+
const match = /^(100644|100755) blob ([0-9a-f]+) +(\d+)\t(.+)$/.exec(entry);
|
|
2457
|
+
const [, mode = "", object = "", size = "0", file = ""] = match ?? [];
|
|
2458
|
+
if (match === null || !file.startsWith(prefix) || !isSafeRelativePath(file) || conflictCopyOf(file) !== null) {
|
|
2459
|
+
continue;
|
|
2460
|
+
}
|
|
2461
|
+
files.push({
|
|
2462
|
+
path: file.slice(prefix.length),
|
|
2463
|
+
object,
|
|
2464
|
+
size: Number.parseInt(size, 10),
|
|
2465
|
+
executable: mode === "100755"
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
return { commit, files };
|
|
2469
|
+
}
|
|
2470
|
+
/** A blob of a home's repository, as `folder` names it. */
|
|
2471
|
+
async blob(home, object) {
|
|
2472
|
+
const result = await execa("git", ["cat-file", "blob", object], {
|
|
2473
|
+
cwd: home,
|
|
2474
|
+
encoding: "buffer",
|
|
2475
|
+
stripFinalNewline: false,
|
|
2476
|
+
reject: false
|
|
2477
|
+
});
|
|
2478
|
+
if (result.exitCode !== 0) {
|
|
2479
|
+
throw new Error(`git cat-file failed: ${Buffer.from(result.stderr).toString().trim()}`);
|
|
2480
|
+
}
|
|
2481
|
+
return Buffer.from(result.stdout);
|
|
2482
|
+
}
|
|
2338
2483
|
/** The conflict copies a merge added, against its first parent. */
|
|
2339
2484
|
async keptCopies(home, parent, commit) {
|
|
2340
2485
|
const added = await read(home, [
|
|
@@ -2750,6 +2895,10 @@ var BoardPaths = class {
|
|
|
2750
2895
|
proposal(id) {
|
|
2751
2896
|
return path5.join(this.proposals(), `${id}.md`);
|
|
2752
2897
|
}
|
|
2898
|
+
/** A skill proposal's folder, as the board took it from its proposer's home. */
|
|
2899
|
+
proposalFiles(id) {
|
|
2900
|
+
return path5.join(this.proposals(), id);
|
|
2901
|
+
}
|
|
2753
2902
|
decisions() {
|
|
2754
2903
|
return path5.join(this.society, "decisions");
|
|
2755
2904
|
}
|
|
@@ -3001,6 +3150,20 @@ var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
|
|
|
3001
3150
|
function withoutMentions(text) {
|
|
3002
3151
|
return text.replaceAll(MENTION_PATTERN, "$1$2");
|
|
3003
3152
|
}
|
|
3153
|
+
var SKILL_TEXT_LIMIT_CHARS = 1e5;
|
|
3154
|
+
function skillFolderOf(proposal) {
|
|
3155
|
+
if (proposal.kind !== "skill") {
|
|
3156
|
+
return null;
|
|
3157
|
+
}
|
|
3158
|
+
const parsed = SkillProposalSchema.safeParse(proposal.charter);
|
|
3159
|
+
return parsed.success && parsed.data.body === void 0 ? parsed.data.name : null;
|
|
3160
|
+
}
|
|
3161
|
+
function proposedFrom(origin, where) {
|
|
3162
|
+
return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel;
|
|
3163
|
+
}
|
|
3164
|
+
function megabytes(bytes) {
|
|
3165
|
+
return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
|
|
3166
|
+
}
|
|
3004
3167
|
var CURATING_ROLES = [USER_ROLE, "steward"];
|
|
3005
3168
|
var SEED_INSTRUCTIONS_HEADING = "## Seed instructions";
|
|
3006
3169
|
var OPS_WAKE_TRIGGER = "ops_event";
|
|
@@ -3091,8 +3254,8 @@ function restage(existing, edit) {
|
|
|
3091
3254
|
};
|
|
3092
3255
|
}
|
|
3093
3256
|
function reopenStage(stage) {
|
|
3094
|
-
const { completedBy: _by, completedAt: _at, ...
|
|
3095
|
-
return
|
|
3257
|
+
const { completedBy: _by, completedAt: _at, ...open2 } = stage;
|
|
3258
|
+
return open2;
|
|
3096
3259
|
}
|
|
3097
3260
|
function gateChanges(before, after) {
|
|
3098
3261
|
const changes = [];
|
|
@@ -3487,7 +3650,7 @@ var Board = class _Board {
|
|
|
3487
3650
|
const task = { ...TaskFrontmatterSchema.parse(raw.data), body: raw.body };
|
|
3488
3651
|
if ((legacy === "open" || legacy === "closed") && !await exists(this.paths.threadFile(threads, task.id))) {
|
|
3489
3652
|
const ended = task.status === "done" || task.status === "abandoned";
|
|
3490
|
-
const
|
|
3653
|
+
const open2 = legacy === "open" && !ended;
|
|
3491
3654
|
const openedBy = opened.get(task.id);
|
|
3492
3655
|
const closedBy = closed.get(task.id);
|
|
3493
3656
|
await this.writeThread(threads, {
|
|
@@ -3495,10 +3658,10 @@ var Board = class _Board {
|
|
|
3495
3658
|
channel: channelRef(slug, "general"),
|
|
3496
3659
|
title: task.title,
|
|
3497
3660
|
subject: { kind: "task", id: task.id },
|
|
3498
|
-
state:
|
|
3661
|
+
state: open2 ? "open" : "closed",
|
|
3499
3662
|
openedBy: openedBy?.actor ?? task.createdBy,
|
|
3500
3663
|
openedAt: openedBy?.ts ?? task.createdAt,
|
|
3501
|
-
...
|
|
3664
|
+
...open2 ? {} : {
|
|
3502
3665
|
closedBy: closedBy?.actor ?? SYSTEM_ACTOR.name,
|
|
3503
3666
|
closedAt: closedBy?.ts ?? this.now().toISOString()
|
|
3504
3667
|
},
|
|
@@ -3561,7 +3724,7 @@ var Board = class _Board {
|
|
|
3561
3724
|
for (const agent of await this.listAgents()) {
|
|
3562
3725
|
const file = this.paths.agentCursors(agent.name);
|
|
3563
3726
|
if (await exists(file)) {
|
|
3564
|
-
const legacy = Legacy.safeParse(JSON.parse(await
|
|
3727
|
+
const legacy = Legacy.safeParse(JSON.parse(await readFile4(file, "utf8")));
|
|
3565
3728
|
if (legacy.success) {
|
|
3566
3729
|
await writeJson(file, { digest: legacy.data.inbox });
|
|
3567
3730
|
}
|
|
@@ -3900,6 +4063,10 @@ ${charter.purpose}
|
|
|
3900
4063
|
async latestEventId() {
|
|
3901
4064
|
return this.events.lastId();
|
|
3902
4065
|
}
|
|
4066
|
+
/** Calls `listener` after each event the board appends, until the returned function is called. */
|
|
4067
|
+
onEventAppended(listener) {
|
|
4068
|
+
return this.events.onAppend(listener);
|
|
4069
|
+
}
|
|
3903
4070
|
/** The roster as projected: every citizen with identity, reach, availability, and profile. */
|
|
3904
4071
|
async listMembers() {
|
|
3905
4072
|
const members = [];
|
|
@@ -3923,22 +4090,41 @@ ${charter.purpose}
|
|
|
3923
4090
|
const dir = project === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project);
|
|
3924
4091
|
const topics = [];
|
|
3925
4092
|
for (const file of await listFiles(dir)) {
|
|
3926
|
-
|
|
3927
|
-
const parsed = KnowledgeSchema.safeParse({
|
|
3928
|
-
...doc.data,
|
|
3929
|
-
project,
|
|
3930
|
-
topic: file.replace(/\.md$/, "")
|
|
3931
|
-
});
|
|
3932
|
-
const data = parsed.success ? parsed.data : {
|
|
3933
|
-
topic: file.replace(/\.md$/, ""),
|
|
3934
|
-
project,
|
|
3935
|
-
updatedBy: SYSTEM_ACTOR.name,
|
|
3936
|
-
updatedAt: (await stat2(path6.join(dir, file))).mtime.toISOString()
|
|
3937
|
-
};
|
|
3938
|
-
topics.push({ ...data, body: doc.body });
|
|
4093
|
+
topics.push(await this.knowledgeAt(path6.join(dir, file), project, file.replace(/\.md$/, "")));
|
|
3939
4094
|
}
|
|
3940
4095
|
return topics;
|
|
3941
4096
|
}
|
|
4097
|
+
knowledgeFile(project, topic) {
|
|
4098
|
+
return project === null ? this.paths.societyKnowledgeFile(topic) : this.paths.projectKnowledgeFile(project, topic);
|
|
4099
|
+
}
|
|
4100
|
+
/**
|
|
4101
|
+
* Read loosely, as a dashboard is: a topic written before revisions counts as revision 0, and an
|
|
4102
|
+
* author or a time that does not parse is the board's at the file's mtime.
|
|
4103
|
+
*/
|
|
4104
|
+
async knowledgeAt(file, project, topic) {
|
|
4105
|
+
const { data, body } = await readLooseMarkdown(file);
|
|
4106
|
+
const revision = z13.number().int().min(0).safeParse(data["revision"]);
|
|
4107
|
+
const updatedBy = NameSchema.safeParse(data["updatedBy"]);
|
|
4108
|
+
const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
|
|
4109
|
+
return {
|
|
4110
|
+
topic,
|
|
4111
|
+
project,
|
|
4112
|
+
revision: revision.success ? revision.data : 0,
|
|
4113
|
+
updatedBy: updatedBy.success ? updatedBy.data : SYSTEM_ACTOR.name,
|
|
4114
|
+
updatedAt: updatedAt.success ? updatedAt.data : (await stat3(file)).mtime.toISOString(),
|
|
4115
|
+
body
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
/** The highest revision among a topic's removed copies, 0 when it has none. */
|
|
4119
|
+
async removedRevision(project, topic) {
|
|
4120
|
+
const aside = this.paths.removedKnowledge(project ?? SOCIETY_SCOPE);
|
|
4121
|
+
let highest = 0;
|
|
4122
|
+
for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
|
|
4123
|
+
const copy = await this.knowledgeAt(path6.join(aside, file), project, topic);
|
|
4124
|
+
highest = Math.max(highest, copy.revision);
|
|
4125
|
+
}
|
|
4126
|
+
return highest;
|
|
4127
|
+
}
|
|
3942
4128
|
/** Skills promoted to the society: every citizen may read them. */
|
|
3943
4129
|
async listSocietySkills() {
|
|
3944
4130
|
return this.listSkillsIn(this.paths.societySkills(), "society");
|
|
@@ -4027,6 +4213,28 @@ ${charter.purpose}
|
|
|
4027
4213
|
async readThread(id) {
|
|
4028
4214
|
return (await this.findThread(id)).thread;
|
|
4029
4215
|
}
|
|
4216
|
+
/**
|
|
4217
|
+
* Where each of the messages named is and who wrote it, for text that names them by id; an id
|
|
4218
|
+
* that names no message is left out. Message files start with their ids, so only the files
|
|
4219
|
+
* asked for are read.
|
|
4220
|
+
*/
|
|
4221
|
+
async findMessages(ids) {
|
|
4222
|
+
const wanted = new Set(ids);
|
|
4223
|
+
const found = [];
|
|
4224
|
+
for await (const dir of this.messageDirs()) {
|
|
4225
|
+
for (const file of await listFiles(dir)) {
|
|
4226
|
+
if (!wanted.delete(file.slice(0, 26))) {
|
|
4227
|
+
continue;
|
|
4228
|
+
}
|
|
4229
|
+
const { data } = await readMarkdown(path6.join(dir, file), MessageFrontmatterSchema);
|
|
4230
|
+
found.push(MessageRefSchema.parse(data));
|
|
4231
|
+
}
|
|
4232
|
+
if (wanted.size === 0) {
|
|
4233
|
+
break;
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
return found;
|
|
4237
|
+
}
|
|
4030
4238
|
/**
|
|
4031
4239
|
* What citizens have asked the user and the user has not answered, oldest first: every message
|
|
4032
4240
|
* by someone else that mentions the user, in an open thread or a channel the society or an
|
|
@@ -4528,12 +4736,12 @@ ${DASHBOARD_LINE}
|
|
|
4528
4736
|
continue;
|
|
4529
4737
|
}
|
|
4530
4738
|
const named = thread === null ? channelConversation(filedIn, message.channel) : void 0;
|
|
4531
|
-
let
|
|
4532
|
-
if (named !== void 0 &&
|
|
4533
|
-
|
|
4534
|
-
openChannels.set(message.channel,
|
|
4739
|
+
let open2 = named === void 0 ? void 0 : openChannels.get(message.channel);
|
|
4740
|
+
if (named !== void 0 && open2 === void 0) {
|
|
4741
|
+
open2 = await this.channelOpen(message.channel);
|
|
4742
|
+
openChannels.set(message.channel, open2);
|
|
4535
4743
|
}
|
|
4536
|
-
const part = thread ?? (
|
|
4744
|
+
const part = thread ?? (open2 === true ? `#${named}` : null);
|
|
4537
4745
|
if (conversation !== void 0 && (filedIn !== conversation.scope || part !== conversation.part)) {
|
|
4538
4746
|
continue;
|
|
4539
4747
|
}
|
|
@@ -4598,7 +4806,7 @@ ${task.body}`;
|
|
|
4598
4806
|
}
|
|
4599
4807
|
}
|
|
4600
4808
|
for (const file of await listFiles(this.paths.projectKnowledge(project))) {
|
|
4601
|
-
const content = await
|
|
4809
|
+
const content = await readFile4(path6.join(this.paths.projectKnowledge(project), file), "utf8");
|
|
4602
4810
|
if (content.toLowerCase().includes(needle)) {
|
|
4603
4811
|
hits.push({
|
|
4604
4812
|
kind: "knowledge",
|
|
@@ -4636,7 +4844,7 @@ ${task.body}`;
|
|
|
4636
4844
|
});
|
|
4637
4845
|
}
|
|
4638
4846
|
for (const entry of shared) {
|
|
4639
|
-
const content = await
|
|
4847
|
+
const content = await readFile4(entry.file, "utf8");
|
|
4640
4848
|
if (content.toLowerCase().includes(needle)) {
|
|
4641
4849
|
hits.push({
|
|
4642
4850
|
kind: entry.kind,
|
|
@@ -4652,26 +4860,42 @@ ${task.body}`;
|
|
|
4652
4860
|
}
|
|
4653
4861
|
/**
|
|
4654
4862
|
* Writes a topic: a project's by its members, the society's by the curating roles. The note in
|
|
4655
|
-
* the matching general channel carries no mention, so nobody is woken.
|
|
4863
|
+
* the matching general channel carries no mention, so nobody is woken. A turn reads a topic from
|
|
4864
|
+
* the copy of the projection pulled when it started, so a revision other than the current one, 0
|
|
4865
|
+
* for a topic not there, means someone wrote or removed it since; the refusal carries the current
|
|
4866
|
+
* text, which that copy lacks.
|
|
4656
4867
|
*/
|
|
4657
4868
|
async writeKnowledge(actor, input) {
|
|
4658
4869
|
const args = VerbInputs.write_knowledge.parse(input);
|
|
4659
4870
|
await this.authorize(actor, "write_knowledge");
|
|
4660
4871
|
return this.mutex.run(async () => {
|
|
4661
4872
|
await this.assertMayCurate(actor, args.project);
|
|
4873
|
+
const file = this.knowledgeFile(args.project, args.topic);
|
|
4874
|
+
const current = await exists(file) ? await this.knowledgeAt(file, args.project, args.topic) : null;
|
|
4875
|
+
const place = args.project ?? "the society";
|
|
4876
|
+
if (current === null && args.revision !== 0) {
|
|
4877
|
+
throw new BoardError("INVALID_STATE", `${place} has no knowledge topic ${args.topic}, so it has no revision ${args.revision}: it was removed since you read it, or never written. Write it with revision 0 to start it anew.`);
|
|
4878
|
+
}
|
|
4879
|
+
if (current !== null && args.revision !== current.revision) {
|
|
4880
|
+
throw new BoardError("INVALID_STATE", `the knowledge topic ${args.topic} of ${place} is at revision ${current.revision}, updated by ${current.updatedBy} at ${current.updatedAt}, not ${args.revision}. Merge your changes into its current text, below, and write again with revision ${current.revision}.
|
|
4881
|
+
|
|
4882
|
+
${current.body}`);
|
|
4883
|
+
}
|
|
4884
|
+
const base = current?.revision ?? await this.removedRevision(args.project, args.topic);
|
|
4662
4885
|
const data = KnowledgeSchema.parse({
|
|
4663
4886
|
topic: args.topic,
|
|
4664
4887
|
project: args.project,
|
|
4888
|
+
revision: base + 1,
|
|
4665
4889
|
updatedBy: actor.name,
|
|
4666
4890
|
updatedAt: this.now().toISOString()
|
|
4667
4891
|
});
|
|
4668
|
-
const
|
|
4669
|
-
const replaced = await exists(file);
|
|
4892
|
+
const replaced = current !== null;
|
|
4670
4893
|
await writeMarkdown(file, data, args.body);
|
|
4671
4894
|
await this.events.append("knowledge.written", actor.name, {
|
|
4672
4895
|
topic: args.topic,
|
|
4673
4896
|
project: args.project,
|
|
4674
|
-
replaced
|
|
4897
|
+
replaced,
|
|
4898
|
+
revision: data.revision
|
|
4675
4899
|
});
|
|
4676
4900
|
const where = args.project === null ? "general" : channelRef(args.project, "general");
|
|
4677
4901
|
await this.appendMessage(actor.name, where, `Knowledge ${replaced ? "updated" : "written"}: ${args.topic}. It is under knowledge/${args.topic}.md${args.project === null ? " of the society" : ""}.`, {
|
|
@@ -4694,7 +4918,7 @@ ${task.body}`;
|
|
|
4694
4918
|
await this.authorize(actor, "remove_knowledge");
|
|
4695
4919
|
return this.mutex.run(async () => {
|
|
4696
4920
|
await this.assertMayCurate(actor, args.project);
|
|
4697
|
-
const file =
|
|
4921
|
+
const file = this.knowledgeFile(args.project, args.topic);
|
|
4698
4922
|
if (!await exists(file)) {
|
|
4699
4923
|
throw new BoardError("NOT_FOUND", `${args.project ?? "the society"} has no knowledge topic ${args.topic}`);
|
|
4700
4924
|
}
|
|
@@ -5745,7 +5969,7 @@ ${args.summary}`);
|
|
|
5745
5969
|
throw new BoardError("VALIDATION", `invalid ${args.kind} charter: ${parsed.error.message}`);
|
|
5746
5970
|
}
|
|
5747
5971
|
return this.mutex.run(async () => {
|
|
5748
|
-
|
|
5972
|
+
let proposal = {
|
|
5749
5973
|
...ProposalFrontmatterSchema.parse({
|
|
5750
5974
|
id: this.newId(),
|
|
5751
5975
|
kind: args.kind,
|
|
@@ -5764,6 +5988,14 @@ ${args.summary}`);
|
|
|
5764
5988
|
body: args.rationale
|
|
5765
5989
|
};
|
|
5766
5990
|
await this.validateProvision(proposal.kind, proposal.charter);
|
|
5991
|
+
const skill = skillFolderOf(proposal);
|
|
5992
|
+
if (skill !== null && proposal.filedFrom === void 0) {
|
|
5993
|
+
const taken = await this.takeSkillFolderUnlocked(proposal, skill);
|
|
5994
|
+
if (typeof taken === "string") {
|
|
5995
|
+
throw new BoardError("VALIDATION", taken);
|
|
5996
|
+
}
|
|
5997
|
+
proposal = { ...proposal, skillFiles: taken };
|
|
5998
|
+
}
|
|
5767
5999
|
await this.writeProposal(proposal);
|
|
5768
6000
|
await this.events.append("proposal.created", actor.name, {
|
|
5769
6001
|
proposalId: proposal.id,
|
|
@@ -6101,7 +6333,7 @@ ${rationale}`}`);
|
|
|
6101
6333
|
throw new BoardError("NOT_FOUND", `no transcript of turn ${turnId} for ${agent}`);
|
|
6102
6334
|
}
|
|
6103
6335
|
const entries = [];
|
|
6104
|
-
for (const line of (await
|
|
6336
|
+
for (const line of (await readFile4(file, "utf8")).split("\n")) {
|
|
6105
6337
|
if (line.trim() === "")
|
|
6106
6338
|
continue;
|
|
6107
6339
|
const parsed = TranscriptEntrySchema.safeParse(JSON.parse(line));
|
|
@@ -6580,7 +6812,7 @@ ${rationale}`}`);
|
|
|
6580
6812
|
}
|
|
6581
6813
|
const file = entry.isFile() ? conflictCopyOf(relative) : null;
|
|
6582
6814
|
if (file !== null) {
|
|
6583
|
-
const since = await this.homes.changedAt(home, relative) ?? (await
|
|
6815
|
+
const since = await this.homes.changedAt(home, relative) ?? (await stat3(path6.join(home, relative)).catch(() => null))?.mtime.toISOString();
|
|
6584
6816
|
if (since !== void 0) {
|
|
6585
6817
|
found.push({ path: relative, file, since });
|
|
6586
6818
|
}
|
|
@@ -6980,7 +7212,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
6980
7212
|
}
|
|
6981
7213
|
case "skill": {
|
|
6982
7214
|
const skill = SkillProposalSchema.parse(proposal.charter);
|
|
6983
|
-
const replaced = await this.promoteSkillUnlocked(by, proposal
|
|
7215
|
+
const replaced = await this.promoteSkillUnlocked(by, proposal, skill, meta);
|
|
6984
7216
|
return { skill: skill.name, replaced };
|
|
6985
7217
|
}
|
|
6986
7218
|
case "archive": {
|
|
@@ -6994,26 +7226,180 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
6994
7226
|
return void 0;
|
|
6995
7227
|
}
|
|
6996
7228
|
}
|
|
6997
|
-
/**
|
|
6998
|
-
|
|
7229
|
+
/**
|
|
7230
|
+
* Publishes a promoted skill under the society's skills, where every citizen's skills index lists
|
|
7231
|
+
* it: the folder the board took from its proposer's home, or the text proposed alone. A
|
|
7232
|
+
* replacement is the whole skill, so files the new version dropped go with the old one.
|
|
7233
|
+
*/
|
|
7234
|
+
async promoteSkillUnlocked(by, proposal, skill, meta) {
|
|
7235
|
+
const dir = this.paths.societySkill(skill.name);
|
|
6999
7236
|
const file = this.paths.societySkillFile(skill.name);
|
|
7000
7237
|
const replaced = await exists(file);
|
|
7001
|
-
|
|
7238
|
+
const taken = proposal.skillFiles;
|
|
7239
|
+
const staged = path6.join(this.paths.proposalFiles(proposal.id), "SKILL.md");
|
|
7240
|
+
if (skill.body === void 0 && (taken === void 0 || !await exists(staged))) {
|
|
7241
|
+
throw new BoardError("INVALID_STATE", `proposal ${proposal.id} holds no skill files`);
|
|
7242
|
+
}
|
|
7243
|
+
await rm2(dir, { recursive: true, force: true });
|
|
7244
|
+
if (skill.body === void 0) {
|
|
7245
|
+
await cp(this.paths.proposalFiles(proposal.id), dir, { recursive: true });
|
|
7246
|
+
} else {
|
|
7247
|
+
await ensureDir(dir);
|
|
7248
|
+
}
|
|
7249
|
+
const body = skill.body ?? (await readLooseMarkdown(file)).body;
|
|
7002
7250
|
await writeMarkdown(file, {
|
|
7003
7251
|
name: skill.name,
|
|
7004
7252
|
description: skill.summary,
|
|
7005
|
-
metadata: {
|
|
7006
|
-
|
|
7253
|
+
metadata: {
|
|
7254
|
+
proposedBy: proposal.proposedBy,
|
|
7255
|
+
promotedBy: by,
|
|
7256
|
+
promotedAt: this.now().toISOString(),
|
|
7257
|
+
...taken === void 0 ? {} : { commit: taken.commit }
|
|
7258
|
+
}
|
|
7259
|
+
}, body);
|
|
7007
7260
|
await this.events.append("skill.promoted", by, {
|
|
7008
7261
|
name: skill.name,
|
|
7009
7262
|
summary: skill.summary,
|
|
7010
|
-
proposedBy,
|
|
7263
|
+
proposedBy: proposal.proposedBy,
|
|
7011
7264
|
replaced,
|
|
7265
|
+
...taken === void 0 ? {} : { files: taken.files.length },
|
|
7012
7266
|
...meta
|
|
7013
7267
|
});
|
|
7014
7268
|
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.`);
|
|
7015
7269
|
return replaced;
|
|
7016
7270
|
}
|
|
7271
|
+
/**
|
|
7272
|
+
* Copies a skill's folder from its proposer's home, as the home's newest commit holds it, into the
|
|
7273
|
+
* proposal's folder in the projection, where its deciders read it and from where approval
|
|
7274
|
+
* publishes it. Returns what was taken, or why the home could not give it.
|
|
7275
|
+
*/
|
|
7276
|
+
async takeSkillFolderUnlocked(proposal, name) {
|
|
7277
|
+
const home = this.paths.agent(proposal.proposedBy);
|
|
7278
|
+
const folder = `skills/${name}`;
|
|
7279
|
+
const where = `${folder} in ${proposal.proposedBy}'s home`;
|
|
7280
|
+
const taken = await this.homes.folder(home, folder);
|
|
7281
|
+
if (taken === null || !taken.files.some((entry) => entry.path === "SKILL.md")) {
|
|
7282
|
+
return `no ${folder}/SKILL.md in ${proposal.proposedBy}'s home`;
|
|
7283
|
+
}
|
|
7284
|
+
if (taken.files.length > SKILL_FILES_LIMIT) {
|
|
7285
|
+
return `${where} holds ${taken.files.length} files, more than the ${SKILL_FILES_LIMIT} a society skill may`;
|
|
7286
|
+
}
|
|
7287
|
+
const bytes = taken.files.reduce((sum, entry) => sum + entry.size, 0);
|
|
7288
|
+
if (bytes > SKILL_BYTES_LIMIT) {
|
|
7289
|
+
return `${where} holds ${megabytes(bytes)} MB, more than the ${megabytes(SKILL_BYTES_LIMIT)} MB a society skill may`;
|
|
7290
|
+
}
|
|
7291
|
+
const contents = /* @__PURE__ */ new Map();
|
|
7292
|
+
for (const entry of taken.files) {
|
|
7293
|
+
contents.set(entry.path, await this.homes.blob(home, entry.object));
|
|
7294
|
+
}
|
|
7295
|
+
const manual = parseLooseMarkdown(contents.get("SKILL.md")?.toString("utf8") ?? "");
|
|
7296
|
+
if (manual.error !== null) {
|
|
7297
|
+
return `the frontmatter of ${folder}/SKILL.md in ${proposal.proposedBy}'s home does not parse (${manual.error})`;
|
|
7298
|
+
}
|
|
7299
|
+
const dir = this.paths.proposalFiles(proposal.id);
|
|
7300
|
+
await rm2(dir, { recursive: true, force: true });
|
|
7301
|
+
for (const entry of taken.files) {
|
|
7302
|
+
const target = path6.join(dir, ...entry.path.split("/"));
|
|
7303
|
+
await ensureDir(path6.dirname(target));
|
|
7304
|
+
await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
|
|
7305
|
+
await chmod2(target, entry.executable ? 493 : 420);
|
|
7306
|
+
}
|
|
7307
|
+
return {
|
|
7308
|
+
commit: taken.commit,
|
|
7309
|
+
takenAt: this.now().toISOString(),
|
|
7310
|
+
files: taken.files.map((entry) => ({
|
|
7311
|
+
path: entry.path,
|
|
7312
|
+
size: entry.size,
|
|
7313
|
+
executable: entry.executable
|
|
7314
|
+
}))
|
|
7315
|
+
};
|
|
7316
|
+
}
|
|
7317
|
+
/** Keeps what was taken of a skill proposal's folder on the proposal, and logs it. */
|
|
7318
|
+
async recordSkillFiles(proposal, taken) {
|
|
7319
|
+
const next = { ...proposal, skillFiles: taken };
|
|
7320
|
+
await this.writeProposal(next);
|
|
7321
|
+
await this.events.append("proposal.files_taken", SYSTEM_ACTOR.name, {
|
|
7322
|
+
proposalId: proposal.id,
|
|
7323
|
+
kind: proposal.kind,
|
|
7324
|
+
proposedBy: proposal.proposedBy,
|
|
7325
|
+
commit: taken.commit,
|
|
7326
|
+
files: taken.files.length
|
|
7327
|
+
});
|
|
7328
|
+
return next;
|
|
7329
|
+
}
|
|
7330
|
+
/**
|
|
7331
|
+
* A skill proposal with its folder, for approval: taken now when the turn that filed it ended
|
|
7332
|
+
* without it, as when the server restarted before the turn's end was recorded, and refused while
|
|
7333
|
+
* that turn is still in flight, since the folder reaches the board only when it ends.
|
|
7334
|
+
*/
|
|
7335
|
+
async skillFilesForApproval(proposal) {
|
|
7336
|
+
const name = skillFolderOf(proposal);
|
|
7337
|
+
if (name === null || proposal.skillFiles !== void 0) {
|
|
7338
|
+
return proposal;
|
|
7339
|
+
}
|
|
7340
|
+
const cannot = `proposal ${proposal.id} cannot be approved`;
|
|
7341
|
+
const filing = this.turnsInFlight().some((turn) => turn.agent === proposal.proposedBy && proposedFrom(proposal.filedFrom, turn));
|
|
7342
|
+
if (filing) {
|
|
7343
|
+
throw new BoardError("INVALID_STATE", `${cannot} until ${proposal.proposedBy}'s turn that proposed it ends, when the board takes the skill's folder from its home`);
|
|
7344
|
+
}
|
|
7345
|
+
const taken = await this.takeSkillFolderUnlocked(proposal, name);
|
|
7346
|
+
if (typeof taken === "string") {
|
|
7347
|
+
throw new BoardError("INVALID_STATE", `${cannot}: ${taken}; reject it with that reason`);
|
|
7348
|
+
}
|
|
7349
|
+
return this.recordSkillFiles(proposal, taken);
|
|
7350
|
+
}
|
|
7351
|
+
/**
|
|
7352
|
+
* Takes the folders of the skills a citizen proposed from a conversation, once the turn there has
|
|
7353
|
+
* ended and its runner has pushed the citizen's home. A proposal whose folder the home does not
|
|
7354
|
+
* give is rejected by the board with the reason, which wakes the proposer to put it right; while
|
|
7355
|
+
* someone is in a turn in the proposal's thread, it stays as it is, and approval meets the reason.
|
|
7356
|
+
*/
|
|
7357
|
+
async takeSkillFiles(turn) {
|
|
7358
|
+
const where = {
|
|
7359
|
+
scope: turn.project,
|
|
7360
|
+
...turn.thread === void 0 ? {} : { thread: turn.thread },
|
|
7361
|
+
...turn.channel === void 0 ? {} : { channel: turn.channel }
|
|
7362
|
+
};
|
|
7363
|
+
await this.mutex.run(async () => {
|
|
7364
|
+
for (const proposal of await this.listProposals()) {
|
|
7365
|
+
const name = skillFolderOf(proposal);
|
|
7366
|
+
if (name === null || proposal.status !== "proposed" || proposal.skillFiles !== void 0 || proposal.proposedBy !== turn.agent || !proposedFrom(proposal.filedFrom, where)) {
|
|
7367
|
+
continue;
|
|
7368
|
+
}
|
|
7369
|
+
const taken = await this.takeSkillFolderUnlocked(proposal, name);
|
|
7370
|
+
if (typeof taken !== "string") {
|
|
7371
|
+
await this.recordSkillFiles(proposal, taken);
|
|
7372
|
+
continue;
|
|
7373
|
+
}
|
|
7374
|
+
try {
|
|
7375
|
+
await this.decideUnlocked(SYSTEM_ACTOR, proposal.id, "rejected", `${taken} when the turn that proposed it ended; propose it again once the folder is there`);
|
|
7376
|
+
} catch (error) {
|
|
7377
|
+
if (!(error instanceof BoardError)) {
|
|
7378
|
+
throw error;
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7381
|
+
}
|
|
7382
|
+
});
|
|
7383
|
+
}
|
|
7384
|
+
/** A skill proposal's files with their text, as the board took them; none before then. */
|
|
7385
|
+
async readSkillFiles(proposalId) {
|
|
7386
|
+
const proposal = await this.readProposal(proposalId);
|
|
7387
|
+
const dir = this.paths.proposalFiles(proposal.id);
|
|
7388
|
+
const files = [];
|
|
7389
|
+
for (const entry of proposal.skillFiles?.files ?? []) {
|
|
7390
|
+
if (!isSafeRelativePath(entry.path)) {
|
|
7391
|
+
continue;
|
|
7392
|
+
}
|
|
7393
|
+
const bytes = await readFile4(path6.join(dir, ...entry.path.split("/"))).catch(() => null);
|
|
7394
|
+
const text = bytes === null || bytes.includes(0) ? null : bytes.toString("utf8");
|
|
7395
|
+
files.push({
|
|
7396
|
+
...entry,
|
|
7397
|
+
text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
|
|
7398
|
+
truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
|
|
7399
|
+
});
|
|
7400
|
+
}
|
|
7401
|
+
return files;
|
|
7402
|
+
}
|
|
7017
7403
|
async validateAddAgent(input) {
|
|
7018
7404
|
if (await exists(this.paths.agentFile(input.name))) {
|
|
7019
7405
|
throw new BoardError("ALREADY_EXISTS", `agent ${input.name} already exists`);
|
|
@@ -7525,67 +7911,70 @@ ${charter.purpose}
|
|
|
7525
7911
|
* retired. Validation runs before any write, so an impossible provision fails the decision.
|
|
7526
7912
|
*/
|
|
7527
7913
|
async decide(actor, proposalId, outcome, reason) {
|
|
7528
|
-
return this.mutex.run(
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
this.
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7914
|
+
return this.mutex.run(() => this.decideUnlocked(actor, proposalId, outcome, reason));
|
|
7915
|
+
}
|
|
7916
|
+
async decideUnlocked(actor, proposalId, outcome, reason) {
|
|
7917
|
+
const filed = await this.readProposal(proposalId);
|
|
7918
|
+
if (filed.status !== "proposed") {
|
|
7919
|
+
throw new BoardError("INVALID_STATE", `proposal ${proposalId} is already ${filed.status}`);
|
|
7920
|
+
}
|
|
7921
|
+
if (filed.proposedBy === actor.name) {
|
|
7922
|
+
throw new BoardError("FORBIDDEN", "a proposer never decides its own proposal");
|
|
7923
|
+
}
|
|
7924
|
+
const approvers = ROLE_KIND_APPROVERS[filed.kind];
|
|
7925
|
+
if (!approvers.includes(actor.role)) {
|
|
7926
|
+
throw new BoardError("FORBIDDEN", `a ${filed.kind} proposal requires one of: ${approvers.join(", ")}`);
|
|
7927
|
+
}
|
|
7928
|
+
if (outcome === "approved") {
|
|
7929
|
+
await this.validateProvision(filed.kind, filed.charter);
|
|
7930
|
+
}
|
|
7931
|
+
this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn) => turn.thread === proposalId);
|
|
7932
|
+
if (outcome === "approved" && filed.kind === "archive") {
|
|
7933
|
+
await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(filed.charter).project);
|
|
7934
|
+
}
|
|
7935
|
+
const proposal = outcome === "approved" ? await this.skillFilesForApproval(filed) : filed;
|
|
7936
|
+
const ts = this.now().toISOString();
|
|
7937
|
+
const decision = DecisionSchema.parse({
|
|
7938
|
+
id: this.newId(),
|
|
7939
|
+
proposalId,
|
|
7940
|
+
decidedBy: actor.name,
|
|
7941
|
+
outcome,
|
|
7942
|
+
...reason === void 0 ? {} : { reason },
|
|
7943
|
+
ts
|
|
7944
|
+
});
|
|
7945
|
+
await writeMarkdown(this.paths.decision(decision.id), decision, reason ?? "");
|
|
7946
|
+
const provision = outcome === "approved" ? await this.executeProvision(actor.name, proposal) : void 0;
|
|
7947
|
+
const status = outcome === "rejected" ? "rejected" : provision === void 0 ? "approved" : "provisioned";
|
|
7948
|
+
await this.writeProposal({
|
|
7949
|
+
...proposal,
|
|
7950
|
+
status,
|
|
7951
|
+
decidedBy: actor.name,
|
|
7952
|
+
decidedAt: ts,
|
|
7953
|
+
...reason === void 0 ? {} : { reason },
|
|
7954
|
+
...provision === void 0 ? {} : { provisionedAt: ts, provision }
|
|
7955
|
+
});
|
|
7956
|
+
await this.events.append("proposal.decided", actor.name, {
|
|
7957
|
+
proposalId,
|
|
7958
|
+
outcome,
|
|
7959
|
+
kind: proposal.kind,
|
|
7960
|
+
proposedBy: proposal.proposedBy,
|
|
7961
|
+
...proposal.filedFrom === void 0 ? {} : { filedFrom: proposal.filedFrom }
|
|
7962
|
+
});
|
|
7963
|
+
if (provision !== void 0) {
|
|
7964
|
+
await this.events.append("proposal.provisioned", actor.name, {
|
|
7568
7965
|
proposalId,
|
|
7569
|
-
outcome,
|
|
7570
7966
|
kind: proposal.kind,
|
|
7571
|
-
|
|
7572
|
-
...proposal.filedFrom === void 0 ? {} : { filedFrom: proposal.filedFrom }
|
|
7967
|
+
...provision
|
|
7573
7968
|
});
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
|
|
7584
|
-
const thread = await this.proposalThreadUnlocked(actor.name, proposal);
|
|
7585
|
-
await this.appendThreadMessage(actor.name, thread.id, `${verdict}: ${describeCharter(proposal.kind, proposal.charter)}.${why}${result}`, { step: { action: outcome } });
|
|
7586
|
-
await this.endThreadOf(proposalId, actor.name, outcome);
|
|
7587
|
-
return decision;
|
|
7588
|
-
});
|
|
7969
|
+
}
|
|
7970
|
+
const verdict = outcome === "approved" ? "Approved" : "Rejected";
|
|
7971
|
+
const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
|
|
7972
|
+
const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
|
|
7973
|
+
const thread = await this.proposalThreadUnlocked(actor.name, proposal);
|
|
7974
|
+
const post = `${verdict}: ${describeCharter(proposal.kind, proposal.charter)}.${why}${result}`;
|
|
7975
|
+
await this.appendThreadMessage(actor.name, thread.id, actor.name === SYSTEM_ACTOR.name ? withoutMentions(post) : post, { step: { action: outcome } });
|
|
7976
|
+
await this.endThreadOf(proposalId, actor.name, outcome);
|
|
7977
|
+
return decision;
|
|
7589
7978
|
}
|
|
7590
7979
|
/** The opener, the people of its subject, and anyone who posted in it; mentions reach the rest. */
|
|
7591
7980
|
async participatesInThread(actor, thread, threads) {
|
|
@@ -7700,6 +8089,23 @@ ${charter.purpose}
|
|
|
7700
8089
|
yield* this.threadMessagesIn(this.paths.threads(slug), since);
|
|
7701
8090
|
}
|
|
7702
8091
|
}
|
|
8092
|
+
/** Every folder that holds messages: each channel's and each thread's, in the society and every project. */
|
|
8093
|
+
async *messageDirs() {
|
|
8094
|
+
for (const channel of await listDirs(this.paths.societyChannels())) {
|
|
8095
|
+
yield this.paths.societyChannel(channel);
|
|
8096
|
+
}
|
|
8097
|
+
for (const id of await listDirs(this.paths.societyThreads())) {
|
|
8098
|
+
yield this.paths.threadMessages(this.paths.societyThreads(), id);
|
|
8099
|
+
}
|
|
8100
|
+
for (const slug of await listDirs(this.paths.projects())) {
|
|
8101
|
+
for (const channel of await listDirs(this.paths.projectChannels(slug))) {
|
|
8102
|
+
yield this.paths.projectChannel(slug, channel);
|
|
8103
|
+
}
|
|
8104
|
+
for (const id of await listDirs(this.paths.threads(slug))) {
|
|
8105
|
+
yield this.paths.threadMessages(this.paths.threads(slug), id);
|
|
8106
|
+
}
|
|
8107
|
+
}
|
|
8108
|
+
}
|
|
7703
8109
|
async *threadMessagesIn(threads, since) {
|
|
7704
8110
|
for (const id of await listDirs(threads)) {
|
|
7705
8111
|
yield* await this.readMessagesIn(this.paths.threadMessages(threads, id), since);
|
|
@@ -9336,7 +9742,8 @@ var TURN_CONTRACT = [
|
|
|
9336
9742
|
"- An @mention wakes the citizen named, and every wake costs a turn. Address someone with @ only when you need them to act; when you merely refer to citizens, write their names plainly. @user wakes nobody, since the user takes no turns; it is what puts a question before the user, so use it only when you need the user to answer or decide.",
|
|
9337
9743
|
"- A thread is a conversation off a channel that reaches only its participants and whoever is mentioned in it. Every task has one, opened with it under the task's id on the channel of its project it was filed in, general unless it was filed in another: talk about a task there, with post_message and its thread_id, and the notes you give advance_task and update_task are posted there as you. Its participants are the task's creator, its stage holders and assignees, whoever may take the stage that waits, and anyone who posted; it closes when the task ends. Every proposal has one too, in governance, opened with its pitch and closed by its decision. Open other threads with open_thread on a channel with a title, for any other topic; close_thread posts your summary to its channel.",
|
|
9338
9744
|
"- Route every lesson: about you, your craft, or the user, write it to memory/core.md in your home directory, and keep that file short by moving detail to memory/<topic>.md, your archive; a durable fact about a project's codebase or process goes through `write_knowledge` on that project; something the whole project should know now, post it to the project channel, and anything about one task, in the task's thread.",
|
|
9339
|
-
'- A procedure you have followed twice is a skill: write it to skills/<name>/SKILL.md in your home, frontmatter with `name` and a one-line `description` and then the steps, and your skills index lists it from the next turn. Propose it with kind "skill" when the whole society would use it.',
|
|
9745
|
+
'- A procedure you have followed twice is a skill: write it to skills/<name>/SKILL.md in your home, frontmatter with `name` and a one-line `description` and then the steps, and your skills index lists it from the next turn. Files the steps use, such as scripts/ and references/, go in the same folder, named in SKILL.md by paths relative to it. Propose it with kind "skill" when the whole society would use it.',
|
|
9746
|
+
"- To take a published skill for yourself, install it in a temporary folder and copy it into your home: in a folder from `mktemp -d`, `npx -y skills add <source> --skill <name> -a universal --copy -y` writes `.agents/skills/<name>/` and a lock file there, and you copy that skill folder to skills/<name>/ in your home, named in lowercase letters, digits, and hyphens. Never install one inside a worktree, where it would be committed into the project, or with -g, which writes to the runner's own home, shared by every citizen on it and listed in no skills index.",
|
|
9340
9747
|
"- Report memoryUpdated: true in the status object whenever you changed memory/core.md or a skill, so a warm session restarts with the new instructions.",
|
|
9341
9748
|
"- Other turns of yours may be running at the same time and share your home. Change a file there with your edit tool, which refuses a file changed since you read it, and never rewrite one whole from an earlier read, with a shell redirect or a script, or you erase what another of your turns wrote.",
|
|
9342
9749
|
"- To ask the user something, mention @user where the question belongs: the task's thread for a task, the proposal's thread for a proposal, and for anything else a thread you open on your scope's general channel, titled with the question. The user answers in the same thread, and the answer comes back to that thread's conversation. A proposal waiting on the user is already in front of them and needs no mention.",
|
|
@@ -9359,7 +9766,7 @@ var GOVERNANCE = [
|
|
|
9359
9766
|
' - retirement: {"agent", "reason"}',
|
|
9360
9767
|
' - 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',
|
|
9361
9768
|
' - reallocation: {"description"}',
|
|
9362
|
-
' - skill: {"name", "summary"
|
|
9769
|
+
' - skill: {"name", "summary"}: the folder skills/<name>/ in your home, SKILL.md and every file beside it, which the board takes as it stands when this turn ends into society/proposals/<id>/, where whoever decides reads it; a proposal whose folder has no SKILL.md then is rejected by the board. Approval publishes it under society/skills/<name>/, where every citizen\'s skills index lists it until the steward or the user removes it with `remove_skill`; ask the steward to remove one that is wrong or superseded. A proposal from before folders carries "body", its SKILL.md text alone',
|
|
9363
9770
|
"- 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.",
|
|
9364
9771
|
"- Prefer scaling an existing role over inventing one; a new role is justified by work a project needs that no existing role covers. A role that needs a tool the board lacks is an engineering task, not a hiring request.",
|
|
9365
9772
|
"- 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."
|
|
@@ -9387,7 +9794,7 @@ function renderSkillsIndex(skills) {
|
|
|
9387
9794
|
];
|
|
9388
9795
|
}
|
|
9389
9796
|
return [
|
|
9390
|
-
"Read a skill's file when its summary matches the work at hand; do not load them all.",
|
|
9797
|
+
"Read a skill's file when its summary matches the work at hand; do not load them all. Paths in it are relative to its folder, which holds whatever else it uses.",
|
|
9391
9798
|
"",
|
|
9392
9799
|
...skills.map((skill) => `- ${skill.name} (${skill.scope === "own" ? "yours" : "society"}): ${skill.summary.trim().length === 0 ? "no summary" : skill.summary.trim().replace(/\.+$/, "")}. File: ${skill.path}`)
|
|
9393
9800
|
];
|
|
@@ -9412,7 +9819,7 @@ var REFLECTION_INTRO = "This turn is for your memory; take no new work, and answ
|
|
|
9412
9819
|
var RECONCILE = "- Reconcile first: merge each copy listed under Edits to reconcile in your home into the file beside it, then delete the copy.";
|
|
9413
9820
|
var REFLECTION_STEPS = [
|
|
9414
9821
|
"- Consolidate memory/core.md: keep it to lessons that still hold, about sixty lines at most, and move detail worth keeping to memory/<topic>.md in your home, which the board's search covers for you alone.",
|
|
9415
|
-
'- Extract a skill: a procedure you have followed twice goes to skills/<name>/SKILL.md, frontmatter with `name` and a one-line `description` and then the steps. Propose it with kind "skill" when other citizens would use it.',
|
|
9822
|
+
'- Extract a skill: a procedure you have followed twice goes to skills/<name>/SKILL.md, frontmatter with `name` and a one-line `description` and then the steps, with any script or reference it uses in the same folder. Propose it with kind "skill" when other citizens would use it.',
|
|
9416
9823
|
"- Refresh profile.md: one paragraph on what you do well and what to send your way; the roster the front desk routes with is built from it.",
|
|
9417
9824
|
"- Share: a durable fact about this scope's codebase or process goes through write_knowledge; a norm the whole society should follow goes to the steward as a plain post in general.",
|
|
9418
9825
|
"- Report memoryUpdated: true in the status object when core.md or a skill changed."
|
|
@@ -9627,7 +10034,7 @@ function buildTurnPrompt(input) {
|
|
|
9627
10034
|
} else {
|
|
9628
10035
|
lines.push(`Topics under ${knowledge.dir}; open one with your file tools when it is relevant.`, "");
|
|
9629
10036
|
for (const topic of knowledge.topics) {
|
|
9630
|
-
lines.push(`- ${topic.topic}: updated by ${topic.updatedBy} at ${topic.updatedAt}`);
|
|
10037
|
+
lines.push(`- ${topic.topic}: revision ${topic.revision}, updated by ${topic.updatedBy} at ${topic.updatedAt}`);
|
|
9631
10038
|
}
|
|
9632
10039
|
}
|
|
9633
10040
|
}
|
|
@@ -10157,6 +10564,7 @@ var TurnHost = class {
|
|
|
10157
10564
|
channel: job.channel,
|
|
10158
10565
|
exitReason: finished.exitReason
|
|
10159
10566
|
}, "turn finished");
|
|
10567
|
+
await this.takeSkillFiles(finished);
|
|
10160
10568
|
turn.done(finished);
|
|
10161
10569
|
}
|
|
10162
10570
|
return { dropWorkspace: ended && (closing ? decided : !outcome.leftovers) };
|
|
@@ -10181,9 +10589,21 @@ var TurnHost = class {
|
|
|
10181
10589
|
};
|
|
10182
10590
|
if (await this.recorded(turn, () => this.board.finishTurn(finished, turn.transcript))) {
|
|
10183
10591
|
this.log.warn({ agent: turn.job.agent, project: turn.job.scope, reason }, "turn aborted");
|
|
10592
|
+
await this.takeSkillFiles(finished);
|
|
10184
10593
|
turn.done(finished);
|
|
10185
10594
|
}
|
|
10186
10595
|
}
|
|
10596
|
+
/**
|
|
10597
|
+
* Takes the folders of the skills the turn proposed, which reached the board with the home its
|
|
10598
|
+
* runner pushed before reporting. A failure only leaves them for approval to take.
|
|
10599
|
+
*/
|
|
10600
|
+
async takeSkillFiles(record) {
|
|
10601
|
+
try {
|
|
10602
|
+
await this.board.takeSkillFiles(record);
|
|
10603
|
+
} catch (error) {
|
|
10604
|
+
this.log.warn({ agent: record.agent, project: record.project, error: String(error) }, "could not take the folders of the skills a turn proposed");
|
|
10605
|
+
}
|
|
10606
|
+
}
|
|
10187
10607
|
/**
|
|
10188
10608
|
* Records the end of a turn already taken out of `open`, and says whether it could. The scheduler
|
|
10189
10609
|
* holds the turn's conversation until the turn ends, and nothing would end it once it has left
|
|
@@ -11553,7 +11973,7 @@ function runnerRoutes(board2, hub, enrollments, enrollLimit) {
|
|
|
11553
11973
|
}
|
|
11554
11974
|
|
|
11555
11975
|
// apps/server/dist/static.js
|
|
11556
|
-
import { readFile as
|
|
11976
|
+
import { readFile as readFile5, stat as stat4 } from "node:fs/promises";
|
|
11557
11977
|
import path8 from "node:path";
|
|
11558
11978
|
var CONTENT_TYPES = {
|
|
11559
11979
|
".html": "text/html; charset=utf-8",
|
|
@@ -11570,7 +11990,7 @@ var CONTENT_TYPES = {
|
|
|
11570
11990
|
};
|
|
11571
11991
|
async function isFile(file) {
|
|
11572
11992
|
try {
|
|
11573
|
-
return (await
|
|
11993
|
+
return (await stat4(file)).isFile();
|
|
11574
11994
|
} catch {
|
|
11575
11995
|
return false;
|
|
11576
11996
|
}
|
|
@@ -11595,7 +12015,7 @@ function spaHandler(staticDir) {
|
|
|
11595
12015
|
const file = within(root, pathname);
|
|
11596
12016
|
const headers = { "x-content-type-options": "nosniff" };
|
|
11597
12017
|
if (file !== null && await isFile(file)) {
|
|
11598
|
-
return c.body(await
|
|
12018
|
+
return c.body(await readFile5(file), 200, {
|
|
11599
12019
|
...headers,
|
|
11600
12020
|
"content-type": CONTENT_TYPES[path8.extname(file)] ?? "application/octet-stream",
|
|
11601
12021
|
"cache-control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache"
|
|
@@ -11608,7 +12028,7 @@ function spaHandler(staticDir) {
|
|
|
11608
12028
|
if (!await isFile(index)) {
|
|
11609
12029
|
return c.text("The interface is not built: run pnpm build:web.", 404);
|
|
11610
12030
|
}
|
|
11611
|
-
return c.body(await
|
|
12031
|
+
return c.body(await readFile5(index), 200, {
|
|
11612
12032
|
...headers,
|
|
11613
12033
|
"content-type": CONTENT_TYPES[".html"] ?? "text/html",
|
|
11614
12034
|
"cache-control": "no-cache"
|
|
@@ -11626,6 +12046,7 @@ var ERROR_STATUS = {
|
|
|
11626
12046
|
INVALID_TRANSITION: 409,
|
|
11627
12047
|
INVALID_STATE: 409
|
|
11628
12048
|
};
|
|
12049
|
+
var EVENT_PAGE = 100;
|
|
11629
12050
|
var RetireBodySchema = z16.object({ reason: z16.string().min(1) });
|
|
11630
12051
|
var RunnerBodySchema = z16.object({ name: NameSchema });
|
|
11631
12052
|
var EnrollmentApprovalSchema = z16.object({ name: NameSchema });
|
|
@@ -11633,6 +12054,7 @@ var HomeRunnerBodySchema = z16.object({ runner: NameSchema.nullable() });
|
|
|
11633
12054
|
var ModelBodySchema = z16.object({ model: ModelNameSchema.nullable() });
|
|
11634
12055
|
var EffortBodySchema = z16.object({ effort: EffortSchema.nullable() });
|
|
11635
12056
|
var HistoryLimitSchema = z16.coerce.number().int().min(1).max(500);
|
|
12057
|
+
var MessageIdsSchema = z16.string().transform((ids) => ids.split(",").filter((id) => id !== "")).pipe(z16.array(UlidSchema).min(1).max(MESSAGE_LOOKUP_LIMIT));
|
|
11636
12058
|
var TimezoneBodySchema = z16.object({ timezone: TimeZoneSchema });
|
|
11637
12059
|
var ChannelBodySchema = z16.object({
|
|
11638
12060
|
project: NameSchema.nullable().default(null),
|
|
@@ -11646,6 +12068,7 @@ function bearer2(c) {
|
|
|
11646
12068
|
}
|
|
11647
12069
|
function createApp(deps) {
|
|
11648
12070
|
const { board: board2, version, turns: turns2, scheduler: scheduler2, models } = deps;
|
|
12071
|
+
const eventStreamPingMs = deps.eventStreamPingMs ?? 15e3;
|
|
11649
12072
|
const app2 = new Hono3();
|
|
11650
12073
|
app2.get("/health", (c) => c.json({ ok: true, version }));
|
|
11651
12074
|
app2.onError((error, c) => {
|
|
@@ -11743,6 +12166,8 @@ function createApp(deps) {
|
|
|
11743
12166
|
api.get("/proposals", async (c) => c.json(await board2.listProposals()));
|
|
11744
12167
|
api.get("/crons", async (c) => c.json(await board2.listCrons()));
|
|
11745
12168
|
api.get("/proposals/:id", async (c) => c.json(await board2.readProposal(c.req.param("id"))));
|
|
12169
|
+
api.get("/messages", async (c) => c.json(await board2.findMessages(MessageIdsSchema.parse(c.req.query("ids") ?? ""))));
|
|
12170
|
+
api.get("/proposals/:id/files", async (c) => c.json(await board2.readSkillFiles(UlidSchema.parse(c.req.param("id")))));
|
|
11746
12171
|
api.get("/signals", async (c) => c.json(await board2.listSignals(Number(c.req.query("limit") ?? "100"))));
|
|
11747
12172
|
api.get("/metrics", async (c) => {
|
|
11748
12173
|
const window = MetricsWindowSchema.parse(c.req.query("window") ?? "7d");
|
|
@@ -11846,16 +12271,41 @@ function createApp(deps) {
|
|
|
11846
12271
|
api.get("/events/stream", (c) => {
|
|
11847
12272
|
let cursor = c.req.query("since") ?? null;
|
|
11848
12273
|
return streamSSE2(c, async (stream) => {
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
12274
|
+
let appended = false;
|
|
12275
|
+
let wake = null;
|
|
12276
|
+
const unsubscribe = board2.onEventAppended(() => {
|
|
12277
|
+
appended = true;
|
|
12278
|
+
wake?.();
|
|
12279
|
+
});
|
|
12280
|
+
stream.onAbort(() => wake?.());
|
|
12281
|
+
try {
|
|
12282
|
+
if (cursor === "latest") {
|
|
12283
|
+
cursor = await board2.latestEventId();
|
|
11857
12284
|
}
|
|
11858
|
-
|
|
12285
|
+
while (!stream.aborted && !stream.closed) {
|
|
12286
|
+
appended = false;
|
|
12287
|
+
const events = await board2.readEvents(cursor === "" ? null : cursor, EVENT_PAGE);
|
|
12288
|
+
for (const event of events) {
|
|
12289
|
+
await stream.writeSSE({ id: event.id, event: event.type, data: JSON.stringify(event) });
|
|
12290
|
+
cursor = event.id;
|
|
12291
|
+
}
|
|
12292
|
+
if (appended || events.length === EVENT_PAGE) {
|
|
12293
|
+
continue;
|
|
12294
|
+
}
|
|
12295
|
+
const woken = await new Promise((resolve) => {
|
|
12296
|
+
const timer = setTimeout(() => resolve(false), eventStreamPingMs);
|
|
12297
|
+
wake = () => {
|
|
12298
|
+
clearTimeout(timer);
|
|
12299
|
+
resolve(true);
|
|
12300
|
+
};
|
|
12301
|
+
});
|
|
12302
|
+
wake = null;
|
|
12303
|
+
if (!woken && !stream.aborted) {
|
|
12304
|
+
await stream.writeSSE({ event: "ping", data: "" });
|
|
12305
|
+
}
|
|
12306
|
+
}
|
|
12307
|
+
} finally {
|
|
12308
|
+
unsubscribe();
|
|
11859
12309
|
}
|
|
11860
12310
|
});
|
|
11861
12311
|
});
|