@kubitnodes/stellaris 0.3.18 → 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 +393 -95
- package/dist/server/index.js +409 -97
- package/package.json +1 -1
- package/web/dist/assets/BackLink-CDrsO9Ls.js +1 -0
- package/web/dist/assets/{ChannelForms-DfG4vYvk.js → ChannelForms-BjD5Lp_u.js} +1 -1
- package/web/dist/assets/ChannelView-dI5EHpof.js +2 -0
- package/web/dist/assets/{CitizenView-q24CExsI.js → CitizenView-tvC6dNUB.js} +2 -2
- package/web/dist/assets/{CitizensView-bKbUaCab.js → CitizensView-DsUhIYtl.js} +1 -1
- package/web/dist/assets/{CliIcon-DTVk0pGU.js → CliIcon-TlyIqh7V.js} +1 -1
- package/web/dist/assets/{CronsView-DaG1xhQu.js → CronsView-BPlSyoAe.js} +1 -1
- package/web/dist/assets/{KnowledgeView-Dt7wE3FT.js → KnowledgeView-CgRGZrOK.js} +1 -1
- package/web/dist/assets/{Markdown-DeF9PKNm.js → Markdown-CePsfcUS.js} +3 -3
- package/web/dist/assets/MessageItem-kg5zWMDe.js +1 -0
- package/web/dist/assets/MetricsView-cJeDTfBE.js +1 -0
- package/web/dist/assets/NeedsYouView-Cd1TnY7W.js +1 -0
- package/web/dist/assets/Overview-B6aBmove.js +1 -0
- package/web/dist/assets/{Pane-v044FaP5.js → Pane-DGtHsANK.js} +1 -1
- package/web/dist/assets/{Picker-wWsO86nu.js → Picker-Dq5Vvxod.js} +1 -1
- package/web/dist/assets/{ProjectView--nzlqSDA.js → ProjectView-uskZHP8l.js} +1 -1
- package/web/dist/assets/ProposalView-DmjIL53_.js +2 -0
- package/web/dist/assets/{ProposalsView-dVA50yG1.js → ProposalsView-BjEfHBtx.js} +1 -1
- package/web/dist/assets/{RunnersView-C-l5QYIa.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-BMAkbh-e.js +4 -0
- package/web/dist/assets/{TaskView-BgJcOmGj.js → TaskView-HDwqXuYy.js} +2 -2
- package/web/dist/assets/{TasksView-f8KIsif_.js → TasksView-Dt8M_z96.js} +1 -1
- package/web/dist/assets/{ThreadForms-CXsunS-N.js → ThreadForms-DbV3kPCi.js} +1 -1
- package/web/dist/assets/ThreadItem-BVlz2w6q.js +1 -0
- package/web/dist/assets/{ThreadView-DzpaXkik.js → ThreadView-CpBytSH7.js} +1 -1
- package/web/dist/assets/{citizen-VtLNxjtf.js → citizen-DQ0Bmk-r.js} +1 -1
- package/web/dist/assets/{compose-D-SO-HE-.js → compose-Cx9c9k90.js} +1 -1
- package/web/dist/assets/{format-CkySAdH1.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-BN04dcBX.js → link-DxSeFzFf.js} +1 -1
- package/web/dist/assets/{tasks-CD70eybR.js → tasks-BeWF0Xy2.js} +1 -1
- package/web/dist/assets/useMatch-HNekgJrF.js +1 -0
- package/web/dist/assets/{useStickyScroll-WSVpAMrA.js → useStickyScroll-BmqddNx5.js} +1 -1
- package/web/dist/index.html +9 -9
- package/web/dist/assets/BackLink-DUOMS3fv.js +0 -1
- package/web/dist/assets/ChannelView-DTLhLXy4.js +0 -2
- package/web/dist/assets/MessageItem-DDs7m3-4.js +0 -1
- package/web/dist/assets/MetricsView-Bw7nL6Y9.js +0 -1
- package/web/dist/assets/NeedsYouView-DbESNtQT.js +0 -1
- package/web/dist/assets/Overview-Dv7SS4yW.js +0 -1
- package/web/dist/assets/ProposalView-Cdw_xzG1.js +0 -1
- package/web/dist/assets/SocietyView--mWRXRj_.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/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/useMatch-i33QT_GI.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";
|
|
@@ -622,7 +622,7 @@ var ArchiveProposalSchema = z4.object({
|
|
|
622
622
|
var SkillProposalSchema = z4.object({
|
|
623
623
|
name: NameSchema,
|
|
624
624
|
summary: z4.string().min(1).max(200),
|
|
625
|
-
body: z4.string().min(1)
|
|
625
|
+
body: z4.string().min(1).optional()
|
|
626
626
|
});
|
|
627
627
|
var ProposalCharterSchemas = {
|
|
628
628
|
role: RoleCharterSchema,
|
|
@@ -749,6 +749,7 @@ var HomeFileDiffSchema = HomeFileChangeSchema.pick({ path: true, status: true })
|
|
|
749
749
|
var KnowledgeSchema = z4.object({
|
|
750
750
|
topic: NameSchema,
|
|
751
751
|
project: NameSchema.nullable(),
|
|
752
|
+
revision: z4.number().int().min(0),
|
|
752
753
|
updatedBy: NameSchema,
|
|
753
754
|
updatedAt: IsoDateTimeSchema
|
|
754
755
|
});
|
|
@@ -771,6 +772,24 @@ var ProposalOriginSchema = z4.object({
|
|
|
771
772
|
thread: UlidSchema.optional(),
|
|
772
773
|
channel: NameSchema.optional()
|
|
773
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
|
+
});
|
|
774
793
|
var ProposalFrontmatterSchema = z4.object({
|
|
775
794
|
id: UlidSchema,
|
|
776
795
|
kind: ProposalKindSchema,
|
|
@@ -779,6 +798,8 @@ var ProposalFrontmatterSchema = z4.object({
|
|
|
779
798
|
createdAt: IsoDateTimeSchema,
|
|
780
799
|
/** Absent for a proposal filed outside a turn, or before proposals recorded it. */
|
|
781
800
|
filedFrom: ProposalOriginSchema.optional(),
|
|
801
|
+
/** A skill proposal's folder, once taken; absent for one proposed as text alone. */
|
|
802
|
+
skillFiles: SkillFilesSchema.optional(),
|
|
782
803
|
decidedBy: NameSchema.optional(),
|
|
783
804
|
decidedAt: IsoDateTimeSchema.optional(),
|
|
784
805
|
reason: z4.string().optional(),
|
|
@@ -881,7 +902,9 @@ var VerbInputs = {
|
|
|
881
902
|
/** A project you belong to, or null for society knowledge, which the steward and the user curate. */
|
|
882
903
|
project: NameSchema.nullable().default(null),
|
|
883
904
|
topic: NameSchema,
|
|
884
|
-
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)
|
|
885
908
|
}),
|
|
886
909
|
remove_knowledge: z5.object({
|
|
887
910
|
/** As for write_knowledge: a project you belong to, or null for society knowledge. */
|
|
@@ -982,6 +1005,7 @@ var BOARD_EVENT_TYPES = [
|
|
|
982
1005
|
"proposal.created",
|
|
983
1006
|
"proposal.decided",
|
|
984
1007
|
"proposal.provisioned",
|
|
1008
|
+
"proposal.files_taken",
|
|
985
1009
|
"channel.added",
|
|
986
1010
|
"channel.archived",
|
|
987
1011
|
"role.added",
|
|
@@ -1388,6 +1412,9 @@ var RunnerAnswerSchema = z7.discriminatedUnion("ok", [
|
|
|
1388
1412
|
]);
|
|
1389
1413
|
var WarmSessionsSchema = z7.object({ warm: z7.array(z7.string()) });
|
|
1390
1414
|
var FileManifestSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
|
|
1415
|
+
function fileVersion(sha, executable) {
|
|
1416
|
+
return executable ? `${sha}+x` : sha;
|
|
1417
|
+
}
|
|
1391
1418
|
var FileReadSchema = z7.object({ paths: z7.array(z7.string()) });
|
|
1392
1419
|
var FileContentsSchema = z7.object({ files: z7.record(z7.string(), z7.string()) });
|
|
1393
1420
|
function isSafeRelativePath(file) {
|
|
@@ -1706,7 +1733,7 @@ var RunnerConfigSchema = z11.object({
|
|
|
1706
1733
|
});
|
|
1707
1734
|
|
|
1708
1735
|
// packages/shared/dist/version.js
|
|
1709
|
-
var STELLARIS_VERSION = "0.3.
|
|
1736
|
+
var STELLARIS_VERSION = "0.3.19";
|
|
1710
1737
|
|
|
1711
1738
|
// packages/board-core/dist/errors.js
|
|
1712
1739
|
var BoardError = class extends Error {
|
|
@@ -1760,7 +1787,9 @@ async function readMarkdown(file, schema) {
|
|
|
1760
1787
|
return { data: schema.parse(parsed.data), body: parsed.content.replace(/^\n/, "") };
|
|
1761
1788
|
}
|
|
1762
1789
|
async function readLooseMarkdown(file) {
|
|
1763
|
-
|
|
1790
|
+
return parseLooseMarkdown(await readFile(file, "utf8"));
|
|
1791
|
+
}
|
|
1792
|
+
function parseLooseMarkdown(raw) {
|
|
1764
1793
|
try {
|
|
1765
1794
|
const parsed = parseMatter(raw);
|
|
1766
1795
|
return { data: parsed.data, body: parsed.content.replace(/^\n/, ""), error: null };
|
|
@@ -1947,13 +1976,14 @@ var FileTree = class {
|
|
|
1947
1976
|
if (info.size > MAX_FILE_BYTES) {
|
|
1948
1977
|
return null;
|
|
1949
1978
|
}
|
|
1979
|
+
const executable = (info.mode & 73) !== 0;
|
|
1950
1980
|
const known = this.hashes.get(absolute);
|
|
1951
1981
|
if (known !== void 0 && known.mtimeMs === info.mtimeMs && known.size === info.size) {
|
|
1952
|
-
return known.sha;
|
|
1982
|
+
return fileVersion(known.sha, executable);
|
|
1953
1983
|
}
|
|
1954
1984
|
const sha = createHash("sha256").update(await readFile3(absolute)).digest("hex");
|
|
1955
1985
|
this.hashes.set(absolute, { mtimeMs: info.mtimeMs, size: info.size, sha });
|
|
1956
|
-
return sha;
|
|
1986
|
+
return fileVersion(sha, executable);
|
|
1957
1987
|
} catch (error) {
|
|
1958
1988
|
if (isMissing2(error)) {
|
|
1959
1989
|
return null;
|
|
@@ -2204,6 +2234,55 @@ var HomeRepos = class {
|
|
|
2204
2234
|
}
|
|
2205
2235
|
return files;
|
|
2206
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
|
+
}
|
|
2207
2286
|
/** The conflict copies a merge added, against its first parent. */
|
|
2208
2287
|
async keptCopies(home, parent, commit) {
|
|
2209
2288
|
const added = await read(home, [
|
|
@@ -2619,6 +2698,10 @@ var BoardPaths = class {
|
|
|
2619
2698
|
proposal(id) {
|
|
2620
2699
|
return path5.join(this.proposals(), `${id}.md`);
|
|
2621
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
|
+
}
|
|
2622
2705
|
decisions() {
|
|
2623
2706
|
return path5.join(this.society, "decisions");
|
|
2624
2707
|
}
|
|
@@ -2870,6 +2953,20 @@ var MENTION_PATTERN = /(^|[^\w@])@([a-z0-9][a-z0-9-]{0,31})(?![\w-])/g;
|
|
|
2870
2953
|
function withoutMentions(text) {
|
|
2871
2954
|
return text.replaceAll(MENTION_PATTERN, "$1$2");
|
|
2872
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
|
+
}
|
|
2873
2970
|
var CURATING_ROLES = [USER_ROLE, "steward"];
|
|
2874
2971
|
var SEED_INSTRUCTIONS_HEADING = "## Seed instructions";
|
|
2875
2972
|
var OPS_WAKE_TRIGGER = "ops_event";
|
|
@@ -3792,22 +3889,41 @@ ${charter.purpose}
|
|
|
3792
3889
|
const dir = project2 === null ? this.paths.societyKnowledge() : this.paths.projectKnowledge(project2);
|
|
3793
3890
|
const topics = [];
|
|
3794
3891
|
for (const file of await listFiles(dir)) {
|
|
3795
|
-
|
|
3796
|
-
const parsed = KnowledgeSchema.safeParse({
|
|
3797
|
-
...doc.data,
|
|
3798
|
-
project: project2,
|
|
3799
|
-
topic: file.replace(/\.md$/, "")
|
|
3800
|
-
});
|
|
3801
|
-
const data = parsed.success ? parsed.data : {
|
|
3802
|
-
topic: file.replace(/\.md$/, ""),
|
|
3803
|
-
project: project2,
|
|
3804
|
-
updatedBy: SYSTEM_ACTOR.name,
|
|
3805
|
-
updatedAt: (await stat2(path6.join(dir, file))).mtime.toISOString()
|
|
3806
|
-
};
|
|
3807
|
-
topics.push({ ...data, body: doc.body });
|
|
3892
|
+
topics.push(await this.knowledgeAt(path6.join(dir, file), project2, file.replace(/\.md$/, "")));
|
|
3808
3893
|
}
|
|
3809
3894
|
return topics;
|
|
3810
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
|
+
}
|
|
3811
3927
|
/** Skills promoted to the society: every citizen may read them. */
|
|
3812
3928
|
async listSocietySkills() {
|
|
3813
3929
|
return this.listSkillsIn(this.paths.societySkills(), "society");
|
|
@@ -4521,26 +4637,42 @@ ${task2.body}`;
|
|
|
4521
4637
|
}
|
|
4522
4638
|
/**
|
|
4523
4639
|
* Writes a topic: a project's by its members, the society's by the curating roles. The note in
|
|
4524
|
-
* 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.
|
|
4525
4644
|
*/
|
|
4526
4645
|
async writeKnowledge(actor, input) {
|
|
4527
4646
|
const args = VerbInputs.write_knowledge.parse(input);
|
|
4528
4647
|
await this.authorize(actor, "write_knowledge");
|
|
4529
4648
|
return this.mutex.run(async () => {
|
|
4530
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);
|
|
4531
4662
|
const data = KnowledgeSchema.parse({
|
|
4532
4663
|
topic: args.topic,
|
|
4533
4664
|
project: args.project,
|
|
4665
|
+
revision: base + 1,
|
|
4534
4666
|
updatedBy: actor.name,
|
|
4535
4667
|
updatedAt: this.now().toISOString()
|
|
4536
4668
|
});
|
|
4537
|
-
const
|
|
4538
|
-
const replaced = await exists(file);
|
|
4669
|
+
const replaced = current !== null;
|
|
4539
4670
|
await writeMarkdown(file, data, args.body);
|
|
4540
4671
|
await this.events.append("knowledge.written", actor.name, {
|
|
4541
4672
|
topic: args.topic,
|
|
4542
4673
|
project: args.project,
|
|
4543
|
-
replaced
|
|
4674
|
+
replaced,
|
|
4675
|
+
revision: data.revision
|
|
4544
4676
|
});
|
|
4545
4677
|
const where = args.project === null ? "general" : channelRef(args.project, "general");
|
|
4546
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" : ""}.`, {
|
|
@@ -4563,7 +4695,7 @@ ${task2.body}`;
|
|
|
4563
4695
|
await this.authorize(actor, "remove_knowledge");
|
|
4564
4696
|
return this.mutex.run(async () => {
|
|
4565
4697
|
await this.assertMayCurate(actor, args.project);
|
|
4566
|
-
const file =
|
|
4698
|
+
const file = this.knowledgeFile(args.project, args.topic);
|
|
4567
4699
|
if (!await exists(file)) {
|
|
4568
4700
|
throw new BoardError("NOT_FOUND", `${args.project ?? "the society"} has no knowledge topic ${args.topic}`);
|
|
4569
4701
|
}
|
|
@@ -5614,7 +5746,7 @@ ${args.summary}`);
|
|
|
5614
5746
|
throw new BoardError("VALIDATION", `invalid ${args.kind} charter: ${parsed.error.message}`);
|
|
5615
5747
|
}
|
|
5616
5748
|
return this.mutex.run(async () => {
|
|
5617
|
-
|
|
5749
|
+
let proposal2 = {
|
|
5618
5750
|
...ProposalFrontmatterSchema.parse({
|
|
5619
5751
|
id: this.newId(),
|
|
5620
5752
|
kind: args.kind,
|
|
@@ -5633,6 +5765,14 @@ ${args.summary}`);
|
|
|
5633
5765
|
body: args.rationale
|
|
5634
5766
|
};
|
|
5635
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
|
+
}
|
|
5636
5776
|
await this.writeProposal(proposal2);
|
|
5637
5777
|
await this.events.append("proposal.created", actor.name, {
|
|
5638
5778
|
proposalId: proposal2.id,
|
|
@@ -6849,7 +6989,7 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
6849
6989
|
}
|
|
6850
6990
|
case "skill": {
|
|
6851
6991
|
const skill2 = SkillProposalSchema.parse(proposal2.charter);
|
|
6852
|
-
const replaced = await this.promoteSkillUnlocked(by, proposal2
|
|
6992
|
+
const replaced = await this.promoteSkillUnlocked(by, proposal2, skill2, meta);
|
|
6853
6993
|
return { skill: skill2.name, replaced };
|
|
6854
6994
|
}
|
|
6855
6995
|
case "archive": {
|
|
@@ -6863,26 +7003,180 @@ Short, curated, loaded on every turn. Keep entries that change future behavior a
|
|
|
6863
7003
|
return void 0;
|
|
6864
7004
|
}
|
|
6865
7005
|
}
|
|
6866
|
-
/**
|
|
6867
|
-
|
|
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);
|
|
6868
7013
|
const file = this.paths.societySkillFile(skill2.name);
|
|
6869
7014
|
const replaced = await exists(file);
|
|
6870
|
-
|
|
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;
|
|
6871
7027
|
await writeMarkdown(file, {
|
|
6872
7028
|
name: skill2.name,
|
|
6873
7029
|
description: skill2.summary,
|
|
6874
|
-
metadata: {
|
|
6875
|
-
|
|
7030
|
+
metadata: {
|
|
7031
|
+
proposedBy: proposal2.proposedBy,
|
|
7032
|
+
promotedBy: by,
|
|
7033
|
+
promotedAt: this.now().toISOString(),
|
|
7034
|
+
...taken === void 0 ? {} : { commit: taken.commit }
|
|
7035
|
+
}
|
|
7036
|
+
}, body);
|
|
6876
7037
|
await this.events.append("skill.promoted", by, {
|
|
6877
7038
|
name: skill2.name,
|
|
6878
7039
|
summary: skill2.summary,
|
|
6879
|
-
proposedBy,
|
|
7040
|
+
proposedBy: proposal2.proposedBy,
|
|
6880
7041
|
replaced,
|
|
7042
|
+
...taken === void 0 ? {} : { files: taken.files.length },
|
|
6881
7043
|
...meta
|
|
6882
7044
|
});
|
|
6883
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.`);
|
|
6884
7046
|
return replaced;
|
|
6885
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
|
+
}
|
|
6886
7180
|
async validateAddAgent(input) {
|
|
6887
7181
|
if (await exists(this.paths.agentFile(input.name))) {
|
|
6888
7182
|
throw new BoardError("ALREADY_EXISTS", `agent ${input.name} already exists`);
|
|
@@ -7394,67 +7688,70 @@ ${charter.purpose}
|
|
|
7394
7688
|
* retired. Validation runs before any write, so an impossible provision fails the decision.
|
|
7395
7689
|
*/
|
|
7396
7690
|
async decide(actor, proposalId, outcome, reason) {
|
|
7397
|
-
return this.mutex.run(
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
this.
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
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, {
|
|
7437
7742
|
proposalId,
|
|
7438
|
-
outcome,
|
|
7439
7743
|
kind: proposal2.kind,
|
|
7440
|
-
|
|
7441
|
-
...proposal2.filedFrom === void 0 ? {} : { filedFrom: proposal2.filedFrom }
|
|
7744
|
+
...provision
|
|
7442
7745
|
});
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
const result = provision === void 0 ? "" : ` Provisioned: ${Object.entries(provision).map(([key, value]) => `${key} ${plain(value)}`).join(", ")}.`;
|
|
7453
|
-
const thread2 = await this.proposalThreadUnlocked(actor.name, proposal2);
|
|
7454
|
-
await this.appendThreadMessage(actor.name, thread2.id, `${verdict}: ${describeCharter(proposal2.kind, proposal2.charter)}.${why}${result}`, { step: { action: outcome } });
|
|
7455
|
-
await this.endThreadOf(proposalId, actor.name, outcome);
|
|
7456
|
-
return decision;
|
|
7457
|
-
});
|
|
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;
|
|
7458
7755
|
}
|
|
7459
7756
|
/** The opener, the people of its subject, and anyone who posted in it; mentions reach the rest. */
|
|
7460
7757
|
async participatesInThread(actor, thread2, threads) {
|
|
@@ -8039,7 +8336,7 @@ var knowledge = program.command("knowledge").description("Shared knowledge, by p
|
|
|
8039
8336
|
knowledge.command("list [project]").description("List knowledge topics of a project, or of the society when no project is given").action(async (projectSlug) => {
|
|
8040
8337
|
const board = await open();
|
|
8041
8338
|
const topics = await board.listKnowledge(projectSlug ?? null);
|
|
8042
|
-
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"));
|
|
8043
8340
|
});
|
|
8044
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) => {
|
|
8045
8342
|
const board = await open();
|
|
@@ -8049,15 +8346,16 @@ knowledge.command("show <topic>").description("Print a knowledge topic").option(
|
|
|
8049
8346
|
}
|
|
8050
8347
|
print(found, () => found.body);
|
|
8051
8348
|
});
|
|
8052
|
-
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) => {
|
|
8053
8350
|
const board = await open();
|
|
8054
8351
|
const body = opts.file === void 0 ? await readStdin() : await readFile6(path7.resolve(opts.file), "utf8");
|
|
8055
8352
|
const written = await board.writeKnowledge(await actorFor(board, opts.as), {
|
|
8056
8353
|
project: opts.project ?? null,
|
|
8057
8354
|
topic,
|
|
8058
|
-
body
|
|
8355
|
+
body,
|
|
8356
|
+
revision: Number(opts.revision)
|
|
8059
8357
|
});
|
|
8060
|
-
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}`);
|
|
8061
8359
|
});
|
|
8062
8360
|
var skill = program.command("skill").description("Skills promoted to the society");
|
|
8063
8361
|
skill.command("list").description("List the society's skills, and each member's own").action(async () => {
|