@kubitnodes/stellaris 0.3.17 → 0.3.19
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 +475 -117
- package/dist/server/index.js +466 -102
- package/package.json +1 -1
- package/web/dist/assets/{BackLink-CssfJ-vJ.js → BackLink-CDrsO9Ls.js} +1 -1
- package/web/dist/assets/{ChannelForms-B3XwkJj_.js → ChannelForms-BjD5Lp_u.js} +1 -1
- package/web/dist/assets/ChannelView-dI5EHpof.js +2 -0
- package/web/dist/assets/{CitizenView-BUeC52q2.js → CitizenView-tvC6dNUB.js} +2 -2
- package/web/dist/assets/{CitizensView-BSWYcQ-Q.js → CitizensView-DsUhIYtl.js} +1 -1
- package/web/dist/assets/{CliIcon-CFObB9XW.js → CliIcon-TlyIqh7V.js} +1 -1
- package/web/dist/assets/{CronsView-BBIx84f7.js → CronsView-BPlSyoAe.js} +1 -1
- package/web/dist/assets/KnowledgeView-CgRGZrOK.js +1 -0
- package/web/dist/assets/{Markdown-BO_bF8WD.js → Markdown-CePsfcUS.js} +4 -4
- package/web/dist/assets/MessageItem-kg5zWMDe.js +1 -0
- package/web/dist/assets/{MetricsView-DqAQ4th0.js → MetricsView-cJeDTfBE.js} +1 -1
- package/web/dist/assets/{NeedsYouView-D2Z-iLdT.js → NeedsYouView-Cd1TnY7W.js} +1 -1
- package/web/dist/assets/{Overview-DThLwb0l.js → Overview-B6aBmove.js} +1 -1
- package/web/dist/assets/{Pane-DHXhVwfI.js → Pane-DGtHsANK.js} +1 -1
- package/web/dist/assets/{Picker-CQQ7hj8T.js → Picker-Dq5Vvxod.js} +1 -1
- package/web/dist/assets/ProjectView-uskZHP8l.js +1 -0
- package/web/dist/assets/ProposalView-DmjIL53_.js +2 -0
- package/web/dist/assets/{ProposalsView-BJg4ZsTy.js → ProposalsView-BjEfHBtx.js} +1 -1
- package/web/dist/assets/{RunnersView-DOW8xq-J.js → RunnersView-CM-q_AX9.js} +1 -1
- package/web/dist/assets/SocietyView-DZuGEqIl.js +1 -0
- package/web/dist/assets/SubjectThread-pCRDn42X.js +1 -0
- package/web/dist/assets/{TaskFileView-CPwEmR6p.js → TaskFileView-BMAkbh-e.js} +2 -2
- package/web/dist/assets/{TaskView-q6Kn30K7.js → TaskView-HDwqXuYy.js} +2 -2
- package/web/dist/assets/{TasksView-CT9Cui_C.js → TasksView-Dt8M_z96.js} +1 -1
- package/web/dist/assets/ThreadForms-DbV3kPCi.js +1 -0
- package/web/dist/assets/ThreadItem-BVlz2w6q.js +1 -0
- package/web/dist/assets/{ThreadView-BZ2nKfyD.js → ThreadView-CpBytSH7.js} +1 -1
- package/web/dist/assets/{citizen-DFaT61S1.js → citizen-DQ0Bmk-r.js} +1 -1
- package/web/dist/assets/compose-Cx9c9k90.js +1 -0
- package/web/dist/assets/{format-Cp_C4ygr.js → format-SP8GnzAX.js} +1 -1
- package/web/dist/assets/governance-DX2ph-qr.js +1 -0
- package/web/dist/assets/index-BDKI0Rje.js +23 -0
- package/web/dist/assets/index-BPVeX6tV.css +2 -0
- package/web/dist/assets/{link-DIpiSYJj.js → link-DxSeFzFf.js} +1 -1
- package/web/dist/assets/{tasks-C1wf85BJ.js → tasks-BeWF0Xy2.js} +1 -1
- package/web/dist/assets/useMatch-HNekgJrF.js +1 -0
- package/web/dist/assets/{useStickyScroll-7ZZcyLI0.js → useStickyScroll-BmqddNx5.js} +1 -1
- package/web/dist/index.html +9 -9
- package/web/dist/assets/ChannelView-B3qNylz5.js +0 -2
- package/web/dist/assets/KnowledgeView-C8r5hAa_.js +0 -1
- package/web/dist/assets/MessageItem-BkDlfhQC.js +0 -1
- package/web/dist/assets/ProjectView-DOtPYF6z.js +0 -1
- package/web/dist/assets/ProposalView-BpeiCg46.js +0 -1
- package/web/dist/assets/SocietyView-CbIpCGhP.js +0 -1
- package/web/dist/assets/SubjectThread-Do5As1cO.js +0 -1
- package/web/dist/assets/ThreadCard-DsIkg4Br.js +0 -1
- package/web/dist/assets/ThreadForms-BPowHd4M.js +0 -1
- package/web/dist/assets/compose-96HAETM2.js +0 -1
- package/web/dist/assets/governance-BbogC_X-.js +0 -1
- package/web/dist/assets/index-BXHV32GA.js +0 -23
- package/web/dist/assets/index-BoTts74M.css +0 -2
- package/web/dist/assets/useMatch-DYOY33wl.js +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import path7 from "node:path";
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
|
|
8
8
|
// packages/board-core/dist/board.js
|
|
9
|
-
import { mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, stat as stat2 } from "node:fs/promises";
|
|
9
|
+
import { chmod as chmod2, cp, mkdir as mkdir4, readdir as readdir3, readFile as readFile5, rename as rename3, rm as rm2, stat as stat2, 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";
|
|
@@ -63,7 +63,8 @@ var VERB_NAMES = [
|
|
|
63
63
|
"update_dashboard",
|
|
64
64
|
"create_cron",
|
|
65
65
|
"update_cron",
|
|
66
|
-
"remove_cron"
|
|
66
|
+
"remove_cron",
|
|
67
|
+
"remove_skill"
|
|
67
68
|
];
|
|
68
69
|
var VerbNameSchema = z2.enum(VERB_NAMES);
|
|
69
70
|
var CharterTriggerSchema = z2.enum(["user_post", "ops_event", "heartbeat"]);
|
|
@@ -123,6 +124,7 @@ var PLANNING_VERBS = [
|
|
|
123
124
|
"archive_channel"
|
|
124
125
|
];
|
|
125
126
|
var ARCHIVE_VERBS = ["archive_project"];
|
|
127
|
+
var CURATING_VERBS = ["remove_skill"];
|
|
126
128
|
var SEED_ROLES = [
|
|
127
129
|
{
|
|
128
130
|
name: USER_ROLE,
|
|
@@ -132,7 +134,8 @@ var SEED_ROLES = [
|
|
|
132
134
|
...GOVERNANCE_VERBS,
|
|
133
135
|
...FRONT_DESK_VERBS,
|
|
134
136
|
...PLANNING_VERBS,
|
|
135
|
-
...ARCHIVE_VERBS
|
|
137
|
+
...ARCHIVE_VERBS,
|
|
138
|
+
...CURATING_VERBS
|
|
136
139
|
],
|
|
137
140
|
wakeTriggers: [],
|
|
138
141
|
maxReplicas: 1,
|
|
@@ -144,7 +147,7 @@ var SEED_ROLES = [
|
|
|
144
147
|
{
|
|
145
148
|
name: "steward",
|
|
146
149
|
purpose: "Watches the operations signals and the task board for capacity, skill, and capability gaps, and turns signals into proposals: a member when a project's plans need a role nobody fills or a backlog persists, a role when a project's work needs one no existing role covers, a retirement when a member has been idle for a long time. Opens a channel with create_channel when a topic or a workstream needs one, and archives it once its work is done. Owns how work is shaped over time: answers stages waiting too long, stages assigned to roles nobody fills, and work sent back again and again by replanning with plan_task, adjusting gates, or proposing the role a plan needs; sets each project's completion effect with configure_project; turns recurring plan shapes into society skills and planning norms into the society's norms. Prefers scaling an existing role over inventing a new one. Never approves its own proposals; hiring, roles, and retirements are decided by the user, and channels, reallocations, and skills the steward may decide. Curates society knowledge and skills.",
|
|
147
|
-
verbs: [...MEMBER_VERBS, ...GOVERNANCE_VERBS, ...PLANNING_VERBS],
|
|
150
|
+
verbs: [...MEMBER_VERBS, ...GOVERNANCE_VERBS, ...PLANNING_VERBS, ...CURATING_VERBS],
|
|
148
151
|
wakeTriggers: ["ops_event", "heartbeat"],
|
|
149
152
|
maxReplicas: 1,
|
|
150
153
|
backlogThreshold: 3,
|
|
@@ -619,7 +622,7 @@ var ArchiveProposalSchema = z4.object({
|
|
|
619
622
|
var SkillProposalSchema = z4.object({
|
|
620
623
|
name: NameSchema,
|
|
621
624
|
summary: z4.string().min(1).max(200),
|
|
622
|
-
body: z4.string().min(1)
|
|
625
|
+
body: z4.string().min(1).optional()
|
|
623
626
|
});
|
|
624
627
|
var ProposalCharterSchemas = {
|
|
625
628
|
role: RoleCharterSchema,
|
|
@@ -697,6 +700,12 @@ var SkillSchema = z4.object({
|
|
|
697
700
|
scope: z4.enum(["own", "society"]),
|
|
698
701
|
path: z4.string().min(1)
|
|
699
702
|
});
|
|
703
|
+
var RemovedSkillSchema = z4.object({
|
|
704
|
+
name: NameSchema,
|
|
705
|
+
reason: z4.string().min(1),
|
|
706
|
+
removedBy: NameSchema,
|
|
707
|
+
removedAt: IsoDateTimeSchema
|
|
708
|
+
});
|
|
700
709
|
var HomeConflictSchema = z4.object({
|
|
701
710
|
/** The copy, relative to the home. */
|
|
702
711
|
path: z4.string().min(1),
|
|
@@ -740,6 +749,7 @@ var HomeFileDiffSchema = HomeFileChangeSchema.pick({ path: true, status: true })
|
|
|
740
749
|
var KnowledgeSchema = z4.object({
|
|
741
750
|
topic: NameSchema,
|
|
742
751
|
project: NameSchema.nullable(),
|
|
752
|
+
revision: z4.number().int().min(0),
|
|
743
753
|
updatedBy: NameSchema,
|
|
744
754
|
updatedAt: IsoDateTimeSchema
|
|
745
755
|
});
|
|
@@ -762,6 +772,24 @@ var ProposalOriginSchema = z4.object({
|
|
|
762
772
|
thread: UlidSchema.optional(),
|
|
763
773
|
channel: NameSchema.optional()
|
|
764
774
|
});
|
|
775
|
+
var SKILL_FILES_LIMIT = 100;
|
|
776
|
+
var SKILL_BYTES_LIMIT = 10 * 1024 * 1024;
|
|
777
|
+
var SkillFileSchema = z4.object({
|
|
778
|
+
path: z4.string().min(1),
|
|
779
|
+
size: z4.number().int().nonnegative(),
|
|
780
|
+
executable: z4.boolean()
|
|
781
|
+
});
|
|
782
|
+
var SkillFilesSchema = z4.object({
|
|
783
|
+
/** The commit of the proposer's home the files came from. */
|
|
784
|
+
commit: CommitIdSchema,
|
|
785
|
+
takenAt: IsoDateTimeSchema,
|
|
786
|
+
files: z4.array(SkillFileSchema)
|
|
787
|
+
});
|
|
788
|
+
var SkillFileContentSchema = SkillFileSchema.extend({
|
|
789
|
+
text: z4.string().nullable(),
|
|
790
|
+
/** The text was cut at its size limit. */
|
|
791
|
+
truncated: z4.boolean()
|
|
792
|
+
});
|
|
765
793
|
var ProposalFrontmatterSchema = z4.object({
|
|
766
794
|
id: UlidSchema,
|
|
767
795
|
kind: ProposalKindSchema,
|
|
@@ -770,6 +798,8 @@ var ProposalFrontmatterSchema = z4.object({
|
|
|
770
798
|
createdAt: IsoDateTimeSchema,
|
|
771
799
|
/** Absent for a proposal filed outside a turn, or before proposals recorded it. */
|
|
772
800
|
filedFrom: ProposalOriginSchema.optional(),
|
|
801
|
+
/** A skill proposal's folder, once taken; absent for one proposed as text alone. */
|
|
802
|
+
skillFiles: SkillFilesSchema.optional(),
|
|
773
803
|
decidedBy: NameSchema.optional(),
|
|
774
804
|
decidedAt: IsoDateTimeSchema.optional(),
|
|
775
805
|
reason: z4.string().optional(),
|
|
@@ -872,7 +902,9 @@ var VerbInputs = {
|
|
|
872
902
|
/** A project you belong to, or null for society knowledge, which the steward and the user curate. */
|
|
873
903
|
project: NameSchema.nullable().default(null),
|
|
874
904
|
topic: NameSchema,
|
|
875
|
-
body: z5.string().min(1)
|
|
905
|
+
body: z5.string().min(1),
|
|
906
|
+
/** The `revision` in the frontmatter of the topic you read, 0 when it names none or for a new topic. */
|
|
907
|
+
revision: z5.number().int().min(0)
|
|
876
908
|
}),
|
|
877
909
|
remove_knowledge: z5.object({
|
|
878
910
|
/** As for write_knowledge: a project you belong to, or null for society knowledge. */
|
|
@@ -940,7 +972,8 @@ var VerbInputs = {
|
|
|
940
972
|
timezone: TimeZoneSchema.nullable().optional(),
|
|
941
973
|
at: MomentSchema.optional()
|
|
942
974
|
}),
|
|
943
|
-
remove_cron: z5.object({ cron_id: UlidSchema, reason: z5.string().trim().min(1) })
|
|
975
|
+
remove_cron: z5.object({ cron_id: UlidSchema, reason: z5.string().trim().min(1) }),
|
|
976
|
+
remove_skill: z5.object({ name: NameSchema, reason: z5.string().trim().min(1) })
|
|
944
977
|
};
|
|
945
978
|
|
|
946
979
|
// packages/shared/dist/events.js
|
|
@@ -972,6 +1005,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
972
1005
|
"proposal.created",
|
|
973
1006
|
"proposal.decided",
|
|
974
1007
|
"proposal.provisioned",
|
|
1008
|
+
"proposal.files_taken",
|
|
975
1009
|
"channel.added",
|
|
976
1010
|
"channel.archived",
|
|
977
1011
|
"role.added",
|
|
@@ -982,6 +1016,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
982
1016
|
"knowledge.removed",
|
|
983
1017
|
"dashboard.updated",
|
|
984
1018
|
"skill.promoted",
|
|
1019
|
+
"skill.removed",
|
|
985
1020
|
"runner.added",
|
|
986
1021
|
"user.signed_in",
|
|
987
1022
|
"user.signed_out",
|
|
@@ -1377,6 +1412,9 @@ var RunnerAnswerSchema = z7.discriminatedUnion("ok", [
|
|
|
1377
1412
|
]);
|
|
1378
1413
|
var WarmSessionsSchema = z7.object({ warm: z7.array(z7.string()) });
|
|
1379
1414
|
var FileManifestSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
|
|
1415
|
+
function fileVersion(sha, executable) {
|
|
1416
|
+
return executable ? `${sha}+x` : sha;
|
|
1417
|
+
}
|
|
1380
1418
|
var FileReadSchema = z7.object({ paths: z7.array(z7.string()) });
|
|
1381
1419
|
var FileContentsSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
|
|
1382
1420
|
function isSafeRelativePath(file) {
|
|
@@ -1695,7 +1733,7 @@ var RunnerConfigSchema = z11.object({
|
|
|
1695
1733
|
});
|
|
1696
1734
|
|
|
1697
1735
|
// packages/shared/dist/version.js
|
|
1698
|
-
var STELLARIS_VERSION = "0.3.
|
|
1736
|
+
var STELLARIS_VERSION = "0.3.19";
|
|
1699
1737
|
|
|
1700
1738
|
// packages/board-core/dist/errors.js
|
|
1701
1739
|
var BoardError = class extends Error {
|
|
@@ -1749,7 +1787,9 @@ async function readMarkdown(file, schema) {
|
|
|
1749
1787
|
return { data: schema.parse(parsed.data), body: parsed.content.replace(/^\n/, "") };
|
|
1750
1788
|
}
|
|
1751
1789
|
async function readLooseMarkdown(file) {
|
|
1752
|
-
|
|
1790
|
+
return parseLooseMarkdown(await readFile(file, "utf8"));
|
|
1791
|
+
}
|
|
1792
|
+
function parseLooseMarkdown(raw) {
|
|
1753
1793
|
try {
|
|
1754
1794
|
const parsed = parseMatter(raw);
|
|
1755
1795
|
return { data: parsed.data, body: parsed.content.replace(/^\n/, ""), error: null };
|
|
@@ -1936,13 +1976,14 @@ var FileTree = class {
|
|
|
1936
1976
|
if (info.size > MAX_FILE_BYTES) {
|
|
1937
1977
|
return null;
|
|
1938
1978
|
}
|
|
1979
|
+
const executable = (info.mode & 73) !== 0;
|
|
1939
1980
|
const known = this.hashes.get(absolute);
|
|
1940
1981
|
if (known !== void 0 && known.mtimeMs === info.mtimeMs && known.size === info.size) {
|
|
1941
|
-
return known.sha;
|
|
1982
|
+
return fileVersion(known.sha, executable);
|
|
1942
1983
|
}
|
|
1943
1984
|
const sha = createHash("sha256").update(await readFile3(absolute)).digest("hex");
|
|
1944
1985
|
this.hashes.set(absolute, { mtimeMs: info.mtimeMs, size: info.size, sha });
|
|
1945
|
-
return sha;
|
|
1986
|
+
return fileVersion(sha, executable);
|
|
1946
1987
|
} catch (error) {
|
|
1947
1988
|
if (isMissing2(error)) {
|
|
1948
1989
|
return null;
|
|
@@ -2193,6 +2234,55 @@ var HomeRepos = class {
|
|
|
2193
2234
|
}
|
|
2194
2235
|
return files;
|
|
2195
2236
|
}
|
|
2237
|
+
/**
|
|
2238
|
+
* The files of a folder in a home's newest commit, by path within the folder, with the commit;
|
|
2239
|
+
* conflict copies and anything but a regular file are left out. Null when the home is not a
|
|
2240
|
+
* repository yet.
|
|
2241
|
+
*/
|
|
2242
|
+
async folder(home, folder) {
|
|
2243
|
+
if (!await exists(path4.join(home, ".git"))) {
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
const commit = (await read(home, ["rev-parse", "--verify", "HEAD^{commit}"])).trim();
|
|
2247
|
+
const prefix = `${folder}/`;
|
|
2248
|
+
const listed = await read(home, [
|
|
2249
|
+
"ls-tree",
|
|
2250
|
+
"-r",
|
|
2251
|
+
"-z",
|
|
2252
|
+
"--long",
|
|
2253
|
+
"--full-tree",
|
|
2254
|
+
commit,
|
|
2255
|
+
prefix
|
|
2256
|
+
]);
|
|
2257
|
+
const files = [];
|
|
2258
|
+
for (const entry of listed.split("\0")) {
|
|
2259
|
+
const match = /^(100644|100755) blob ([0-9a-f]+) +(\d+)\t(.+)$/.exec(entry);
|
|
2260
|
+
const [, mode = "", object = "", size = "0", file = ""] = match ?? [];
|
|
2261
|
+
if (match === null || !file.startsWith(prefix) || !isSafeRelativePath(file) || conflictCopyOf(file) !== null) {
|
|
2262
|
+
continue;
|
|
2263
|
+
}
|
|
2264
|
+
files.push({
|
|
2265
|
+
path: file.slice(prefix.length),
|
|
2266
|
+
object,
|
|
2267
|
+
size: Number.parseInt(size, 10),
|
|
2268
|
+
executable: mode === "100755"
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
return { commit, files };
|
|
2272
|
+
}
|
|
2273
|
+
/** A blob of a home's repository, as `folder` names it. */
|
|
2274
|
+
async blob(home, object) {
|
|
2275
|
+
const result = await execa("git", ["cat-file", "blob", object], {
|
|
2276
|
+
cwd: home,
|
|
2277
|
+
encoding: "buffer",
|
|
2278
|
+
stripFinalNewline: false,
|
|
2279
|
+
reject: false
|
|
2280
|
+
});
|
|
2281
|
+
if (result.exitCode !== 0) {
|
|
2282
|
+
throw new Error(`git cat-file failed: ${Buffer.from(result.stderr).toString().trim()}`);
|
|
2283
|
+
}
|
|
2284
|
+
return Buffer.from(result.stdout);
|
|
2285
|
+
}
|
|
2196
2286
|
/** The conflict copies a merge added, against its first parent. */
|
|
2197
2287
|
async keptCopies(home, parent, commit) {
|
|
2198
2288
|
const added = await read(home, [
|
|
@@ -2608,6 +2698,10 @@ var BoardPaths = class {
|
|
|
2608
2698
|
proposal(id) {
|
|
2609
2699
|
return path5.join(this.proposals(), `${id}.md`);
|
|
2610
2700
|
}
|
|
2701
|
+
/** A skill proposal's folder, as the board took it from its proposer's home. */
|
|
2702
|
+
proposalFiles(id) {
|
|
2703
|
+
return path5.join(this.proposals(), id);
|
|
2704
|
+
}
|
|
2611
2705
|
decisions() {
|
|
2612
2706
|
return path5.join(this.society, "decisions");
|
|
2613
2707
|
}
|
|
@@ -2717,11 +2811,11 @@ var BoardPaths = class {
|
|
|
2717
2811
|
agentSkills(name) {
|
|
2718
2812
|
return path5.join(this.agent(name), "skills");
|
|
2719
2813
|
}
|
|
2720
|
-
agentSkill(name,
|
|
2721
|
-
return path5.join(this.agentSkills(name),
|
|
2814
|
+
agentSkill(name, skill2) {
|
|
2815
|
+
return path5.join(this.agentSkills(name), skill2);
|
|
2722
2816
|
}
|
|
2723
|
-
agentSkillFile(name,
|
|
2724
|
-
return path5.join(this.agentSkill(name,
|
|
2817
|
+
agentSkillFile(name, skill2) {
|
|
2818
|
+
return path5.join(this.agentSkill(name, skill2), "SKILL.md");
|
|
2725
2819
|
}
|
|
2726
2820
|
agentProjects(name) {
|
|
2727
2821
|
return path5.join(this.agent(name), "projects");
|
|
@@ -2755,6 +2849,10 @@ var BoardPaths = class {
|
|
|
2755
2849
|
removedKnowledge(scope) {
|
|
2756
2850
|
return path5.join(this.state(), "removed-knowledge", scope);
|
|
2757
2851
|
}
|
|
2852
|
+
/** Skills removed from the society, set aside outside the projection, one directory per removal. */
|
|
2853
|
+
removedSkills() {
|
|
2854
|
+
return path5.join(this.state(), "removed-skills");
|
|
2855
|
+
}
|
|
2758
2856
|
};
|
|
2759
2857
|
|
|
2760
2858
|
// packages/board-core/dist/tokens.js
|
|
@@ -2855,6 +2953,20 @@ var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
|
|
|
2855
2953
|
function withoutMentions(text) {
|
|
2856
2954
|
return text.replaceAll(MENTION_PATTERN, "$1$2");
|
|
2857
2955
|
}
|
|
2956
|
+
var SKILL_TEXT_LIMIT_CHARS = 1e5;
|
|
2957
|
+
function skillFolderOf(proposal2) {
|
|
2958
|
+
if (proposal2.kind !== "skill") {
|
|
2959
|
+
return null;
|
|
2960
|
+
}
|
|
2961
|
+
const parsed = SkillProposalSchema.safeParse(proposal2.charter);
|
|
2962
|
+
return parsed.success && parsed.data.body === void 0 ? parsed.data.name : null;
|
|
2963
|
+
}
|
|
2964
|
+
function proposedFrom(origin, where) {
|
|
2965
|
+
return origin !== void 0 && origin.scope === where.scope && origin.thread === where.thread && origin.channel === where.channel;
|
|
2966
|
+
}
|
|
2967
|
+
function megabytes(bytes) {
|
|
2968
|
+
return (bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, "");
|
|
2969
|
+
}
|
|
2858
2970
|
var CURATING_ROLES = [USER_ROLE, "steward"];
|
|
2859
2971
|
var SEED_INSTRUCTIONS_HEADING = "## Seed instructions";
|
|
2860
2972
|
var OPS_WAKE_TRIGGER = "ops_event";
|
|
@@ -3777,22 +3889,41 @@ ${charter.purpose}
|
|
|
3777
3889
|
const dir = project2 === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project2);
|
|
3778
3890
|
const topics = [];
|
|
3779
3891
|
for (const file of await listFiles(dir)) {
|
|
3780
|
-
|
|
3781
|
-
const parsed = KnowledgeSchema.safeParse({
|
|
3782
|
-
...doc.data,
|
|
3783
|
-
project: project2,
|
|
3784
|
-
topic: file.replace(/\.md$/, "")
|
|
3785
|
-
});
|
|
3786
|
-
const data = parsed.success ? parsed.data : {
|
|
3787
|
-
topic: file.replace(/\.md$/, ""),
|
|
3788
|
-
project: project2,
|
|
3789
|
-
updatedBy: SYSTEM_ACTOR.name,
|
|
3790
|
-
updatedAt: (await stat2(path6.join(dir, file))).mtime.toISOString()
|
|
3791
|
-
};
|
|
3792
|
-
topics.push({ ...data, body: doc.body });
|
|
3892
|
+
topics.push(await this.knowledgeAt(path6.join(dir, file), project2, file.replace(/\.md$/, "")));
|
|
3793
3893
|
}
|
|
3794
3894
|
return topics;
|
|
3795
3895
|
}
|
|
3896
|
+
knowledgeFile(project2, topic) {
|
|
3897
|
+
return project2 === null ? this.paths.societyKnowledgeFile(topic) : this.paths.projectKnowledgeFile(project2, topic);
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* Read loosely, as a dashboard is: a topic written before revisions counts as revision 0, and an
|
|
3901
|
+
* author or a time that does not parse is the board's at the file's mtime.
|
|
3902
|
+
*/
|
|
3903
|
+
async knowledgeAt(file, project2, topic) {
|
|
3904
|
+
const { data, body } = await readLooseMarkdown(file);
|
|
3905
|
+
const revision = z13.number().int().min(0).safeParse(data["revision"]);
|
|
3906
|
+
const updatedBy = NameSchema.safeParse(data["updatedBy"]);
|
|
3907
|
+
const updatedAt = IsoDateTimeSchema.safeParse(data["updatedAt"]);
|
|
3908
|
+
return {
|
|
3909
|
+
topic,
|
|
3910
|
+
project: project2,
|
|
3911
|
+
revision: revision.success ? revision.data : 0,
|
|
3912
|
+
updatedBy: updatedBy.success ? updatedBy.data : SYSTEM_ACTOR.name,
|
|
3913
|
+
updatedAt: updatedAt.success ? updatedAt.data : (await stat2(file)).mtime.toISOString(),
|
|
3914
|
+
body
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
/** The highest revision among a topic's removed copies, 0 when it has none. */
|
|
3918
|
+
async removedRevision(project2, topic) {
|
|
3919
|
+
const aside = this.paths.removedKnowledge(project2 ?? SOCIETY_SCOPE);
|
|
3920
|
+
let highest = 0;
|
|
3921
|
+
for (const file of (await listFiles(aside)).filter((name) => name.startsWith(`${topic}.`))) {
|
|
3922
|
+
const copy = await this.knowledgeAt(path6.join(aside, file), project2, topic);
|
|
3923
|
+
highest = Math.max(highest, copy.revision);
|
|
3924
|
+
}
|
|
3925
|
+
return highest;
|
|
3926
|
+
}
|
|
3796
3927
|
/** Skills promoted to the society: every citizen may read them. */
|
|
3797
3928
|
async listSocietySkills() {
|
|
3798
3929
|
return this.listSkillsIn(this.paths.societySkills(), "society");
|
|
@@ -4476,11 +4607,11 @@ ${task2.body}`;
|
|
|
4476
4607
|
file: path6.join(this.paths.societyKnowledge(), file)
|
|
4477
4608
|
});
|
|
4478
4609
|
}
|
|
4479
|
-
for (const
|
|
4480
|
-
shared.push({ kind: "skill", ref: `society/skills/${
|
|
4610
|
+
for (const skill2 of await this.listSocietySkills()) {
|
|
4611
|
+
shared.push({ kind: "skill", ref: `society/skills/${skill2.name}`, file: skill2.path });
|
|
4481
4612
|
}
|
|
4482
|
-
for (const
|
|
4483
|
-
shared.push({ kind: "skill", ref: `skills/${
|
|
4613
|
+
for (const skill2 of await this.listAgentSkills(actor.name)) {
|
|
4614
|
+
shared.push({ kind: "skill", ref: `skills/${skill2.name}`, file: skill2.path });
|
|
4484
4615
|
}
|
|
4485
4616
|
for (const file of await listFiles(this.paths.agentMemory(actor.name))) {
|
|
4486
4617
|
shared.push({
|
|
@@ -4506,26 +4637,42 @@ ${task2.body}`;
|
|
|
4506
4637
|
}
|
|
4507
4638
|
/**
|
|
4508
4639
|
* Writes a topic: a project's by its members, the society's by the curating roles. The note in
|
|
4509
|
-
* the matching general channel carries no mention, so nobody is woken.
|
|
4640
|
+
* the matching general channel carries no mention, so nobody is woken. A turn reads a topic from
|
|
4641
|
+
* the copy of the projection pulled when it started, so a revision other than the current one, 0
|
|
4642
|
+
* for a topic not there, means someone wrote or removed it since; the refusal carries the current
|
|
4643
|
+
* text, which that copy lacks.
|
|
4510
4644
|
*/
|
|
4511
4645
|
async writeKnowledge(actor, input) {
|
|
4512
4646
|
const args = VerbInputs.write_knowledge.parse(input);
|
|
4513
4647
|
await this.authorize(actor, "write_knowledge");
|
|
4514
4648
|
return this.mutex.run(async () => {
|
|
4515
4649
|
await this.assertMayCurate(actor, args.project);
|
|
4650
|
+
const file = this.knowledgeFile(args.project, args.topic);
|
|
4651
|
+
const current = await exists(file) ? await this.knowledgeAt(file, args.project, args.topic) : null;
|
|
4652
|
+
const place = args.project ?? "the society";
|
|
4653
|
+
if (current === null && args.revision !== 0) {
|
|
4654
|
+
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.`);
|
|
4655
|
+
}
|
|
4656
|
+
if (current !== null && args.revision !== current.revision) {
|
|
4657
|
+
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}.
|
|
4658
|
+
|
|
4659
|
+
${current.body}`);
|
|
4660
|
+
}
|
|
4661
|
+
const base = current?.revision ?? await this.removedRevision(args.project, args.topic);
|
|
4516
4662
|
const data = KnowledgeSchema.parse({
|
|
4517
4663
|
topic: args.topic,
|
|
4518
4664
|
project: args.project,
|
|
4665
|
+
revision: base + 1,
|
|
4519
4666
|
updatedBy: actor.name,
|
|
4520
4667
|
updatedAt: this.now().toISOString()
|
|
4521
4668
|
});
|
|
4522
|
-
const
|
|
4523
|
-
const replaced = await exists(file);
|
|
4669
|
+
const replaced = current !== null;
|
|
4524
4670
|
await writeMarkdown(file, data, args.body);
|
|
4525
4671
|
await this.events.append("knowledge.written", actor.name, {
|
|
4526
4672
|
topic: args.topic,
|
|
4527
4673
|
project: args.project,
|
|
4528
|
-
replaced
|
|
4674
|
+
replaced,
|
|
4675
|
+
revision: data.revision
|
|
4529
4676
|
});
|
|
4530
4677
|
const where = args.project === null ? "general" : channelRef(args.project, "general");
|
|
4531
4678
|
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" : ""}.`, {
|
|
@@ -4548,7 +4695,7 @@ ${task2.body}`;
|
|
|
4548
4695
|
await this.authorize(actor, "remove_knowledge");
|
|
4549
4696
|
return this.mutex.run(async () => {
|
|
4550
4697
|
await this.assertMayCurate(actor, args.project);
|
|
4551
|
-
const file =
|
|
4698
|
+
const file = this.knowledgeFile(args.project, args.topic);
|
|
4552
4699
|
if (!await exists(file)) {
|
|
4553
4700
|
throw new BoardError("NOT_FOUND", `${args.project ?? "the society"} has no knowledge topic ${args.topic}`);
|
|
4554
4701
|
}
|
|
@@ -4583,6 +4730,40 @@ ${task2.body}`;
|
|
|
4583
4730
|
throw new BoardError("FORBIDDEN", `${actor.name} is not a member of ${project2}`);
|
|
4584
4731
|
}
|
|
4585
4732
|
}
|
|
4733
|
+
/**
|
|
4734
|
+
* Takes a skill out of the society's skills, by the steward or the user. Its directory leaves
|
|
4735
|
+
* the projection, so no skills index or search lists it from the next turn, and is set aside
|
|
4736
|
+
* under `state/removed-skills` for the user to restore by hand. The notice is the board's, as a
|
|
4737
|
+
* promotion's is, so a removal by the user wakes no front desk, and quotes the reason without
|
|
4738
|
+
* its mentions. The proposer's own copy in its home is not touched.
|
|
4739
|
+
*/
|
|
4740
|
+
async removeSkill(actor, input) {
|
|
4741
|
+
const args = VerbInputs.remove_skill.parse(input);
|
|
4742
|
+
await this.authorize(actor, "remove_skill");
|
|
4743
|
+
if (!CURATING_ROLES.includes(actor.role)) {
|
|
4744
|
+
throw new BoardError("FORBIDDEN", "the society's skills are curated by the steward and the user");
|
|
4745
|
+
}
|
|
4746
|
+
return this.mutex.run(async () => {
|
|
4747
|
+
if (!await exists(this.paths.societySkillFile(args.name))) {
|
|
4748
|
+
throw new BoardError("NOT_FOUND", `the society has no skill ${args.name}`);
|
|
4749
|
+
}
|
|
4750
|
+
const removed = RemovedSkillSchema.parse({
|
|
4751
|
+
name: args.name,
|
|
4752
|
+
reason: args.reason,
|
|
4753
|
+
removedBy: actor.name,
|
|
4754
|
+
removedAt: this.now().toISOString()
|
|
4755
|
+
});
|
|
4756
|
+
const aside = this.paths.removedSkills();
|
|
4757
|
+
await mkdir4(aside, { recursive: true });
|
|
4758
|
+
await rename3(this.paths.societySkill(args.name), path6.join(aside, `${args.name}.${removed.removedAt.replaceAll(":", "-")}`));
|
|
4759
|
+
await this.events.append("skill.removed", actor.name, {
|
|
4760
|
+
name: args.name,
|
|
4761
|
+
reason: args.reason
|
|
4762
|
+
});
|
|
4763
|
+
await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${args.name} removed from the society by ${actor.name}: ${sentence(withoutMentions(args.reason))}. Every citizen's skills index stops listing it from its next turn.`);
|
|
4764
|
+
return removed;
|
|
4765
|
+
});
|
|
4766
|
+
}
|
|
4586
4767
|
/**
|
|
4587
4768
|
* Replaces a project's dashboard, by a member or the user. A turn reads the dashboard from the
|
|
4588
4769
|
* copy of the projection pulled when it started, so a revision other than the current one means
|
|
@@ -5565,7 +5746,7 @@ ${args.summary}`);
|
|
|
5565
5746
|
throw new BoardError("VALIDATION", `invalid ${args.kind} charter: ${parsed.error.message}`);
|
|
5566
5747
|
}
|
|
5567
5748
|
return this.mutex.run(async () => {
|
|
5568
|
-
|
|
5749
|
+
let proposal2 = {
|
|
5569
5750
|
...ProposalFrontmatterSchema.parse({
|
|
5570
5751
|
id: this.newId(),
|
|
5571
5752
|
kind: args.kind,
|
|
@@ -5584,6 +5765,14 @@ ${args.summary}`);
|
|
|
5584
5765
|
body: args.rationale
|
|
5585
5766
|
};
|
|
5586
5767
|
await this.validateProvision(proposal2.kind, proposal2.charter);
|
|
5768
|
+
const skill2 = skillFolderOf(proposal2);
|
|
5769
|
+
if (skill2 !== null && proposal2.filedFrom === void 0) {
|
|
5770
|
+
const taken = await this.takeSkillFolderUnlocked(proposal2, skill2);
|
|
5771
|
+
if (typeof taken === "string") {
|
|
5772
|
+
throw new BoardError("VALIDATION", taken);
|
|
5773
|
+
}
|
|
5774
|
+
proposal2 = { ...proposal2, skillFiles: taken };
|
|
5775
|
+
}
|
|
5587
5776
|
await this.writeProposal(proposal2);
|
|
5588
5777
|
await this.events.append("proposal.created", actor.name, {
|
|
5589
5778
|
proposalId: proposal2.id,
|
|
@@ -5805,6 +5994,8 @@ ${rationale}`}`);
|
|
|
5805
5994
|
return this.writeKnowledge(actor, VerbInputs.write_knowledge.parse(input));
|
|
5806
5995
|
case "remove_knowledge":
|
|
5807
5996
|
return this.removeKnowledge(actor, VerbInputs.remove_knowledge.parse(input));
|
|
5997
|
+
case "remove_skill":
|
|
5998
|
+
return this.removeSkill(actor, VerbInputs.remove_skill.parse(input));
|
|
5808
5999
|
case "plan_task":
|
|
5809
6000
|
return this.planTask(actor, VerbInputs.plan_task.parse(input));
|
|
5810
6001
|
case "advance_task":
|
|
@@ -6648,7 +6839,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
6648
6839
|
...agent2.model === void 0 ? {} : { model: agent2.model },
|
|
6649
6840
|
...lastModel === void 0 ? {} : { lastModel },
|
|
6650
6841
|
...agent2.effort === void 0 ? {} : { effort: agent2.effort },
|
|
6651
|
-
skills: (await this.listAgentSkills(name)).map((
|
|
6842
|
+
skills: (await this.listAgentSkills(name)).map((skill2) => skill2.name),
|
|
6652
6843
|
memberships: agent2.memberships,
|
|
6653
6844
|
subscriptions: agent2.subscriptions,
|
|
6654
6845
|
claimsHeld,
|
|
@@ -6797,9 +6988,9 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
6797
6988
|
return { agent: agent2.name, releasedTasks };
|
|
6798
6989
|
}
|
|
6799
6990
|
case "skill": {
|
|
6800
|
-
const
|
|
6801
|
-
const replaced = await this.promoteSkillUnlocked(by, proposal2
|
|
6802
|
-
return { skill:
|
|
6991
|
+
const skill2 = SkillProposalSchema.parse(proposal2.charter);
|
|
6992
|
+
const replaced = await this.promoteSkillUnlocked(by, proposal2, skill2, meta);
|
|
6993
|
+
return { skill: skill2.name, replaced };
|
|
6803
6994
|
}
|
|
6804
6995
|
case "archive": {
|
|
6805
6996
|
const input = ArchiveProposalSchema.parse(proposal2.charter);
|
|
@@ -6812,26 +7003,180 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
6812
7003
|
return void 0;
|
|
6813
7004
|
}
|
|
6814
7005
|
}
|
|
6815
|
-
/**
|
|
6816
|
-
|
|
6817
|
-
|
|
7006
|
+
/**
|
|
7007
|
+
* Publishes a promoted skill under the society's skills, where every citizen's skills index lists
|
|
7008
|
+
* it: the folder the board took from its proposer's home, or the text proposed alone. A
|
|
7009
|
+
* replacement is the whole skill, so files the new version dropped go with the old one.
|
|
7010
|
+
*/
|
|
7011
|
+
async promoteSkillUnlocked(by, proposal2, skill2, meta) {
|
|
7012
|
+
const dir = this.paths.societySkill(skill2.name);
|
|
7013
|
+
const file = this.paths.societySkillFile(skill2.name);
|
|
6818
7014
|
const replaced = await exists(file);
|
|
6819
|
-
|
|
7015
|
+
const taken = proposal2.skillFiles;
|
|
7016
|
+
const staged = path6.join(this.paths.proposalFiles(proposal2.id), "SKILL.md");
|
|
7017
|
+
if (skill2.body === void 0 && (taken === void 0 || !await exists(staged))) {
|
|
7018
|
+
throw new BoardError("INVALID_STATE", `proposal ${proposal2.id} holds no skill files`);
|
|
7019
|
+
}
|
|
7020
|
+
await rm2(dir, { recursive: true, force: true });
|
|
7021
|
+
if (skill2.body === void 0) {
|
|
7022
|
+
await cp(this.paths.proposalFiles(proposal2.id), dir, { recursive: true });
|
|
7023
|
+
} else {
|
|
7024
|
+
await ensureDir(dir);
|
|
7025
|
+
}
|
|
7026
|
+
const body = skill2.body ?? (await readLooseMarkdown(file)).body;
|
|
6820
7027
|
await writeMarkdown(file, {
|
|
6821
|
-
name:
|
|
6822
|
-
description:
|
|
6823
|
-
metadata: {
|
|
6824
|
-
|
|
7028
|
+
name: skill2.name,
|
|
7029
|
+
description: skill2.summary,
|
|
7030
|
+
metadata: {
|
|
7031
|
+
proposedBy: proposal2.proposedBy,
|
|
7032
|
+
promotedBy: by,
|
|
7033
|
+
promotedAt: this.now().toISOString(),
|
|
7034
|
+
...taken === void 0 ? {} : { commit: taken.commit }
|
|
7035
|
+
}
|
|
7036
|
+
}, body);
|
|
6825
7037
|
await this.events.append("skill.promoted", by, {
|
|
6826
|
-
name:
|
|
6827
|
-
summary:
|
|
6828
|
-
proposedBy,
|
|
7038
|
+
name: skill2.name,
|
|
7039
|
+
summary: skill2.summary,
|
|
7040
|
+
proposedBy: proposal2.proposedBy,
|
|
6829
7041
|
replaced,
|
|
7042
|
+
...taken === void 0 ? {} : { files: taken.files.length },
|
|
6830
7043
|
...meta
|
|
6831
7044
|
});
|
|
6832
|
-
await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${
|
|
7045
|
+
await this.appendMessage(SYSTEM_ACTOR.name, "general", `Skill ${skill2.name} ${replaced ? "updated in" : "promoted to"} the society: ${sentence(skill2.summary)}. Every citizen's skills index now lists it.`);
|
|
6833
7046
|
return replaced;
|
|
6834
7047
|
}
|
|
7048
|
+
/**
|
|
7049
|
+
* Copies a skill's folder from its proposer's home, as the home's newest commit holds it, into the
|
|
7050
|
+
* proposal's folder in the projection, where its deciders read it and from where approval
|
|
7051
|
+
* publishes it. Returns what was taken, or why the home could not give it.
|
|
7052
|
+
*/
|
|
7053
|
+
async takeSkillFolderUnlocked(proposal2, name) {
|
|
7054
|
+
const home = this.paths.agent(proposal2.proposedBy);
|
|
7055
|
+
const folder = `skills/${name}`;
|
|
7056
|
+
const where = `${folder} in ${proposal2.proposedBy}'s home`;
|
|
7057
|
+
const taken = await this.homes.folder(home, folder);
|
|
7058
|
+
if (taken === null || !taken.files.some((entry) => entry.path === "SKILL.md")) {
|
|
7059
|
+
return `no ${folder}/SKILL.md in ${proposal2.proposedBy}'s home`;
|
|
7060
|
+
}
|
|
7061
|
+
if (taken.files.length > SKILL_FILES_LIMIT) {
|
|
7062
|
+
return `${where} holds ${taken.files.length} files, more than the ${SKILL_FILES_LIMIT} a society skill may`;
|
|
7063
|
+
}
|
|
7064
|
+
const bytes = taken.files.reduce((sum, entry) => sum + entry.size, 0);
|
|
7065
|
+
if (bytes > SKILL_BYTES_LIMIT) {
|
|
7066
|
+
return `${where} holds ${megabytes(bytes)} MB, more than the ${megabytes(SKILL_BYTES_LIMIT)} MB a society skill may`;
|
|
7067
|
+
}
|
|
7068
|
+
const contents = /* @__PURE__ */ new Map();
|
|
7069
|
+
for (const entry of taken.files) {
|
|
7070
|
+
contents.set(entry.path, await this.homes.blob(home, entry.object));
|
|
7071
|
+
}
|
|
7072
|
+
const manual = parseLooseMarkdown(contents.get("SKILL.md")?.toString("utf8") ?? "");
|
|
7073
|
+
if (manual.error !== null) {
|
|
7074
|
+
return `the frontmatter of ${folder}/SKILL.md in ${proposal2.proposedBy}'s home does not parse (${manual.error})`;
|
|
7075
|
+
}
|
|
7076
|
+
const dir = this.paths.proposalFiles(proposal2.id);
|
|
7077
|
+
await rm2(dir, { recursive: true, force: true });
|
|
7078
|
+
for (const entry of taken.files) {
|
|
7079
|
+
const target = path6.join(dir, ...entry.path.split("/"));
|
|
7080
|
+
await ensureDir(path6.dirname(target));
|
|
7081
|
+
await writeFile4(target, contents.get(entry.path) ?? Buffer.alloc(0));
|
|
7082
|
+
await chmod2(target, entry.executable ? 493 : 420);
|
|
7083
|
+
}
|
|
7084
|
+
return {
|
|
7085
|
+
commit: taken.commit,
|
|
7086
|
+
takenAt: this.now().toISOString(),
|
|
7087
|
+
files: taken.files.map((entry) => ({
|
|
7088
|
+
path: entry.path,
|
|
7089
|
+
size: entry.size,
|
|
7090
|
+
executable: entry.executable
|
|
7091
|
+
}))
|
|
7092
|
+
};
|
|
7093
|
+
}
|
|
7094
|
+
/** Keeps what was taken of a skill proposal's folder on the proposal, and logs it. */
|
|
7095
|
+
async recordSkillFiles(proposal2, taken) {
|
|
7096
|
+
const next = { ...proposal2, skillFiles: taken };
|
|
7097
|
+
await this.writeProposal(next);
|
|
7098
|
+
await this.events.append("proposal.files_taken", SYSTEM_ACTOR.name, {
|
|
7099
|
+
proposalId: proposal2.id,
|
|
7100
|
+
kind: proposal2.kind,
|
|
7101
|
+
proposedBy: proposal2.proposedBy,
|
|
7102
|
+
commit: taken.commit,
|
|
7103
|
+
files: taken.files.length
|
|
7104
|
+
});
|
|
7105
|
+
return next;
|
|
7106
|
+
}
|
|
7107
|
+
/**
|
|
7108
|
+
* A skill proposal with its folder, for approval: taken now when the turn that filed it ended
|
|
7109
|
+
* without it, as when the server restarted before the turn's end was recorded, and refused while
|
|
7110
|
+
* that turn is still in flight, since the folder reaches the board only when it ends.
|
|
7111
|
+
*/
|
|
7112
|
+
async skillFilesForApproval(proposal2) {
|
|
7113
|
+
const name = skillFolderOf(proposal2);
|
|
7114
|
+
if (name === null || proposal2.skillFiles !== void 0) {
|
|
7115
|
+
return proposal2;
|
|
7116
|
+
}
|
|
7117
|
+
const cannot = `proposal ${proposal2.id} cannot be approved`;
|
|
7118
|
+
const filing = this.turnsInFlight().some((turn2) => turn2.agent === proposal2.proposedBy && proposedFrom(proposal2.filedFrom, turn2));
|
|
7119
|
+
if (filing) {
|
|
7120
|
+
throw new BoardError("INVALID_STATE", `${cannot} until ${proposal2.proposedBy}'s turn that proposed it ends, when the board takes the skill's folder from its home`);
|
|
7121
|
+
}
|
|
7122
|
+
const taken = await this.takeSkillFolderUnlocked(proposal2, name);
|
|
7123
|
+
if (typeof taken === "string") {
|
|
7124
|
+
throw new BoardError("INVALID_STATE", `${cannot}: ${taken}; reject it with that reason`);
|
|
7125
|
+
}
|
|
7126
|
+
return this.recordSkillFiles(proposal2, taken);
|
|
7127
|
+
}
|
|
7128
|
+
/**
|
|
7129
|
+
* Takes the folders of the skills a citizen proposed from a conversation, once the turn there has
|
|
7130
|
+
* ended and its runner has pushed the citizen's home. A proposal whose folder the home does not
|
|
7131
|
+
* give is rejected by the board with the reason, which wakes the proposer to put it right; while
|
|
7132
|
+
* someone is in a turn in the proposal's thread, it stays as it is, and approval meets the reason.
|
|
7133
|
+
*/
|
|
7134
|
+
async takeSkillFiles(turn2) {
|
|
7135
|
+
const where = {
|
|
7136
|
+
scope: turn2.project,
|
|
7137
|
+
...turn2.thread === void 0 ? {} : { thread: turn2.thread },
|
|
7138
|
+
...turn2.channel === void 0 ? {} : { channel: turn2.channel }
|
|
7139
|
+
};
|
|
7140
|
+
await this.mutex.run(async () => {
|
|
7141
|
+
for (const proposal2 of await this.listProposals()) {
|
|
7142
|
+
const name = skillFolderOf(proposal2);
|
|
7143
|
+
if (name === null || proposal2.status !== "proposed" || proposal2.skillFiles !== void 0 || proposal2.proposedBy !== turn2.agent || !proposedFrom(proposal2.filedFrom, where)) {
|
|
7144
|
+
continue;
|
|
7145
|
+
}
|
|
7146
|
+
const taken = await this.takeSkillFolderUnlocked(proposal2, name);
|
|
7147
|
+
if (typeof taken !== "string") {
|
|
7148
|
+
await this.recordSkillFiles(proposal2, taken);
|
|
7149
|
+
continue;
|
|
7150
|
+
}
|
|
7151
|
+
try {
|
|
7152
|
+
await this.decideUnlocked(SYSTEM_ACTOR, proposal2.id, "rejected", `${taken} when the turn that proposed it ended; propose it again once the folder is there`);
|
|
7153
|
+
} catch (error) {
|
|
7154
|
+
if (!(error instanceof BoardError)) {
|
|
7155
|
+
throw error;
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
7158
|
+
}
|
|
7159
|
+
});
|
|
7160
|
+
}
|
|
7161
|
+
/** A skill proposal's files with their text, as the board took them; none before then. */
|
|
7162
|
+
async readSkillFiles(proposalId) {
|
|
7163
|
+
const proposal2 = await this.readProposal(proposalId);
|
|
7164
|
+
const dir = this.paths.proposalFiles(proposal2.id);
|
|
7165
|
+
const files = [];
|
|
7166
|
+
for (const entry of proposal2.skillFiles?.files ?? []) {
|
|
7167
|
+
if (!isSafeRelativePath(entry.path)) {
|
|
7168
|
+
continue;
|
|
7169
|
+
}
|
|
7170
|
+
const bytes = await readFile5(path6.join(dir, ...entry.path.split("/"))).catch(() => null);
|
|
7171
|
+
const text = bytes === null || bytes.includes(0) ? null : bytes.toString("utf8");
|
|
7172
|
+
files.push({
|
|
7173
|
+
...entry,
|
|
7174
|
+
text: text === null ? null : text.slice(0, SKILL_TEXT_LIMIT_CHARS),
|
|
7175
|
+
truncated: text !== null && text.length > SKILL_TEXT_LIMIT_CHARS
|
|
7176
|
+
});
|
|
7177
|
+
}
|
|
7178
|
+
return files;
|
|
7179
|
+
}
|
|
6835
7180
|
async validateAddAgent(input) {
|
|
6836
7181
|
if (await exists(this.paths.agentFile(input.name))) {
|
|
6837
7182
|
throw new BoardError("ALREADY_EXISTS", `agent ${input.name} already exists`);
|
|
@@ -7343,67 +7688,70 @@ ${charter.purpose}
|
|
|
7343
7688
|
* retired. Validation runs before any write, so an impossible provision fails the decision.
|
|
7344
7689
|
*/
|
|
7345
7690
|
async decide(actor, proposalId, outcome, reason) {
|
|
7346
|
-
return this.mutex.run(
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
this.
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7691
|
+
return this.mutex.run(() => this.decideUnlocked(actor, proposalId, outcome, reason));
|
|
7692
|
+
}
|
|
7693
|
+
async decideUnlocked(actor, proposalId, outcome, reason) {
|
|
7694
|
+
const filed = await this.readProposal(proposalId);
|
|
7695
|
+
if (filed.status !== "proposed") {
|
|
7696
|
+
throw new BoardError("INVALID_STATE", `proposal ${proposalId} is already ${filed.status}`);
|
|
7697
|
+
}
|
|
7698
|
+
if (filed.proposedBy === actor.name) {
|
|
7699
|
+
throw new BoardError("FORBIDDEN", "a proposer never decides its own proposal");
|
|
7700
|
+
}
|
|
7701
|
+
const approvers = ROLE_KIND_APPROVERS[filed.kind];
|
|
7702
|
+
if (!approvers.includes(actor.role)) {
|
|
7703
|
+
throw new BoardError("FORBIDDEN", `a ${filed.kind} proposal requires one of: ${approvers.join(", ")}`);
|
|
7704
|
+
}
|
|
7705
|
+
if (outcome === "approved") {
|
|
7706
|
+
await this.validateProvision(filed.kind, filed.charter);
|
|
7707
|
+
}
|
|
7708
|
+
this.assertNoTurnCut(actor, `proposal ${proposalId} cannot be decided`, (turn2) => turn2.thread === proposalId);
|
|
7709
|
+
if (outcome === "approved" && filed.kind === "archive") {
|
|
7710
|
+
await this.assertProjectQuiet(actor, ArchiveProposalSchema.parse(filed.charter).project);
|
|
7711
|
+
}
|
|
7712
|
+
const proposal2 = outcome === "approved" ? await this.skillFilesForApproval(filed) : filed;
|
|
7713
|
+
const ts = this.now().toISOString();
|
|
7714
|
+
const decision = DecisionSchema.parse({
|
|
7715
|
+
id: this.newId(),
|
|
7716
|
+
proposalId,
|
|
7717
|
+
decidedBy: actor.name,
|
|
7718
|
+
outcome,
|
|
7719
|
+
...reason === void 0 ? {} : { reason },
|
|
7720
|
+
ts
|
|
7721
|
+
});
|
|
7722
|
+
await writeMarkdown(this.paths.decision(decision.id), decision, reason ?? "");
|
|
7723
|
+
const provision = outcome === "approved" ? await this.executeProvision(actor.name, proposal2) : void 0;
|
|
7724
|
+
const status = outcome === "rejected" ? "rejected" : provision === void 0 ? "approved" : "provisioned";
|
|
7725
|
+
await this.writeProposal({
|
|
7726
|
+
...proposal2,
|
|
7727
|
+
status,
|
|
7728
|
+
decidedBy: actor.name,
|
|
7729
|
+
decidedAt: ts,
|
|
7730
|
+
...reason === void 0 ? {} : { reason },
|
|
7731
|
+
...provision === void 0 ? {} : { provisionedAt: ts, provision }
|
|
7732
|
+
});
|
|
7733
|
+
await this.events.append("proposal.decided", actor.name, {
|
|
7734
|
+
proposalId,
|
|
7735
|
+
outcome,
|
|
7736
|
+
kind: proposal2.kind,
|
|
7737
|
+
proposedBy: proposal2.proposedBy,
|
|
7738
|
+
...proposal2.filedFrom === void 0 ? {} : { filedFrom: proposal2.filedFrom }
|
|
7739
|
+
});
|
|
7740
|
+
if (provision !== void 0) {
|
|
7741
|
+
await this.events.append("proposal.provisioned", actor.name, {
|
|
7386
7742
|
proposalId,
|
|
7387
|
-
outcome,
|
|
7388
7743
|
kind: proposal2.kind,
|
|
7389
|
-
|
|
7390
|
-
...proposal2.filedFrom === void 0 ? {} : { filedFrom: proposal2.filedFrom }
|
|
7744
|
+
...provision
|
|
7391
7745
|
});
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
|
|
7402
|
-
const thread2 = await this.proposalThreadUnlocked(actor.name, proposal2);
|
|
7403
|
-
await this.appendThreadMessage(actor.name, thread2.id, `${verdict}: ${describeCharter(proposal2.kind, proposal2.charter)}.${why}${result}`, { step: { action: outcome } });
|
|
7404
|
-
await this.endThreadOf(proposalId, actor.name, outcome);
|
|
7405
|
-
return decision;
|
|
7406
|
-
});
|
|
7746
|
+
}
|
|
7747
|
+
const verdict = outcome === "approved" ? "Approved" : "Rejected";
|
|
7748
|
+
const why = reason === void 0 ? "" : ` Reason: ${reason}.`;
|
|
7749
|
+
const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
|
|
7750
|
+
const thread2 = await this.proposalThreadUnlocked(actor.name, proposal2);
|
|
7751
|
+
const post = `${verdict}: ${describeCharter(proposal2.kind, proposal2.charter)}.${why}${result}`;
|
|
7752
|
+
await this.appendThreadMessage(actor.name, thread2.id, actor.name === SYSTEM_ACTOR.name ? withoutMentions(post) : post, { step: { action: outcome } });
|
|
7753
|
+
await this.endThreadOf(proposalId, actor.name, outcome);
|
|
7754
|
+
return decision;
|
|
7407
7755
|
}
|
|
7408
7756
|
/** The opener, the people of its subject, and anyone who posted in it; mentions reach the rest. */
|
|
7409
7757
|
async participatesInThread(actor, thread2, threads) {
|
|
@@ -7988,7 +8336,7 @@ var knowledge = program.command("knowledge").description("Shared knowledge, by p
|
|
|
7988
8336
|
knowledge.command("list [project]").description("List knowledge topics of a project, or of the society when no project is given").action(async (projectSlug) => {
|
|
7989
8337
|
const board = await open();
|
|
7990
8338
|
const topics = await board.listKnowledge(projectSlug ?? null);
|
|
7991
|
-
print(topics, () => topics.length === 0 ? "No knowledge yet." : topics.map((t) => `${t.topic} updated by ${t.updatedBy} at ${t.updatedAt}`).join("\n"));
|
|
8339
|
+
print(topics, () => topics.length === 0 ? "No knowledge yet." : topics.map((t) => `${t.topic} revision ${t.revision}, updated by ${t.updatedBy} at ${t.updatedAt}`).join("\n"));
|
|
7992
8340
|
});
|
|
7993
8341
|
knowledge.command("show <topic>").description("Print a knowledge topic").option("--project <slug>", "the project's topic instead of the society's").action(async (topic, opts) => {
|
|
7994
8342
|
const board = await open();
|
|
@@ -7998,17 +8346,19 @@ knowledge.command("show <topic>").description("Print a knowledge topic").option(
|
|
|
7998
8346
|
}
|
|
7999
8347
|
print(found, () => found.body);
|
|
8000
8348
|
});
|
|
8001
|
-
knowledge.command("write <topic>").description("Write a knowledge topic as the user, from a file or standard input").option("--project <slug>", "the project's topic instead of the society's").option("--file <path>", "read the body from this file instead of standard input").option("--as <agent>", "act as this agent instead of the user").action(async (topic, opts) => {
|
|
8349
|
+
knowledge.command("write <topic>").description("Write a knowledge topic as the user, from a file or standard input").requiredOption("--revision <n>", "the revision it replaces, as knowledge list shows it; 0 for a new topic").option("--project <slug>", "the project's topic instead of the society's").option("--file <path>", "read the body from this file instead of standard input").option("--as <agent>", "act as this agent instead of the user").action(async (topic, opts) => {
|
|
8002
8350
|
const board = await open();
|
|
8003
8351
|
const body = opts.file === void 0 ? await readStdin() : await readFile6(path7.resolve(opts.file), "utf8");
|
|
8004
8352
|
const written = await board.writeKnowledge(await actorFor(board, opts.as), {
|
|
8005
8353
|
project: opts.project ?? null,
|
|
8006
8354
|
topic,
|
|
8007
|
-
body
|
|
8355
|
+
body,
|
|
8356
|
+
revision: Number(opts.revision)
|
|
8008
8357
|
});
|
|
8009
|
-
print(written, () => `Wrote ${written.topic} for ${written.project ?? "the society"}`);
|
|
8358
|
+
print(written, () => `Wrote ${written.topic} for ${written.project ?? "the society"} at revision ${written.revision}`);
|
|
8010
8359
|
});
|
|
8011
|
-
program.command("skill").description("Skills promoted to the society")
|
|
8360
|
+
var skill = program.command("skill").description("Skills promoted to the society");
|
|
8361
|
+
skill.command("list").description("List the society's skills, and each member's own").action(async () => {
|
|
8012
8362
|
const board = await open();
|
|
8013
8363
|
const society = await board.listSocietySkills();
|
|
8014
8364
|
const own = [];
|
|
@@ -8023,6 +8373,14 @@ program.command("skill").description("Skills promoted to the society").command("
|
|
|
8023
8373
|
...own.flatMap(({ agent: name, skills }) => skills.map((s) => `${name} ${s.name} ${s.summary}`))
|
|
8024
8374
|
].join("\n") || "No skills yet.");
|
|
8025
8375
|
});
|
|
8376
|
+
skill.command("remove <name>").description("Remove a skill from the society's skills; its text is set aside, not destroyed").requiredOption("--reason <text>", "why it is wrong, superseded, or no longer used").option("--as <agent>", "act as this agent instead of the user").action(async (name, opts) => {
|
|
8377
|
+
const board = await open();
|
|
8378
|
+
const removed = await board.removeSkill(await actorFor(board, opts.as), {
|
|
8379
|
+
name,
|
|
8380
|
+
reason: opts.reason
|
|
8381
|
+
});
|
|
8382
|
+
print(removed, () => `Removed ${removed.name} from the society's skills: ${removed.reason}`);
|
|
8383
|
+
});
|
|
8026
8384
|
var turn = program.command("turn").description("Turn operations");
|
|
8027
8385
|
turn.command("digest <agent>").description("Show the unread messages the agent's next turn opens with, in one scope or all; the cursors stay put").option("--limit <n>", "maximum messages", "50").option("--project <slug>", "the scope whose turn to show: a project, or society").action(async (agentName, opts) => {
|
|
8028
8386
|
const board = await open();
|