@nanocollective/roster 0.1.0-alpha.50 → 0.1.0-alpha.51
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.js +537 -469
- package/docs/portal.md +12 -10
- package/package.json +1 -1
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +20 -8
- package/templates/briefs/amend.md +4 -3
- package/templates/portal/css/health.css +13 -0
- package/templates/portal/css/home.css +3 -1
- package/templates/portal/css/setup.css +3 -2
- package/templates/portal/js/views/health.js +10 -5
- package/templates/portal/js/views/paste.js +87 -79
- package/templates/portal/js/views/prompt.js +90 -39
package/dist/cli.js
CHANGED
|
@@ -654,20 +654,276 @@ function parseFlags(argv) {
|
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
// src/commands/brief.ts
|
|
657
|
-
import { existsSync as
|
|
658
|
-
import { join as
|
|
657
|
+
import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync10 } from "fs";
|
|
658
|
+
import { join as join11 } from "path";
|
|
659
659
|
|
|
660
660
|
// src/lib/amend.ts
|
|
661
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
662
|
+
import { join as join7 } from "path";
|
|
663
|
+
|
|
664
|
+
// src/lib/pastebrief.ts
|
|
661
665
|
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
662
666
|
import { join as join6 } from "path";
|
|
663
|
-
|
|
664
|
-
|
|
667
|
+
|
|
668
|
+
// src/lib/stub.ts
|
|
669
|
+
var MARKERS = [
|
|
670
|
+
"**This file is a stub",
|
|
671
|
+
"This file is a stub, and it is the most important file in this repo",
|
|
672
|
+
"This file is a stub, and everything the staff say is downstream of it",
|
|
673
|
+
"The headings below are the shape that has worked; the words are yours",
|
|
674
|
+
"Write it with your own AI",
|
|
675
|
+
"One paragraph. What this role is for, in this business specifically.",
|
|
676
|
+
"Not a segment. The person, what they were doing ten minutes before they arrived",
|
|
677
|
+
"The single thing this staff member is optimising",
|
|
678
|
+
"Or answer these by hand. Short is better than complete."
|
|
679
|
+
];
|
|
680
|
+
function looksUnwritten(body) {
|
|
681
|
+
if (!body.trim()) return true;
|
|
682
|
+
return MARKERS.some((marker) => body.includes(marker));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/lib/paste.ts
|
|
686
|
+
var BEGIN = (path) => `<<<ROSTER FILE ${path}>>>`;
|
|
687
|
+
var END = "<<<ROSTER END>>>";
|
|
688
|
+
var BLOCK = /^[ \t]*<<<ROSTER FILE ([^>\n]+)>>>[ \t]*\n([\s\S]*?)\n?[ \t]*<<<ROSTER END>>>/gm;
|
|
689
|
+
var MIN_WORDS = 40;
|
|
690
|
+
function parsePaste(raw, targets) {
|
|
691
|
+
const files = [];
|
|
692
|
+
const problems = [];
|
|
693
|
+
const text = raw.replace(/\r\n/g, "\n");
|
|
694
|
+
if (!text.trim()) {
|
|
695
|
+
return {
|
|
696
|
+
files,
|
|
697
|
+
problems: [{ id: "empty", message: "Nothing was pasted.", retry: "" }]
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
const known = new Map(targets.map((t) => [t.path, t]));
|
|
701
|
+
BLOCK.lastIndex = 0;
|
|
702
|
+
for (const m of text.matchAll(BLOCK)) {
|
|
703
|
+
const written = m[1].trim();
|
|
704
|
+
const body = strip(m[2] ?? "");
|
|
705
|
+
const target = resolveTarget(written, known);
|
|
706
|
+
if (!target) {
|
|
707
|
+
problems.push({
|
|
708
|
+
id: "unknown-path",
|
|
709
|
+
path: written,
|
|
710
|
+
message: `The answer writes ${written}, which this brief did not ask for.`,
|
|
711
|
+
retry: `Write only ${[...known.keys()].join(", ")}, using the sentinels exactly as given.`
|
|
712
|
+
});
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
files.push({
|
|
716
|
+
path: target.path,
|
|
717
|
+
text: ensureNewline(body),
|
|
718
|
+
unchanged: body.trim() === target.before.trim()
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
if (!files.length && !problems.length) {
|
|
722
|
+
problems.push({
|
|
723
|
+
id: "no-envelope",
|
|
724
|
+
message: "Your AI answered in prose. The reply has no <<<ROSTER FILE ...>>> block, so there is nothing here that can be saved as a file.",
|
|
725
|
+
retry: `Send that again as the finished file only, wrapped exactly like this and with nothing outside it:
|
|
726
|
+
|
|
727
|
+
${BEGIN(targets[0]?.path ?? "<path>")}
|
|
728
|
+
...the whole file...
|
|
729
|
+
${END}`
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
for (const file of files) {
|
|
733
|
+
const words = file.text.split(/\s+/).filter(Boolean).length;
|
|
734
|
+
if (file.unchanged) {
|
|
735
|
+
problems.push({
|
|
736
|
+
id: "brief-echoed",
|
|
737
|
+
path: file.path,
|
|
738
|
+
message: `${file.path} came back byte for byte. Some models restate a long prompt before working.`,
|
|
739
|
+
retry: "That is the file I sent you. Write the new version."
|
|
740
|
+
});
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (words < MIN_WORDS) {
|
|
744
|
+
problems.push({
|
|
745
|
+
id: "too-short",
|
|
746
|
+
path: file.path,
|
|
747
|
+
message: `${file.path} is ${words} words. That is a failed answer, not a short one.`,
|
|
748
|
+
retry: "That is far too short. Interview me properly first, then write the whole file."
|
|
749
|
+
});
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (looksUnwritten(file.text)) {
|
|
753
|
+
problems.push({
|
|
754
|
+
id: "stub-echoed",
|
|
755
|
+
path: file.path,
|
|
756
|
+
message: `${file.path} still carries the scaffold's own headings, so the questions were never answered.`,
|
|
757
|
+
retry: "You returned the template with its instructions still in it. Answer the questions and write the real file, with none of the scaffold's guidance left in it."
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return { files, problems };
|
|
762
|
+
}
|
|
763
|
+
function resolveTarget(written, known) {
|
|
764
|
+
const exact = known.get(written);
|
|
765
|
+
if (exact) return exact;
|
|
766
|
+
const tail2 = written.replace(/^(\.\/)+/, "").replace(/^\/+/, "");
|
|
767
|
+
if (!tail2 || tail2.split("/").includes("..")) return void 0;
|
|
768
|
+
const matches = [...known.values()].filter((t) => t.path === tail2 || t.path.endsWith(`/${tail2}`));
|
|
769
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
770
|
+
}
|
|
771
|
+
function strip(body) {
|
|
772
|
+
const trimmed = body.replace(/^\n+/, "").replace(/\s+$/, "");
|
|
773
|
+
const fenced = /^(`{3,}|~{3,})[^\n]*\n([\s\S]*)\n\1\s*$/.exec(trimmed);
|
|
774
|
+
return fenced ? fenced[2] : trimmed;
|
|
775
|
+
}
|
|
776
|
+
function ensureNewline(text) {
|
|
777
|
+
return text.endsWith("\n") ? text : `${text}
|
|
778
|
+
`;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/lib/pastebrief.ts
|
|
782
|
+
var WRITES = {
|
|
783
|
+
discover: (c) => [`${c.opsName}/org/business.md`],
|
|
784
|
+
voice: (c) => [`${c.opsName}/org/voice.md`],
|
|
785
|
+
priorities: (c) => [`${c.opsName}/org/priorities.md`],
|
|
786
|
+
charter: (c) => [`${c.dir}/CHARTER.md`]
|
|
787
|
+
};
|
|
788
|
+
var READS = {
|
|
789
|
+
discover: (c) => [
|
|
790
|
+
`${c.opsName}/org/operating.md`,
|
|
791
|
+
`${c.opsName}/org/voice.md`,
|
|
792
|
+
`${c.opsName}/org/guardrails.md`
|
|
793
|
+
],
|
|
794
|
+
voice: (c) => [`${c.opsName}/org/business.md`, `${c.opsName}/org/guardrails.md`],
|
|
795
|
+
priorities: (c) => [`${c.opsName}/org/business.md`],
|
|
796
|
+
/* A charter is the difference between this staff member and everyone else, so the peers'
|
|
797
|
+
charters are the most useful thing in the room: without them the model writes a second
|
|
798
|
+
copy of whoever it was shown. */
|
|
799
|
+
charter: (c) => [
|
|
800
|
+
`${c.opsName}/org/business.md`,
|
|
801
|
+
`${c.opsName}/org/operating.md`,
|
|
802
|
+
`${c.opsName}/org/voice.md`,
|
|
803
|
+
`${c.opsName}/org/guardrails.md`,
|
|
804
|
+
...c.peers.map((p) => `${p}/CHARTER.md`)
|
|
805
|
+
]
|
|
806
|
+
};
|
|
807
|
+
function pasteable(kind) {
|
|
808
|
+
return kind in WRITES;
|
|
809
|
+
}
|
|
810
|
+
function pasteBrief(ws, kind, rendered, ctx) {
|
|
811
|
+
if (!pasteable(kind)) throw new Error(`there is no paste-mode brief for "${kind}"`);
|
|
812
|
+
const c = { opsName: ws.opsName, dir: ctx.dir ?? "", peers: ctx.peers ?? [] };
|
|
813
|
+
const writes = WRITES[kind](c);
|
|
814
|
+
const reads = READS[kind](c).filter((rel) => !writes.includes(rel));
|
|
815
|
+
const targets = writes.map((rel) => ({ path: rel, before: read(ws, rel) }));
|
|
816
|
+
const parts = [rendered.trimEnd(), "", "---", "", head(writes)];
|
|
817
|
+
parts.push("", "## The files this refers to", "");
|
|
818
|
+
parts.push(
|
|
819
|
+
"You cannot open anything, so everything the brief mentions is below. Do not ask for files;",
|
|
820
|
+
"if something you want is genuinely not here, say what and why, and work without it.",
|
|
821
|
+
""
|
|
822
|
+
);
|
|
823
|
+
for (const rel of reads) parts.push(...fileBlock(ws, rel, "read this, do not rewrite it"));
|
|
824
|
+
parts.push("## What you are writing", "");
|
|
825
|
+
for (const rel of targets) {
|
|
826
|
+
parts.push(...fileBlock(ws, rel.path, rel.before.trim() ? "replace this" : "currently empty"));
|
|
827
|
+
}
|
|
828
|
+
parts.push(...tail(writes));
|
|
829
|
+
return { kind, text: `${parts.join("\n").replace(/\n{3,}/g, "\n\n")}
|
|
830
|
+
`, targets };
|
|
831
|
+
}
|
|
832
|
+
function head(writes) {
|
|
833
|
+
return [
|
|
834
|
+
"## How to answer",
|
|
835
|
+
"",
|
|
836
|
+
"**You are talking to a person, not to a filesystem.** You cannot read or write anything.",
|
|
837
|
+
"Everything you need is in this message, and your answer comes back by being pasted into a",
|
|
838
|
+
"tool that turns it into " + (writes.length === 1 ? "a file" : "files") + ".",
|
|
839
|
+
"",
|
|
840
|
+
"**Interview first.** Ask one question at a time and wait. Do not produce the file in your",
|
|
841
|
+
"first reply, and do not present a form. When you have enough, say so and write it."
|
|
842
|
+
].join("\n");
|
|
843
|
+
}
|
|
844
|
+
function handBackAnyOf(paths) {
|
|
845
|
+
return [
|
|
846
|
+
"---",
|
|
847
|
+
"",
|
|
848
|
+
"## How to hand it back",
|
|
849
|
+
"",
|
|
850
|
+
"Wrap each file you changed like this, and only the files you changed:",
|
|
851
|
+
"",
|
|
852
|
+
"```",
|
|
853
|
+
`${BEGIN(paths[0] ?? "path/to/file.md")}
|
|
854
|
+
...the whole file, exactly as it should be saved...
|
|
855
|
+
${END}`,
|
|
856
|
+
"```",
|
|
857
|
+
"",
|
|
858
|
+
"**Rules**, because the answer is parsed rather than read:",
|
|
859
|
+
"",
|
|
860
|
+
"- The sentinel lines go on their own lines, spelled exactly as above, with the file's path.",
|
|
861
|
+
"- Between them goes the **whole file**, exactly as it should be saved. No fences around it,",
|
|
862
|
+
" no commentary inside it.",
|
|
863
|
+
"- Anything you want to say goes **outside** the block.",
|
|
864
|
+
"- The files you may write are these, and no others:",
|
|
865
|
+
...paths.map((p) => ` - \`${p}\``),
|
|
866
|
+
""
|
|
867
|
+
].join("\n");
|
|
868
|
+
}
|
|
869
|
+
function tail(writes) {
|
|
870
|
+
const example = writes.map((rel) => `${BEGIN(rel)}
|
|
871
|
+
...the whole file, exactly as it should be saved...
|
|
872
|
+
${END}`).join("\n\n");
|
|
873
|
+
return [
|
|
874
|
+
"---",
|
|
875
|
+
"",
|
|
876
|
+
"## How to hand it back",
|
|
877
|
+
"",
|
|
878
|
+
"When the interview is done and you are writing the final version, wrap it like this:",
|
|
879
|
+
"",
|
|
880
|
+
"```",
|
|
881
|
+
example,
|
|
882
|
+
"```",
|
|
883
|
+
"",
|
|
884
|
+
`**Rules**, because the answer is parsed rather than read:`,
|
|
885
|
+
"",
|
|
886
|
+
"- The sentinel lines go on their own lines, spelled exactly as above.",
|
|
887
|
+
"- Between them goes the **whole file**, exactly as it should be saved. No fences around it,",
|
|
888
|
+
" no commentary inside it, no summary of what you changed.",
|
|
889
|
+
"- Anything you want to say to the person goes **outside** the block. That part is ignored by",
|
|
890
|
+
" the tool and read by them, so it is the right place for what you were unsure about.",
|
|
891
|
+
`- Write only ${writes.join(" and ")}. Nothing else.`,
|
|
892
|
+
"",
|
|
893
|
+
"Nothing is saved automatically. They will see a diff and decide."
|
|
894
|
+
];
|
|
895
|
+
}
|
|
896
|
+
function fileBlock(ws, rel, note) {
|
|
897
|
+
const body = read(ws, rel);
|
|
898
|
+
if (!body.trim()) return [`### \`${rel}\``, "", `*Empty or absent (${note}).*`, ""];
|
|
899
|
+
return [`### \`${rel}\``, "", `*${note}*`, "", fence(body), ""];
|
|
900
|
+
}
|
|
901
|
+
function read(ws, rel) {
|
|
902
|
+
const full = join6(ws.root, rel);
|
|
903
|
+
return existsSync6(full) ? readFileSync5(full, "utf8") : "";
|
|
904
|
+
}
|
|
905
|
+
function fence(text) {
|
|
906
|
+
const longest = (text.match(/`{3,}/g) ?? []).reduce((n, m) => Math.max(n, m.length), 2);
|
|
907
|
+
const bars = "`".repeat(longest + 1);
|
|
908
|
+
return `${bars}
|
|
909
|
+
${text.trimEnd()}
|
|
910
|
+
${bars}`;
|
|
911
|
+
}
|
|
912
|
+
function briefTemplate(kind) {
|
|
913
|
+
const path = join6(briefTemplateDir(), `${kind}.md`);
|
|
914
|
+
if (!existsSync6(path)) throw new Error(`no brief template for "${kind}"`);
|
|
915
|
+
return readFileSync5(path, "utf8");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/lib/amend.ts
|
|
919
|
+
function amendBrief(ws, view, tokens, want, writable = []) {
|
|
920
|
+
const template = readFileSync6(join7(briefTemplateDir(), "amend.md"), "utf8");
|
|
665
921
|
const head2 = template.replace(/%%([A-Z_]+)%%/g, (m, name) => {
|
|
666
922
|
if (name === "WANT") return want.trim() || WANT_PLACEHOLDER;
|
|
667
923
|
return tokens[name] ?? m;
|
|
668
924
|
});
|
|
669
925
|
const parts = [head2, "", "---", "", `## The ${view.kind} prompt as it composes today`, ""];
|
|
670
|
-
parts.push(
|
|
926
|
+
parts.push(fence2(view.composed), "");
|
|
671
927
|
parts.push("---", "", "## Every file it is assembled from, in order", "");
|
|
672
928
|
for (const layer of view.layers) {
|
|
673
929
|
if (layer.missing) {
|
|
@@ -675,7 +931,7 @@ function amendBrief(ws, view, tokens, want) {
|
|
|
675
931
|
continue;
|
|
676
932
|
}
|
|
677
933
|
parts.push(`### ${layer.path}`, "", `Reaches ${reach(layer.rel)}.`, "");
|
|
678
|
-
parts.push(
|
|
934
|
+
parts.push(fence2(read2(ws, layer.path)), "");
|
|
679
935
|
}
|
|
680
936
|
parts.push("---", "", "## Named by the prompt, not contained in it", "");
|
|
681
937
|
parts.push(
|
|
@@ -685,23 +941,24 @@ function amendBrief(ws, view, tokens, want) {
|
|
|
685
941
|
);
|
|
686
942
|
for (const layer of view.runtime) {
|
|
687
943
|
if (layer.missing) continue;
|
|
688
|
-
parts.push(`- \`${layer.path}\` \u2014 ${size(
|
|
944
|
+
parts.push(`- \`${layer.path}\` \u2014 ${size(read2(ws, layer.path))}`);
|
|
689
945
|
}
|
|
946
|
+
if (writable.length) parts.push("", handBackAnyOf(writable));
|
|
690
947
|
return parts.join("\n").replace(/\n{3,}/g, "\n\n") + "\n";
|
|
691
948
|
}
|
|
692
949
|
var WANT_PLACEHOLDER = "**Replace this line with what you want changed**, then send the whole message.";
|
|
693
950
|
function reach(rel) {
|
|
694
951
|
return rel.startsWith("staff:") ? "this staff member only" : "every staff member";
|
|
695
952
|
}
|
|
696
|
-
function
|
|
697
|
-
const full =
|
|
698
|
-
return
|
|
953
|
+
function read2(ws, rel) {
|
|
954
|
+
const full = join7(ws.root, rel);
|
|
955
|
+
return existsSync7(full) ? readFileSync6(full, "utf8") : "";
|
|
699
956
|
}
|
|
700
957
|
function size(text) {
|
|
701
958
|
const words = text.split(/\s+/).filter(Boolean).length;
|
|
702
959
|
return `${words.toLocaleString()} words`;
|
|
703
960
|
}
|
|
704
|
-
function
|
|
961
|
+
function fence2(text) {
|
|
705
962
|
const longest = (text.match(/`{3,}/g) ?? []).reduce((n, m) => Math.max(n, m.length), 2);
|
|
706
963
|
const bars = "`".repeat(longest + 1);
|
|
707
964
|
return `${bars}
|
|
@@ -710,27 +967,27 @@ ${bars}`;
|
|
|
710
967
|
}
|
|
711
968
|
|
|
712
969
|
// src/lib/examples.ts
|
|
713
|
-
import { existsSync as
|
|
714
|
-
import { join as
|
|
970
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
|
|
971
|
+
import { join as join9 } from "path";
|
|
715
972
|
|
|
716
973
|
// src/lib/docs.ts
|
|
717
|
-
import { existsSync as
|
|
718
|
-
import { dirname as dirname3, join as
|
|
974
|
+
import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
|
|
975
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
719
976
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
720
977
|
function docsDir() {
|
|
721
978
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
722
|
-
const candidates = [
|
|
723
|
-
for (const path of candidates) if (
|
|
979
|
+
const candidates = [join8(here, "..", "..", "docs"), join8(here, "..", "docs")];
|
|
980
|
+
for (const path of candidates) if (existsSync8(path)) return path;
|
|
724
981
|
throw new Error(`docs not found. Looked in:
|
|
725
982
|
${candidates.join("\n ")}`);
|
|
726
983
|
}
|
|
727
984
|
function docPages() {
|
|
728
985
|
const dir = docsDir();
|
|
729
986
|
const all = readdirSync2(dir).filter((f) => f.endsWith(".md"));
|
|
730
|
-
const index =
|
|
987
|
+
const index = readFileSync7(join8(dir, "README.md"), "utf8");
|
|
731
988
|
const ordered = [...index.matchAll(/\[([^\]]+)\]\(([\w.-]+\.md)\)/g)].map((m) => ({ title: m[1], file: m[2] })).filter((d, i, xs) => all.includes(d.file) && xs.findIndex((x) => x.file === d.file) === i);
|
|
732
989
|
const listed = new Set(ordered.map((d) => d.file));
|
|
733
|
-
const rest = all.filter((f) => f !== "README.md" && !listed.has(f)).map((file) => ({ file, title: titleOf(
|
|
990
|
+
const rest = all.filter((f) => f !== "README.md" && !listed.has(f)).map((file) => ({ file, title: titleOf(join8(dir, file)) }));
|
|
734
991
|
return [{ file: "README.md", title: "Overview" }, ...ordered, ...rest];
|
|
735
992
|
}
|
|
736
993
|
var ASSET_TYPES = {
|
|
@@ -742,13 +999,13 @@ var ASSET_TYPES = {
|
|
|
742
999
|
".webp": "image/webp"
|
|
743
1000
|
};
|
|
744
1001
|
function docAsset(name) {
|
|
745
|
-
const dir =
|
|
746
|
-
if (!
|
|
1002
|
+
const dir = join8(docsDir(), "images");
|
|
1003
|
+
if (!existsSync8(dir)) return null;
|
|
747
1004
|
const wanted = String(name ?? "").replace(/^images\//, "");
|
|
748
1005
|
const found = readdirSync2(dir).find((f) => f === wanted);
|
|
749
1006
|
if (!found) return null;
|
|
750
1007
|
const type = ASSET_TYPES[found.slice(found.lastIndexOf(".")).toLowerCase()];
|
|
751
|
-
return type ? { path:
|
|
1008
|
+
return type ? { path: join8(dir, found), type } : null;
|
|
752
1009
|
}
|
|
753
1010
|
function searchDocs(query2, snippets = 3) {
|
|
754
1011
|
const terms = String(query2 ?? "").toLowerCase().split(/\s+/).map((t) => t.trim()).filter(Boolean);
|
|
@@ -756,7 +1013,7 @@ function searchDocs(query2, snippets = 3) {
|
|
|
756
1013
|
const dir = docsDir();
|
|
757
1014
|
const out = [];
|
|
758
1015
|
for (const page2 of docPages()) {
|
|
759
|
-
const text =
|
|
1016
|
+
const text = readFileSync7(join8(dir, page2.file), "utf8").replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
760
1017
|
const haystack = (page2.title + "\n" + text).toLowerCase();
|
|
761
1018
|
if (!terms.every((t) => haystack.includes(t))) continue;
|
|
762
1019
|
const title = page2.title.toLowerCase();
|
|
@@ -792,7 +1049,7 @@ function snippet(line, terms, width = 150) {
|
|
|
792
1049
|
return (from ? "\u2026" : "") + clean.slice(from, from + width).trim() + "\u2026";
|
|
793
1050
|
}
|
|
794
1051
|
function titleOf(path) {
|
|
795
|
-
const first =
|
|
1052
|
+
const first = readFileSync7(path, "utf8").split("\n").find((l) => l.startsWith("# "));
|
|
796
1053
|
return first ? first.slice(2).trim() : path.split("/").pop();
|
|
797
1054
|
}
|
|
798
1055
|
|
|
@@ -839,8 +1096,8 @@ function matchExample(handle, name = "") {
|
|
|
839
1096
|
return PATTERNS.find(([, re]) => re.test(words))?.[0];
|
|
840
1097
|
}
|
|
841
1098
|
function exampleSection(choice) {
|
|
842
|
-
const path =
|
|
843
|
-
if (!
|
|
1099
|
+
const path = join9(docsDir(), "charters", `${choice}.md`);
|
|
1100
|
+
if (!existsSync9(path)) throw new Error(`there is no example charter called "${choice}"`);
|
|
844
1101
|
return [
|
|
845
1102
|
"## A worked example, to adapt",
|
|
846
1103
|
"",
|
|
@@ -850,14 +1107,14 @@ function exampleSection(choice) {
|
|
|
850
1107
|
"charter built from someone else's specifics is the generic agent this file exists to",
|
|
851
1108
|
"prevent. The interview still comes first, and everything in the charter comes from it.",
|
|
852
1109
|
"",
|
|
853
|
-
|
|
1110
|
+
fence3(readFileSync8(path, "utf8")),
|
|
854
1111
|
""
|
|
855
1112
|
].join("\n");
|
|
856
1113
|
}
|
|
857
1114
|
function charterStarter(choice, business) {
|
|
858
|
-
const path =
|
|
859
|
-
if (!
|
|
860
|
-
return
|
|
1115
|
+
const path = join9(docsDir(), "charters", `${choice}.md`);
|
|
1116
|
+
if (!existsSync9(path)) throw new Error(`there is no example charter called "${choice}"`);
|
|
1117
|
+
return readFileSync8(path, "utf8").replace(/^> \*\*An example to adapt[\s\S]*?\n\n/m, "").replace(/\bAcme\b/g, business);
|
|
861
1118
|
}
|
|
862
1119
|
function exampleOffer(handle) {
|
|
863
1120
|
return [
|
|
@@ -868,7 +1125,7 @@ function exampleOffer(handle) {
|
|
|
868
1125
|
""
|
|
869
1126
|
].join("\n");
|
|
870
1127
|
}
|
|
871
|
-
function
|
|
1128
|
+
function fence3(text) {
|
|
872
1129
|
const longest = (text.match(/`{3,}/g) ?? []).reduce((n, m) => Math.max(n, m.length), 2);
|
|
873
1130
|
const bars = "`".repeat(longest + 1);
|
|
874
1131
|
return `${bars}markdown
|
|
@@ -904,8 +1161,8 @@ function defaultMarker(name) {
|
|
|
904
1161
|
|
|
905
1162
|
// src/lib/prompt.ts
|
|
906
1163
|
import { execFileSync } from "child_process";
|
|
907
|
-
import { existsSync as
|
|
908
|
-
import { dirname as dirname4, join as
|
|
1164
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, statSync as statSync2, writeFileSync } from "fs";
|
|
1165
|
+
import { dirname as dirname4, join as join10, relative as relative2, resolve as resolve2 } from "path";
|
|
909
1166
|
var KINDS = ["daily", "mention"];
|
|
910
1167
|
function promptView(ws, compose, staffHandle, brainDir, kind) {
|
|
911
1168
|
const had = process.env.ROSTER_CONTEXT;
|
|
@@ -925,7 +1182,7 @@ function promptView(ws, compose, staffHandle, brainDir, kind) {
|
|
|
925
1182
|
staff: staffHandle,
|
|
926
1183
|
kind
|
|
927
1184
|
});
|
|
928
|
-
const entry =
|
|
1185
|
+
const entry = join10(ws.opsDir, "prompts", `${kind}.md`);
|
|
929
1186
|
const layers = [describe(ws, brainDir, `prompts/${kind}.md`, false)];
|
|
930
1187
|
walk(ws, brainDir, entry, layers, /* @__PURE__ */ new Set([resolve2(entry)]));
|
|
931
1188
|
return {
|
|
@@ -941,8 +1198,8 @@ function promptView(ws, compose, staffHandle, brainDir, kind) {
|
|
|
941
1198
|
}
|
|
942
1199
|
var INCLUDE = /\{\{>(\??)\s*([^}\s]+)\s*\}\}/g;
|
|
943
1200
|
function walk(ws, brainDir, file, out, seen) {
|
|
944
|
-
if (!
|
|
945
|
-
const text =
|
|
1201
|
+
if (!existsSync10(file)) return;
|
|
1202
|
+
const text = readFileSync9(file, "utf8");
|
|
946
1203
|
for (const m of text.matchAll(INCLUDE)) {
|
|
947
1204
|
const rel = m[2];
|
|
948
1205
|
const layer = describe(ws, brainDir, rel, m[1] === "?");
|
|
@@ -956,8 +1213,8 @@ function walk(ws, brainDir, file, out, seen) {
|
|
|
956
1213
|
function describe(ws, brainDir, rel, optional) {
|
|
957
1214
|
const inBrain = rel.startsWith("staff:");
|
|
958
1215
|
const base2 = inBrain ? brainDir : ws.opsDir;
|
|
959
|
-
const full =
|
|
960
|
-
const missing = !
|
|
1216
|
+
const full = join10(base2, inBrain ? rel.slice(6) : rel);
|
|
1217
|
+
const missing = !existsSync10(full);
|
|
961
1218
|
return {
|
|
962
1219
|
rel,
|
|
963
1220
|
path: relative2(ws.root, full),
|
|
@@ -975,8 +1232,8 @@ function runtimeReads(ws, brainDir) {
|
|
|
975
1232
|
[ws.opsDir, "org/business.md"]
|
|
976
1233
|
];
|
|
977
1234
|
return named.map(([base2, rel]) => {
|
|
978
|
-
const full =
|
|
979
|
-
const missing = !
|
|
1235
|
+
const full = join10(base2, rel);
|
|
1236
|
+
const missing = !existsSync10(full);
|
|
980
1237
|
return {
|
|
981
1238
|
rel,
|
|
982
1239
|
path: relative2(ws.root, full),
|
|
@@ -1008,7 +1265,7 @@ function saveFile(ws, rel, text, message) {
|
|
|
1008
1265
|
const full = resolve2(ws.root, rel);
|
|
1009
1266
|
const repoDir = repoRootOf(full);
|
|
1010
1267
|
const repo = relative2(ws.root, repoDir) || ".";
|
|
1011
|
-
const before =
|
|
1268
|
+
const before = existsSync10(full) ? readFileSync9(full, "utf8") : null;
|
|
1012
1269
|
if (before === text)
|
|
1013
1270
|
return { path: rel, repo, committed: false, pushed: false, note: "no change" };
|
|
1014
1271
|
writeFileSync(full, text);
|
|
@@ -1143,13 +1400,13 @@ async function briefCommand(argv) {
|
|
|
1143
1400
|
ws,
|
|
1144
1401
|
compose,
|
|
1145
1402
|
handle,
|
|
1146
|
-
|
|
1403
|
+
join11(ws.root, entry.dir ?? entry.handle),
|
|
1147
1404
|
opts.kind ?? "daily"
|
|
1148
1405
|
);
|
|
1149
1406
|
process.stdout.write(amendBrief(ws, view, tokens, opts.want ?? ""));
|
|
1150
1407
|
return 0;
|
|
1151
1408
|
}
|
|
1152
|
-
const text =
|
|
1409
|
+
const text = readFileSync10(join11(briefTemplateDir(), `${kind}.md`), "utf8");
|
|
1153
1410
|
let out = renderBrief(text, tokens, `briefs/${kind}.md`);
|
|
1154
1411
|
if (kind === "charter") out = withExample(out, handle, tokens.NAME ?? "", opts.example);
|
|
1155
1412
|
process.stdout.write(out);
|
|
@@ -1162,9 +1419,9 @@ function staffTokens(ws, org, handle, parseYaml) {
|
|
|
1162
1419
|
throw new Error(`unknown staff handle "${handle}". org.yaml knows: ${known}`);
|
|
1163
1420
|
}
|
|
1164
1421
|
const dir = entry.dir ?? entry.handle;
|
|
1165
|
-
const manifestPath =
|
|
1166
|
-
if (!
|
|
1167
|
-
const manifest = parseYaml(
|
|
1422
|
+
const manifestPath = join11(ws.root, dir, "staff.yaml");
|
|
1423
|
+
if (!existsSync11(manifestPath)) throw new Error(`no staff.yaml at ${manifestPath}`);
|
|
1424
|
+
const manifest = parseYaml(readFileSync10(manifestPath, "utf8"), `${dir}/staff.yaml`);
|
|
1168
1425
|
return tokensFor(spec(org, ws), specFromManifest(manifest, dir));
|
|
1169
1426
|
}
|
|
1170
1427
|
function spec(org, ws) {
|
|
@@ -1295,27 +1552,8 @@ function repoName(full) {
|
|
|
1295
1552
|
|
|
1296
1553
|
// src/lib/setup.ts
|
|
1297
1554
|
import { execFile as execFile3 } from "child_process";
|
|
1298
|
-
import { existsSync as
|
|
1299
|
-
import { join as
|
|
1300
|
-
|
|
1301
|
-
// src/lib/stub.ts
|
|
1302
|
-
var MARKERS = [
|
|
1303
|
-
"**This file is a stub",
|
|
1304
|
-
"This file is a stub, and it is the most important file in this repo",
|
|
1305
|
-
"This file is a stub, and everything the staff say is downstream of it",
|
|
1306
|
-
"The headings below are the shape that has worked; the words are yours",
|
|
1307
|
-
"Write it with your own AI",
|
|
1308
|
-
"One paragraph. What this role is for, in this business specifically.",
|
|
1309
|
-
"Not a segment. The person, what they were doing ten minutes before they arrived",
|
|
1310
|
-
"The single thing this staff member is optimising",
|
|
1311
|
-
"Or answer these by hand. Short is better than complete."
|
|
1312
|
-
];
|
|
1313
|
-
function looksUnwritten(body) {
|
|
1314
|
-
if (!body.trim()) return true;
|
|
1315
|
-
return MARKERS.some((marker) => body.includes(marker));
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// src/lib/setup.ts
|
|
1555
|
+
import { existsSync as existsSync12, mkdirSync, readFileSync as readFileSync11 } from "fs";
|
|
1556
|
+
import { join as join12 } from "path";
|
|
1319
1557
|
var AGENTS = [
|
|
1320
1558
|
{
|
|
1321
1559
|
id: "claude-code-action",
|
|
@@ -1381,17 +1619,17 @@ async function setupStatus(startedIn) {
|
|
|
1381
1619
|
};
|
|
1382
1620
|
}
|
|
1383
1621
|
function readOrgName(opsDir) {
|
|
1384
|
-
const path =
|
|
1385
|
-
if (!
|
|
1386
|
-
const m = /^org:\s*(\S+)\s*$/m.exec(
|
|
1622
|
+
const path = join12(opsDir, "org.yaml");
|
|
1623
|
+
if (!existsSync12(path)) return void 0;
|
|
1624
|
+
const m = /^org:\s*(\S+)\s*$/m.exec(readFileSync11(path, "utf8"));
|
|
1387
1625
|
return m?.[1];
|
|
1388
1626
|
}
|
|
1389
1627
|
function readProducts(opsDir) {
|
|
1390
|
-
const path =
|
|
1391
|
-
if (!
|
|
1628
|
+
const path = join12(opsDir, "org.yaml");
|
|
1629
|
+
if (!existsSync12(path)) return [];
|
|
1392
1630
|
const out = [];
|
|
1393
1631
|
let inside = false;
|
|
1394
|
-
for (const line of
|
|
1632
|
+
for (const line of readFileSync11(path, "utf8").split("\n")) {
|
|
1395
1633
|
if (/^repos:\s*$/.test(line)) {
|
|
1396
1634
|
inside = true;
|
|
1397
1635
|
continue;
|
|
@@ -1404,12 +1642,12 @@ function readProducts(opsDir) {
|
|
|
1404
1642
|
return out;
|
|
1405
1643
|
}
|
|
1406
1644
|
function readBlockNames(opsDir, block, key) {
|
|
1407
|
-
const path =
|
|
1408
|
-
if (!
|
|
1645
|
+
const path = join12(opsDir, "org.yaml");
|
|
1646
|
+
if (!existsSync12(path)) return [];
|
|
1409
1647
|
const names = [];
|
|
1410
1648
|
const pattern = new RegExp(`\\b${key}:\\s*([\\w.-]+)`);
|
|
1411
1649
|
let inside = false;
|
|
1412
|
-
for (const line of
|
|
1650
|
+
for (const line of readFileSync11(path, "utf8").split("\n")) {
|
|
1413
1651
|
if (new RegExp(`^${block}:\\s*$`).test(line)) {
|
|
1414
1652
|
inside = true;
|
|
1415
1653
|
continue;
|
|
@@ -1428,35 +1666,35 @@ function unfinished(opsDir, staffCount) {
|
|
|
1428
1666
|
["business", "org/business.md"],
|
|
1429
1667
|
["priorities", "org/priorities.md"]
|
|
1430
1668
|
]) {
|
|
1431
|
-
const path =
|
|
1432
|
-
if (!
|
|
1669
|
+
const path = join12(opsDir, rel);
|
|
1670
|
+
if (!existsSync12(path) || looksUnwritten(readFileSync11(path, "utf8"))) out.push(id);
|
|
1433
1671
|
}
|
|
1434
1672
|
return out;
|
|
1435
1673
|
}
|
|
1436
1674
|
async function loadFrameworkComposer() {
|
|
1437
|
-
const path =
|
|
1675
|
+
const path = join12(templatesRoot(), "ops", "compose.mjs");
|
|
1438
1676
|
return await import(`file://${path}`);
|
|
1439
1677
|
}
|
|
1440
1678
|
async function joinTenant(root, org) {
|
|
1441
1679
|
mkdirSync(root, { recursive: true });
|
|
1442
1680
|
const cloned = [];
|
|
1443
|
-
const opsDir =
|
|
1444
|
-
if (!
|
|
1681
|
+
const opsDir = join12(root, "roster-ops");
|
|
1682
|
+
if (!existsSync12(opsDir)) {
|
|
1445
1683
|
await clone(`https://github.com/${org}/roster-ops.git`, opsDir);
|
|
1446
1684
|
cloned.push("roster-ops");
|
|
1447
1685
|
}
|
|
1448
|
-
if (!
|
|
1686
|
+
if (!existsSync12(join12(opsDir, "org.yaml"))) {
|
|
1449
1687
|
throw new Error(`${org}/roster-ops has no org.yaml, so it is not a roster tenant`);
|
|
1450
1688
|
}
|
|
1451
|
-
const { parseYaml } = await import(`file://${
|
|
1689
|
+
const { parseYaml } = await import(`file://${join12(opsDir, "compose.mjs")}`).then(
|
|
1452
1690
|
(m) => m
|
|
1453
1691
|
);
|
|
1454
|
-
const orgFile = parseYaml(
|
|
1692
|
+
const orgFile = parseYaml(readFileSync11(join12(opsDir, "org.yaml"), "utf8"), "org.yaml");
|
|
1455
1693
|
for (const person of orgFile.staff ?? []) {
|
|
1456
1694
|
const dir = person.dir ?? person.handle;
|
|
1457
|
-
if (
|
|
1695
|
+
if (existsSync12(join12(root, dir))) continue;
|
|
1458
1696
|
try {
|
|
1459
|
-
await clone(`https://github.com/${org}/${dir}.git`,
|
|
1697
|
+
await clone(`https://github.com/${org}/${dir}.git`, join12(root, dir));
|
|
1460
1698
|
cloned.push(dir);
|
|
1461
1699
|
} catch {
|
|
1462
1700
|
}
|
|
@@ -1471,8 +1709,8 @@ function clone(url, into) {
|
|
|
1471
1709
|
|
|
1472
1710
|
// src/commands/hire.ts
|
|
1473
1711
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1474
|
-
import { existsSync as
|
|
1475
|
-
import { dirname as dirname5, join as
|
|
1712
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "fs";
|
|
1713
|
+
import { dirname as dirname5, join as join14 } from "path";
|
|
1476
1714
|
|
|
1477
1715
|
// src/lib/asks.ts
|
|
1478
1716
|
var ASK_KINDS = ["decision", "review", "chore"];
|
|
@@ -1515,7 +1753,7 @@ function short2(e) {
|
|
|
1515
1753
|
// src/lib/reviewgate.ts
|
|
1516
1754
|
import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1517
1755
|
import { tmpdir } from "os";
|
|
1518
|
-
import { join as
|
|
1756
|
+
import { join as join13 } from "path";
|
|
1519
1757
|
var PLAN_LIMIT = /upgrade to github (pro|team)|make this repository public/i;
|
|
1520
1758
|
function isPlanLimit(error) {
|
|
1521
1759
|
return PLAN_LIMIT.test(error ?? "");
|
|
@@ -1635,8 +1873,8 @@ var GATE_RULESET = {
|
|
|
1635
1873
|
bypass_actors: [{ actor_id: 5, actor_type: "RepositoryRole", bypass_mode: "pull_request" }]
|
|
1636
1874
|
};
|
|
1637
1875
|
async function addGate(repo) {
|
|
1638
|
-
const dir = mkdtempSync(
|
|
1639
|
-
const body =
|
|
1876
|
+
const dir = mkdtempSync(join13(tmpdir(), "roster-gate-"));
|
|
1877
|
+
const body = join13(dir, "ruleset.json");
|
|
1640
1878
|
try {
|
|
1641
1879
|
writeFileSync2(body, JSON.stringify(GATE_RULESET));
|
|
1642
1880
|
const res = await api(`repos/${repo}/rulesets`, ["-X", "POST", "--input", body]);
|
|
@@ -1719,7 +1957,7 @@ function buildPlan(ws, org, handle, opts, parseYaml) {
|
|
|
1719
1957
|
const dir = opts.dir ?? handle;
|
|
1720
1958
|
const siblings = (org.staff ?? []).map((s) => ({
|
|
1721
1959
|
entry: s,
|
|
1722
|
-
manifest: readManifest(
|
|
1960
|
+
manifest: readManifest(join14(ws.root, s.dir ?? s.handle), parseYaml)
|
|
1723
1961
|
})).filter((s) => s.manifest);
|
|
1724
1962
|
const model = opts.model ?? org.defaults?.model ?? "claude-opus-5-5";
|
|
1725
1963
|
const schedule = opts.schedule ?? nextSlot(
|
|
@@ -1880,10 +2118,10 @@ function inferAppSlug(sampleApp, sampleHandle, handle) {
|
|
|
1880
2118
|
return sampleApp.slice(0, sampleApp.length - sampleHandle.length) + handle;
|
|
1881
2119
|
}
|
|
1882
2120
|
function readManifest(root, parseYaml) {
|
|
1883
|
-
const path =
|
|
1884
|
-
if (!
|
|
2121
|
+
const path = join14(root, "staff.yaml");
|
|
2122
|
+
if (!existsSync13(path)) return null;
|
|
1885
2123
|
try {
|
|
1886
|
-
return parseYaml(
|
|
2124
|
+
return parseYaml(readFileSync12(path, "utf8"), "staff.yaml");
|
|
1887
2125
|
} catch {
|
|
1888
2126
|
return null;
|
|
1889
2127
|
}
|
|
@@ -1896,7 +2134,7 @@ function printPlan(plan2, ws) {
|
|
|
1896
2134
|
`);
|
|
1897
2135
|
process.stdout.write(` repo ${staff.brain}
|
|
1898
2136
|
`);
|
|
1899
|
-
process.stdout.write(` directory ${
|
|
2137
|
+
process.stdout.write(` directory ${join14(ws.root, plan2.dir)}
|
|
1900
2138
|
`);
|
|
1901
2139
|
process.stdout.write(` schedule ${staff.schedule} (${staff.timeout}m ceiling)
|
|
1902
2140
|
`);
|
|
@@ -1998,14 +2236,14 @@ async function applyPlan(ws, plan2, opts) {
|
|
|
1998
2236
|
`);
|
|
1999
2237
|
return 1;
|
|
2000
2238
|
}
|
|
2001
|
-
const root =
|
|
2002
|
-
if (
|
|
2239
|
+
const root = join14(ws.root, plan2.dir);
|
|
2240
|
+
if (existsSync13(root)) {
|
|
2003
2241
|
process.stderr.write(`roster: ${root} already exists. Move it aside first.
|
|
2004
2242
|
`);
|
|
2005
2243
|
return 1;
|
|
2006
2244
|
}
|
|
2007
2245
|
for (const [rel, text] of plan2.files) {
|
|
2008
|
-
const dest =
|
|
2246
|
+
const dest = join14(root, rel);
|
|
2009
2247
|
mkdirSync2(dirname5(dest), { recursive: true });
|
|
2010
2248
|
writeFileSync3(dest, text);
|
|
2011
2249
|
}
|
|
@@ -2090,9 +2328,9 @@ async function applyPlan(ws, plan2, opts) {
|
|
|
2090
2328
|
]).catch(() => void 0);
|
|
2091
2329
|
const withIssue = { ...plan2.staff, statusIssue: issue.data.number };
|
|
2092
2330
|
writeFileSync3(
|
|
2093
|
-
|
|
2331
|
+
join14(root, "staff.yaml"),
|
|
2094
2332
|
render(
|
|
2095
|
-
|
|
2333
|
+
readFileSync12(join14(brainTemplateDir(), "staff.yaml"), "utf8"),
|
|
2096
2334
|
tokensFor(plan2.org, withIssue),
|
|
2097
2335
|
"staff.yaml"
|
|
2098
2336
|
)
|
|
@@ -2107,7 +2345,7 @@ async function applyPlan(ws, plan2, opts) {
|
|
|
2107
2345
|
process.stdout.write(` added ${staff.handle} to org.yaml
|
|
2108
2346
|
`);
|
|
2109
2347
|
for (const c of plan2.commits) {
|
|
2110
|
-
const done = commitAndPush(
|
|
2348
|
+
const done = commitAndPush(join14(ws.root, c.dir), [c.file], c.message);
|
|
2111
2349
|
process.stdout.write(
|
|
2112
2350
|
done.pushed ? ` pushed ${c.repo}: ${c.file} (${done.sha})
|
|
2113
2351
|
` : done.committed ? ` committed ${c.repo}: ${c.file} but could not push: ${done.note}. Push it yourself.
|
|
@@ -2143,8 +2381,8 @@ async function ensureGate(repo, apps) {
|
|
|
2143
2381
|
}
|
|
2144
2382
|
function wirePeers(ws, plan2, root) {
|
|
2145
2383
|
const { staff } = plan2;
|
|
2146
|
-
const manifest =
|
|
2147
|
-
let mineText =
|
|
2384
|
+
const manifest = join14(root, "staff.yaml");
|
|
2385
|
+
let mineText = readFileSync12(manifest, "utf8");
|
|
2148
2386
|
for (const p of plan2.peers) {
|
|
2149
2387
|
mineText = insertUnder(
|
|
2150
2388
|
mineText,
|
|
@@ -2154,17 +2392,17 @@ function wirePeers(ws, plan2, root) {
|
|
|
2154
2392
|
}
|
|
2155
2393
|
writeFileSync3(manifest, mineText);
|
|
2156
2394
|
for (const p of plan2.peers) {
|
|
2157
|
-
const path =
|
|
2158
|
-
if (!
|
|
2159
|
-
const text =
|
|
2395
|
+
const path = join14(ws.root, p.dir, "staff.yaml");
|
|
2396
|
+
if (!existsSync13(path)) continue;
|
|
2397
|
+
const text = readFileSync12(path, "utf8");
|
|
2160
2398
|
if (new RegExp(`handle: ${staff.handle}[,\\s}]`).test(text)) continue;
|
|
2161
2399
|
const line = ` - { handle: ${staff.handle}, brain: ${staff.brain}, label: from-${p.handle} }`;
|
|
2162
2400
|
writeFileSync3(path, insertUnder(text, "peers", line));
|
|
2163
2401
|
}
|
|
2164
2402
|
}
|
|
2165
2403
|
function addToOrgYaml(ws, plan2) {
|
|
2166
|
-
const path =
|
|
2167
|
-
const text =
|
|
2404
|
+
const path = join14(ws.opsDir, "org.yaml");
|
|
2405
|
+
const text = readFileSync12(path, "utf8");
|
|
2168
2406
|
const { staff } = plan2;
|
|
2169
2407
|
const staffLine = ` - { handle: ${staff.handle}, dir: ${plan2.dir}, name: ${staff.name}, schedule: "${staff.schedule}" }
|
|
2170
2408
|
`;
|
|
@@ -2403,8 +2641,8 @@ function parseFlags4(argv) {
|
|
|
2403
2641
|
}
|
|
2404
2642
|
|
|
2405
2643
|
// src/commands/doctor.ts
|
|
2406
|
-
import { existsSync as
|
|
2407
|
-
import { join as
|
|
2644
|
+
import { existsSync as existsSync17, readdirSync as readdirSync6, readFileSync as readFileSync16 } from "fs";
|
|
2645
|
+
import { join as join19 } from "path";
|
|
2408
2646
|
|
|
2409
2647
|
// src/lib/callable.ts
|
|
2410
2648
|
function accessLink(org, repo) {
|
|
@@ -2429,16 +2667,16 @@ function refusal(res) {
|
|
|
2429
2667
|
}
|
|
2430
2668
|
|
|
2431
2669
|
// src/lib/memory.ts
|
|
2432
|
-
import { existsSync as
|
|
2433
|
-
import { join as
|
|
2670
|
+
import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync13, statSync as statSync3 } from "fs";
|
|
2671
|
+
import { join as join15 } from "path";
|
|
2434
2672
|
var FACT_RE = /^- \*\*`([^`]+)`\*\*\s*·\s*(.*)$/;
|
|
2435
2673
|
var PROVENANCE_RE = /^\[([a-z][a-z0-9-]*)\]\s*/;
|
|
2436
2674
|
var NOTE_RE = /\s*·?\s*\[note\]\(([^)]+)\)\s*$/;
|
|
2437
2675
|
var SO_RE = /\*\*So:\*\*\s*/;
|
|
2438
2676
|
function parseMemory(dir) {
|
|
2439
|
-
const indexPath =
|
|
2440
|
-
if (!
|
|
2441
|
-
const lines =
|
|
2677
|
+
const indexPath = join15(dir, "INDEX.md");
|
|
2678
|
+
if (!existsSync14(indexPath)) throw new Error(`no memory index at ${indexPath}`);
|
|
2679
|
+
const lines = readFileSync13(indexPath, "utf8").split("\n");
|
|
2442
2680
|
const facts = [];
|
|
2443
2681
|
const sections = [];
|
|
2444
2682
|
const preamble = [];
|
|
@@ -2483,8 +2721,8 @@ function parseMemory(dir) {
|
|
|
2483
2721
|
raw
|
|
2484
2722
|
});
|
|
2485
2723
|
});
|
|
2486
|
-
const notesDir =
|
|
2487
|
-
const notes =
|
|
2724
|
+
const notesDir = join15(dir, "notes");
|
|
2725
|
+
const notes = existsSync14(notesDir) ? readdirSync4(notesDir).filter((f) => f.endsWith(".md")).sort() : [];
|
|
2488
2726
|
return {
|
|
2489
2727
|
facts,
|
|
2490
2728
|
sections,
|
|
@@ -2517,9 +2755,9 @@ function extractLinks(facts, notes, dir) {
|
|
|
2517
2755
|
scan(f.slug, f.raw);
|
|
2518
2756
|
}
|
|
2519
2757
|
for (const note of notes) {
|
|
2520
|
-
const path =
|
|
2521
|
-
if (!
|
|
2522
|
-
scan(note,
|
|
2758
|
+
const path = join15(dir, "notes", note);
|
|
2759
|
+
if (!existsSync14(path)) continue;
|
|
2760
|
+
scan(note, readFileSync13(path, "utf8"));
|
|
2523
2761
|
}
|
|
2524
2762
|
return links;
|
|
2525
2763
|
}
|
|
@@ -2568,7 +2806,7 @@ function lintMemory(doc, dir, budgets = DEFAULT_BUDGETS) {
|
|
|
2568
2806
|
}
|
|
2569
2807
|
if (f.note) {
|
|
2570
2808
|
linked.add(f.note.replace(/^notes\//, ""));
|
|
2571
|
-
if (!
|
|
2809
|
+
if (!existsSync14(join15(dir, f.note))) {
|
|
2572
2810
|
problems.push({
|
|
2573
2811
|
level: "error",
|
|
2574
2812
|
rule: "dangling-note",
|
|
@@ -2629,8 +2867,8 @@ function lintMemory(doc, dir, budgets = DEFAULT_BUDGETS) {
|
|
|
2629
2867
|
function overBudget(doc, dir, budgets) {
|
|
2630
2868
|
const out = [];
|
|
2631
2869
|
const kb = (bytes) => Math.round(bytes / 1024);
|
|
2632
|
-
const index =
|
|
2633
|
-
const indexBytes =
|
|
2870
|
+
const index = join15(dir, "INDEX.md");
|
|
2871
|
+
const indexBytes = existsSync14(index) ? statSync3(index).size : 0;
|
|
2634
2872
|
if (indexBytes > budgets.indexKb * 1024) {
|
|
2635
2873
|
const longest = [...doc.facts].sort((a, b) => b.raw.length - a.raw.length).slice(0, 3).map((f) => `\`${f.slug}\` (${f.raw.length})`);
|
|
2636
2874
|
out.push({
|
|
@@ -2640,8 +2878,8 @@ function overBudget(doc, dir, budgets) {
|
|
|
2640
2878
|
file: "memory/INDEX.md"
|
|
2641
2879
|
});
|
|
2642
2880
|
}
|
|
2643
|
-
const log =
|
|
2644
|
-
const logBytes =
|
|
2881
|
+
const log = join15(dir, "..", "log", "decisions.md");
|
|
2882
|
+
const logBytes = existsSync14(log) ? statSync3(log).size : 0;
|
|
2645
2883
|
if (logBytes > budgets.decisionsKb * 1024) {
|
|
2646
2884
|
out.push({
|
|
2647
2885
|
level: "warning",
|
|
@@ -2654,9 +2892,9 @@ function overBudget(doc, dir, budgets) {
|
|
|
2654
2892
|
}
|
|
2655
2893
|
|
|
2656
2894
|
// src/lib/runs.ts
|
|
2657
|
-
import { existsSync as
|
|
2895
|
+
import { existsSync as existsSync15, mkdtempSync as mkdtempSync2, readFileSync as readFileSync14, rmSync as rmSync2 } from "fs";
|
|
2658
2896
|
import { tmpdir as tmpdir2 } from "os";
|
|
2659
|
-
import { join as
|
|
2897
|
+
import { join as join16 } from "path";
|
|
2660
2898
|
var KINDS2 = ["daily", "mention"];
|
|
2661
2899
|
var records = /* @__PURE__ */ new Map();
|
|
2662
2900
|
async function staffRuns(brain, handle, days = 30) {
|
|
@@ -2717,7 +2955,7 @@ async function staffRuns(brain, handle, days = 30) {
|
|
|
2717
2955
|
async function recordFor(brain, id) {
|
|
2718
2956
|
const key = `${brain}#${id}`;
|
|
2719
2957
|
if (records.has(key)) return records.get(key);
|
|
2720
|
-
const dir = mkdtempSync2(
|
|
2958
|
+
const dir = mkdtempSync2(join16(tmpdir2(), "roster-run-"));
|
|
2721
2959
|
try {
|
|
2722
2960
|
const res = await ghJson([
|
|
2723
2961
|
"run",
|
|
@@ -2730,8 +2968,8 @@ async function recordFor(brain, id) {
|
|
|
2730
2968
|
"--dir",
|
|
2731
2969
|
dir
|
|
2732
2970
|
]);
|
|
2733
|
-
const file =
|
|
2734
|
-
const record = res.ok &&
|
|
2971
|
+
const file = join16(dir, "run.json");
|
|
2972
|
+
const record = res.ok && existsSync15(file) ? JSON.parse(readFileSync14(file, "utf8")) : null;
|
|
2735
2973
|
records.set(key, record);
|
|
2736
2974
|
return record;
|
|
2737
2975
|
} catch {
|
|
@@ -2766,28 +3004,28 @@ function budgetOf(value) {
|
|
|
2766
3004
|
// src/commands/upgrade.ts
|
|
2767
3005
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
2768
3006
|
import {
|
|
2769
|
-
existsSync as
|
|
3007
|
+
existsSync as existsSync16,
|
|
2770
3008
|
mkdirSync as mkdirSync3,
|
|
2771
3009
|
mkdtempSync as mkdtempSync4,
|
|
2772
3010
|
readdirSync as readdirSync5,
|
|
2773
|
-
readFileSync as
|
|
3011
|
+
readFileSync as readFileSync15,
|
|
2774
3012
|
rmSync as rmSync4,
|
|
2775
3013
|
writeFileSync as writeFileSync5
|
|
2776
3014
|
} from "fs";
|
|
2777
3015
|
import { tmpdir as tmpdir4 } from "os";
|
|
2778
|
-
import { dirname as dirname6, join as
|
|
3016
|
+
import { dirname as dirname6, join as join18 } from "path";
|
|
2779
3017
|
|
|
2780
3018
|
// src/lib/merge.ts
|
|
2781
3019
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
2782
3020
|
import { mkdtempSync as mkdtempSync3, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
2783
3021
|
import { tmpdir as tmpdir3 } from "os";
|
|
2784
|
-
import { join as
|
|
3022
|
+
import { join as join17 } from "path";
|
|
2785
3023
|
function merge3(current, base2, incoming) {
|
|
2786
|
-
const dir = mkdtempSync3(
|
|
3024
|
+
const dir = mkdtempSync3(join17(tmpdir3(), "roster-merge-"));
|
|
2787
3025
|
try {
|
|
2788
|
-
const c =
|
|
2789
|
-
const b =
|
|
2790
|
-
const i =
|
|
3026
|
+
const c = join17(dir, "current");
|
|
3027
|
+
const b = join17(dir, "base");
|
|
3028
|
+
const i = join17(dir, "incoming");
|
|
2791
3029
|
writeFileSync4(c, current);
|
|
2792
3030
|
writeFileSync4(b, base2);
|
|
2793
3031
|
writeFileSync4(i, incoming);
|
|
@@ -2860,7 +3098,7 @@ async function upgradeCommand(argv) {
|
|
|
2860
3098
|
const ws = findWorkspace(opts.ops);
|
|
2861
3099
|
const { parseYaml } = await loadComposer(ws.opsDir);
|
|
2862
3100
|
const tplDir = opsTemplateDir();
|
|
2863
|
-
const seedDir =
|
|
3101
|
+
const seedDir = join18(ws.opsDir, ".roster", "seed");
|
|
2864
3102
|
if (opts.baseline) {
|
|
2865
3103
|
return writeBaseline(ws.opsDir, seedDir, tplDir, opts.baseline);
|
|
2866
3104
|
}
|
|
@@ -2891,12 +3129,12 @@ function planAll(tplDir, seedDir, opsDir) {
|
|
|
2891
3129
|
}
|
|
2892
3130
|
function plan(rel, tplDir, seedDir, opsDir) {
|
|
2893
3131
|
const kind = classify(rel);
|
|
2894
|
-
const incoming =
|
|
2895
|
-
const currentPath =
|
|
2896
|
-
const basePath =
|
|
2897
|
-
const has = (p) =>
|
|
2898
|
-
const current = has(currentPath) ?
|
|
2899
|
-
const base2 = has(basePath) ?
|
|
3132
|
+
const incoming = readFileSync15(join18(tplDir, rel), "utf8");
|
|
3133
|
+
const currentPath = join18(opsDir, rel);
|
|
3134
|
+
const basePath = join18(seedDir, rel);
|
|
3135
|
+
const has = (p) => existsSync16(p);
|
|
3136
|
+
const current = has(currentPath) ? readFileSync15(currentPath, "utf8") : null;
|
|
3137
|
+
const base2 = has(basePath) ? readFileSync15(basePath, "utf8") : null;
|
|
2900
3138
|
if (current === null) {
|
|
2901
3139
|
return { rel, kind, verdict: "added", note: "new in the framework", next: incoming };
|
|
2902
3140
|
}
|
|
@@ -2998,7 +3236,7 @@ function apply(opsDir, seedDir, tplDir, plans) {
|
|
|
2998
3236
|
for (const p of plans) {
|
|
2999
3237
|
if (p.verdict === "edited-managed") edited++;
|
|
3000
3238
|
if (p.verdict === "conflict") {
|
|
3001
|
-
const out =
|
|
3239
|
+
const out = join18(opsDir, p.rel + ".roster-merge");
|
|
3002
3240
|
mkdirSync3(dirname6(out), { recursive: true });
|
|
3003
3241
|
writeFileSync5(out, p.rejected);
|
|
3004
3242
|
process.stdout.write(` wrote ${p.rel}.roster-merge \u2014 resolve it, then re-run
|
|
@@ -3014,7 +3252,7 @@ function apply(opsDir, seedDir, tplDir, plans) {
|
|
|
3014
3252
|
if (p.verdict === "same") seed(seedDir, tplDir, p.rel);
|
|
3015
3253
|
continue;
|
|
3016
3254
|
}
|
|
3017
|
-
const dest =
|
|
3255
|
+
const dest = join18(opsDir, p.rel);
|
|
3018
3256
|
mkdirSync3(dirname6(dest), { recursive: true });
|
|
3019
3257
|
writeFileSync5(dest, p.next);
|
|
3020
3258
|
seed(seedDir, tplDir, p.rel);
|
|
@@ -3025,7 +3263,7 @@ function apply(opsDir, seedDir, tplDir, plans) {
|
|
|
3025
3263
|
process.stdout.write(left ? `, ${left} left for you
|
|
3026
3264
|
|
|
3027
3265
|
` : "\n\n");
|
|
3028
|
-
if (!left) writeFileSync5(
|
|
3266
|
+
if (!left) writeFileSync5(join18(opsDir, ".roster-version"), stamp() + "\n");
|
|
3029
3267
|
if (edited) {
|
|
3030
3268
|
process.stdout.write(
|
|
3031
3269
|
` ${edited} framework file${edited === 1 ? "" : "s"} still carr${edited === 1 ? "ies" : "y"} a local edit. Move it into the framework, or doctor keeps failing.
|
|
@@ -3036,12 +3274,12 @@ function apply(opsDir, seedDir, tplDir, plans) {
|
|
|
3036
3274
|
return left || edited ? 1 : 0;
|
|
3037
3275
|
}
|
|
3038
3276
|
function seed(seedDir, tplDir, rel) {
|
|
3039
|
-
const dest =
|
|
3277
|
+
const dest = join18(seedDir, rel);
|
|
3040
3278
|
mkdirSync3(dirname6(dest), { recursive: true });
|
|
3041
|
-
writeFileSync5(dest,
|
|
3279
|
+
writeFileSync5(dest, readFileSync15(join18(tplDir, rel), "utf8"));
|
|
3042
3280
|
}
|
|
3043
3281
|
function writeBaseline(opsDir, seedDir, tplDir, ref) {
|
|
3044
|
-
const framework =
|
|
3282
|
+
const framework = join18(tplDir, "..", "..");
|
|
3045
3283
|
let files;
|
|
3046
3284
|
try {
|
|
3047
3285
|
files = execFileSync5(
|
|
@@ -3062,16 +3300,16 @@ function writeBaseline(opsDir, seedDir, tplDir, ref) {
|
|
|
3062
3300
|
encoding: "utf8",
|
|
3063
3301
|
maxBuffer: 16 * 1024 * 1024
|
|
3064
3302
|
});
|
|
3065
|
-
const dest =
|
|
3303
|
+
const dest = join18(seedDir, rel);
|
|
3066
3304
|
mkdirSync3(dirname6(dest), { recursive: true });
|
|
3067
3305
|
writeFileSync5(dest, text);
|
|
3068
3306
|
}
|
|
3069
|
-
writeFileSync5(
|
|
3307
|
+
writeFileSync5(join18(opsDir, ".roster-version"), `${ref}
|
|
3070
3308
|
`);
|
|
3071
3309
|
process.stdout.write(
|
|
3072
3310
|
`
|
|
3073
3311
|
Recorded ${files.length} files as the base, from the framework at ${ref}.
|
|
3074
|
-
Written to ${
|
|
3312
|
+
Written to ${join18(opsDir, ".roster", "seed")} \u2014 commit it; it is what makes the next
|
|
3075
3313
|
upgrade a merge rather than a copy.
|
|
3076
3314
|
|
|
3077
3315
|
Now run: roster upgrade
|
|
@@ -3084,7 +3322,7 @@ function stamp() {
|
|
|
3084
3322
|
try {
|
|
3085
3323
|
return execFileSync5(
|
|
3086
3324
|
"git",
|
|
3087
|
-
["-C",
|
|
3325
|
+
["-C", join18(opsTemplateDir(), "..", ".."), "rev-parse", "--short", "HEAD"],
|
|
3088
3326
|
{
|
|
3089
3327
|
encoding: "utf8",
|
|
3090
3328
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -3123,17 +3361,17 @@ function planBrains(ws, parseYaml) {
|
|
|
3123
3361
|
const out = [];
|
|
3124
3362
|
for (const entry of org.staff ?? []) {
|
|
3125
3363
|
const dir = entry.dir ?? entry.handle;
|
|
3126
|
-
const root =
|
|
3364
|
+
const root = join18(ws.root, dir);
|
|
3127
3365
|
const base2 = { handle: entry.handle, dir, root, files: [] };
|
|
3128
|
-
const manifestPath =
|
|
3129
|
-
if (!
|
|
3130
|
-
out.push({ ...base2, problem:
|
|
3366
|
+
const manifestPath = join18(root, "staff.yaml");
|
|
3367
|
+
if (!existsSync16(manifestPath)) {
|
|
3368
|
+
out.push({ ...base2, problem: existsSync16(root) ? "no staff.yaml" : "not checked out here" });
|
|
3131
3369
|
continue;
|
|
3132
3370
|
}
|
|
3133
3371
|
let want;
|
|
3134
3372
|
try {
|
|
3135
3373
|
const spec2 = specFromManifest(
|
|
3136
|
-
parseYaml(
|
|
3374
|
+
parseYaml(readFileSync15(manifestPath, "utf8"), "staff.yaml"),
|
|
3137
3375
|
dir
|
|
3138
3376
|
);
|
|
3139
3377
|
want = renderTree(brainTemplateDir(), tokensFor(orgSpec2, spec2));
|
|
@@ -3146,20 +3384,20 @@ function planBrains(ws, parseYaml) {
|
|
|
3146
3384
|
}
|
|
3147
3385
|
for (const [rel, text] of want) {
|
|
3148
3386
|
const kind = classifyBrain(rel);
|
|
3149
|
-
const live =
|
|
3150
|
-
if (!
|
|
3387
|
+
const live = join18(root, rel);
|
|
3388
|
+
if (!existsSync16(live)) {
|
|
3151
3389
|
base2.files.push({ rel, kind, verdict: "missing", next: text });
|
|
3152
3390
|
continue;
|
|
3153
3391
|
}
|
|
3154
3392
|
if (kind === "scaffold") continue;
|
|
3155
|
-
const have =
|
|
3393
|
+
const have = readFileSync15(live, "utf8");
|
|
3156
3394
|
base2.files.push(
|
|
3157
3395
|
have === text ? { rel, kind, verdict: "same" } : { rel, kind, verdict: "regenerate", next: text, diff: unified(have, text) }
|
|
3158
3396
|
);
|
|
3159
3397
|
}
|
|
3160
3398
|
for (const rel of callerFiles(root)) {
|
|
3161
3399
|
if (want.has(rel)) continue;
|
|
3162
|
-
const text =
|
|
3400
|
+
const text = readFileSync15(join18(root, rel), "utf8");
|
|
3163
3401
|
if (!text.includes(`${orgSpec2.opsRepo}/.github/workflows/session.yaml`)) continue;
|
|
3164
3402
|
base2.files.push({ rel, kind: "generated", verdict: "obsolete" });
|
|
3165
3403
|
}
|
|
@@ -3168,14 +3406,14 @@ function planBrains(ws, parseYaml) {
|
|
|
3168
3406
|
return out;
|
|
3169
3407
|
}
|
|
3170
3408
|
function unified(before, after) {
|
|
3171
|
-
const dir = mkdtempSync4(
|
|
3409
|
+
const dir = mkdtempSync4(join18(tmpdir4(), "roster-diff-"));
|
|
3172
3410
|
try {
|
|
3173
|
-
writeFileSync5(
|
|
3174
|
-
writeFileSync5(
|
|
3411
|
+
writeFileSync5(join18(dir, "a"), before);
|
|
3412
|
+
writeFileSync5(join18(dir, "b"), after);
|
|
3175
3413
|
try {
|
|
3176
3414
|
execFileSync5(
|
|
3177
3415
|
"diff",
|
|
3178
|
-
["-u", "--label", "yours", "--label", "generated",
|
|
3416
|
+
["-u", "--label", "yours", "--label", "generated", join18(dir, "a"), join18(dir, "b")],
|
|
3179
3417
|
{ encoding: "utf8" }
|
|
3180
3418
|
);
|
|
3181
3419
|
return "";
|
|
@@ -3188,8 +3426,8 @@ function unified(before, after) {
|
|
|
3188
3426
|
}
|
|
3189
3427
|
}
|
|
3190
3428
|
function callerFiles(root) {
|
|
3191
|
-
const dir =
|
|
3192
|
-
if (!
|
|
3429
|
+
const dir = join18(root, ".github", "workflows");
|
|
3430
|
+
if (!existsSync16(dir)) return [];
|
|
3193
3431
|
return readdirSync5(dir).filter((n) => n.endsWith(".yaml") || n.endsWith(".yml")).map((n) => `.github/workflows/${n}`);
|
|
3194
3432
|
}
|
|
3195
3433
|
function reportBrains(brains, opts) {
|
|
@@ -3237,12 +3475,12 @@ function applyBrains(brains) {
|
|
|
3237
3475
|
for (const b of brains) {
|
|
3238
3476
|
for (const f of b.files) {
|
|
3239
3477
|
if (f.verdict === "obsolete") {
|
|
3240
|
-
rmSync4(
|
|
3478
|
+
rmSync4(join18(b.root, f.rel), { force: true });
|
|
3241
3479
|
wrote++;
|
|
3242
3480
|
continue;
|
|
3243
3481
|
}
|
|
3244
3482
|
if (f.next === void 0) continue;
|
|
3245
|
-
const dest =
|
|
3483
|
+
const dest = join18(b.root, f.rel);
|
|
3246
3484
|
mkdirSync3(dirname6(dest), { recursive: true });
|
|
3247
3485
|
writeFileSync5(dest, f.next);
|
|
3248
3486
|
wrote++;
|
|
@@ -3368,7 +3606,7 @@ function checkWorkspace(ws, org) {
|
|
|
3368
3606
|
});
|
|
3369
3607
|
}
|
|
3370
3608
|
try {
|
|
3371
|
-
const plans = planAll(opsTemplateDir(),
|
|
3609
|
+
const plans = planAll(opsTemplateDir(), join19(ws.opsDir, ".roster", "seed"), ws.opsDir);
|
|
3372
3610
|
const edited = plans.filter((p) => p.verdict === "edited-managed");
|
|
3373
3611
|
const stale = plans.filter((p) => p.next !== void 0);
|
|
3374
3612
|
const unmergeable = plans.filter((p) => p.verdict === "conflict" || p.verdict === "no-base");
|
|
@@ -3417,7 +3655,7 @@ async function checkAgent(ws, org) {
|
|
|
3417
3655
|
const scope = "workspace";
|
|
3418
3656
|
let agent;
|
|
3419
3657
|
try {
|
|
3420
|
-
const { resolveAgent } = await import(`file://${
|
|
3658
|
+
const { resolveAgent } = await import(`file://${join19(ws.opsDir, "agents.mjs")}`);
|
|
3421
3659
|
agent = resolveAgent(org, {});
|
|
3422
3660
|
} catch (err) {
|
|
3423
3661
|
return [
|
|
@@ -3431,8 +3669,8 @@ async function checkAgent(ws, org) {
|
|
|
3431
3669
|
];
|
|
3432
3670
|
}
|
|
3433
3671
|
const out = [{ scope, level: "ok", id: "agent", title: `runs on ${agent.id}` }];
|
|
3434
|
-
const orgFile =
|
|
3435
|
-
if (
|
|
3672
|
+
const orgFile = join19(ws.opsDir, "org.yaml");
|
|
3673
|
+
if (existsSync17(orgFile) && /FILL IN/.test(readFileSync16(orgFile, "utf8"))) {
|
|
3436
3674
|
out.push({
|
|
3437
3675
|
scope,
|
|
3438
3676
|
level: "fail",
|
|
@@ -3442,8 +3680,8 @@ async function checkAgent(ws, org) {
|
|
|
3442
3680
|
});
|
|
3443
3681
|
}
|
|
3444
3682
|
if (!agent.config) return out;
|
|
3445
|
-
const path =
|
|
3446
|
-
if (!
|
|
3683
|
+
const path = join19(ws.opsDir, agent.config.path);
|
|
3684
|
+
if (!existsSync17(path)) {
|
|
3447
3685
|
out.push({
|
|
3448
3686
|
scope,
|
|
3449
3687
|
level: "fail",
|
|
@@ -3453,7 +3691,7 @@ async function checkAgent(ws, org) {
|
|
|
3453
3691
|
});
|
|
3454
3692
|
return out;
|
|
3455
3693
|
}
|
|
3456
|
-
if (/FILL IN/.test(
|
|
3694
|
+
if (/FILL IN/.test(readFileSync16(path, "utf8"))) {
|
|
3457
3695
|
out.push({
|
|
3458
3696
|
scope,
|
|
3459
3697
|
level: "fail",
|
|
@@ -3465,8 +3703,8 @@ async function checkAgent(ws, org) {
|
|
|
3465
3703
|
return out;
|
|
3466
3704
|
}
|
|
3467
3705
|
function checkBusiness(ws) {
|
|
3468
|
-
const path =
|
|
3469
|
-
if (!
|
|
3706
|
+
const path = join19(ws.opsDir, "org", "business.md");
|
|
3707
|
+
if (!existsSync17(path)) {
|
|
3470
3708
|
return [
|
|
3471
3709
|
{
|
|
3472
3710
|
scope: "workspace",
|
|
@@ -3477,7 +3715,7 @@ function checkBusiness(ws) {
|
|
|
3477
3715
|
}
|
|
3478
3716
|
];
|
|
3479
3717
|
}
|
|
3480
|
-
if (looksUnwritten(
|
|
3718
|
+
if (looksUnwritten(readFileSync16(path, "utf8"))) {
|
|
3481
3719
|
return [
|
|
3482
3720
|
{
|
|
3483
3721
|
scope: "workspace",
|
|
@@ -3491,8 +3729,8 @@ function checkBusiness(ws) {
|
|
|
3491
3729
|
return [{ scope: "workspace", level: "ok", id: "business", title: "org/business.md is written" }];
|
|
3492
3730
|
}
|
|
3493
3731
|
function checkPriorities(ws) {
|
|
3494
|
-
const path =
|
|
3495
|
-
if (!
|
|
3732
|
+
const path = join19(ws.opsDir, "org", "priorities.md");
|
|
3733
|
+
if (!existsSync17(path)) {
|
|
3496
3734
|
return [
|
|
3497
3735
|
{
|
|
3498
3736
|
scope: "workspace",
|
|
@@ -3503,7 +3741,7 @@ function checkPriorities(ws) {
|
|
|
3503
3741
|
}
|
|
3504
3742
|
];
|
|
3505
3743
|
}
|
|
3506
|
-
if (looksUnwritten(
|
|
3744
|
+
if (looksUnwritten(readFileSync16(path, "utf8"))) {
|
|
3507
3745
|
return [
|
|
3508
3746
|
{
|
|
3509
3747
|
scope: "workspace",
|
|
@@ -3522,8 +3760,8 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
3522
3760
|
const scope = entry.handle;
|
|
3523
3761
|
const out = [];
|
|
3524
3762
|
const dir = entry.dir ?? entry.handle;
|
|
3525
|
-
const root =
|
|
3526
|
-
if (!
|
|
3763
|
+
const root = join19(ws.root, dir);
|
|
3764
|
+
if (!existsSync17(root)) {
|
|
3527
3765
|
return [
|
|
3528
3766
|
{
|
|
3529
3767
|
scope,
|
|
@@ -3534,9 +3772,9 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
3534
3772
|
}
|
|
3535
3773
|
];
|
|
3536
3774
|
}
|
|
3537
|
-
const manifestPath =
|
|
3775
|
+
const manifestPath = join19(root, "staff.yaml");
|
|
3538
3776
|
let manifest = {};
|
|
3539
|
-
if (!
|
|
3777
|
+
if (!existsSync17(manifestPath)) {
|
|
3540
3778
|
out.push({
|
|
3541
3779
|
scope,
|
|
3542
3780
|
level: "fail",
|
|
@@ -3546,7 +3784,7 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
3546
3784
|
});
|
|
3547
3785
|
} else {
|
|
3548
3786
|
try {
|
|
3549
|
-
manifest = composer.parseYaml(
|
|
3787
|
+
manifest = composer.parseYaml(readFileSync16(manifestPath, "utf8"), "staff.yaml");
|
|
3550
3788
|
if (manifest.handle !== entry.handle) {
|
|
3551
3789
|
out.push({
|
|
3552
3790
|
scope,
|
|
@@ -3575,9 +3813,9 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
3575
3813
|
});
|
|
3576
3814
|
}
|
|
3577
3815
|
}
|
|
3578
|
-
const charterPath =
|
|
3816
|
+
const charterPath = join19(root, "CHARTER.md");
|
|
3579
3817
|
out.push(
|
|
3580
|
-
|
|
3818
|
+
existsSync17(charterPath) ? { scope, level: "ok", id: "charter", title: "CHARTER.md present" } : {
|
|
3581
3819
|
scope,
|
|
3582
3820
|
level: "fail",
|
|
3583
3821
|
id: "charter",
|
|
@@ -3585,7 +3823,7 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
3585
3823
|
fix: "The charter is the personality; nothing else supplies it."
|
|
3586
3824
|
}
|
|
3587
3825
|
);
|
|
3588
|
-
if (
|
|
3826
|
+
if (existsSync17(charterPath) && looksUnwritten(readFileSync16(charterPath, "utf8"))) {
|
|
3589
3827
|
out.push({
|
|
3590
3828
|
scope,
|
|
3591
3829
|
level: "warn",
|
|
@@ -3594,8 +3832,8 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
3594
3832
|
fix: `roster brief charter ${entry.handle}, or "Write the charter" on the portal's Staff screen`
|
|
3595
3833
|
});
|
|
3596
3834
|
}
|
|
3597
|
-
const memDir =
|
|
3598
|
-
if (!
|
|
3835
|
+
const memDir = join19(root, "memory");
|
|
3836
|
+
if (!existsSync17(join19(memDir, "INDEX.md"))) {
|
|
3599
3837
|
out.push({
|
|
3600
3838
|
scope,
|
|
3601
3839
|
level: "fail",
|
|
@@ -3692,8 +3930,8 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
3692
3930
|
fix: "A private reusable workflow is only callable inside its own org, so this can never resolve."
|
|
3693
3931
|
});
|
|
3694
3932
|
}
|
|
3695
|
-
const local =
|
|
3696
|
-
if (!
|
|
3933
|
+
const local = join19(ws.opsDir, ref.slice(opsRepo.length + 1));
|
|
3934
|
+
if (!existsSync17(local)) {
|
|
3697
3935
|
out.push({
|
|
3698
3936
|
scope,
|
|
3699
3937
|
level: "fail",
|
|
@@ -3711,7 +3949,7 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
3711
3949
|
title: `2 callers, both pointing at ${opsRepo}`
|
|
3712
3950
|
});
|
|
3713
3951
|
}
|
|
3714
|
-
const missing = (manifest.surfaces ?? []).map((s) => s.path).filter((p) => p && !
|
|
3952
|
+
const missing = (manifest.surfaces ?? []).map((s) => s.path).filter((p) => p && !existsSync17(join19(root, p)));
|
|
3715
3953
|
if (missing.length) {
|
|
3716
3954
|
out.push({
|
|
3717
3955
|
scope,
|
|
@@ -3755,10 +3993,10 @@ function inferredCeiling(cancelled) {
|
|
|
3755
3993
|
return most >= 2 ? best : null;
|
|
3756
3994
|
}
|
|
3757
3995
|
function readCallers(root, handle) {
|
|
3758
|
-
const dir =
|
|
3759
|
-
if (!
|
|
3996
|
+
const dir = join19(root, ".github", "workflows");
|
|
3997
|
+
if (!existsSync17(dir)) return [];
|
|
3760
3998
|
const mine = new RegExp(`^${handle}-(daily|mention)\\.ya?ml$`);
|
|
3761
|
-
return readdirSync6(dir).filter((f) => mine.test(f)).sort().map((name) => ({ name, text:
|
|
3999
|
+
return readdirSync6(dir).filter((f) => mine.test(f)).sort().map((name) => ({ name, text: readFileSync16(join19(dir, name), "utf8") }));
|
|
3762
4000
|
}
|
|
3763
4001
|
async function checkOrgOnline(ws, org) {
|
|
3764
4002
|
const out = [];
|
|
@@ -3838,10 +4076,10 @@ async function checkReviewGates(ws, org, composer) {
|
|
|
3838
4076
|
);
|
|
3839
4077
|
const apps = /* @__PURE__ */ new Set();
|
|
3840
4078
|
for (const s of org.staff ?? []) {
|
|
3841
|
-
const path =
|
|
3842
|
-
if (!
|
|
4079
|
+
const path = join19(ws.root, s.dir ?? s.handle, "staff.yaml");
|
|
4080
|
+
if (!existsSync17(path)) continue;
|
|
3843
4081
|
try {
|
|
3844
|
-
const m = composer.parseYaml(
|
|
4082
|
+
const m = composer.parseYaml(readFileSync16(path, "utf8"), "staff.yaml");
|
|
3845
4083
|
for (const w of m.works_in ?? []) if (w?.repo) repos.add(String(w.repo));
|
|
3846
4084
|
for (const i of m.identities ?? []) if (i?.app) apps.add(String(i.app));
|
|
3847
4085
|
} catch {
|
|
@@ -4282,18 +4520,18 @@ import { writeFileSync as writeFileSync6 } from "fs";
|
|
|
4282
4520
|
|
|
4283
4521
|
// src/lib/export.ts
|
|
4284
4522
|
import { execFileSync as execFileSync6 } from "child_process";
|
|
4285
|
-
import { existsSync as
|
|
4286
|
-
import { join as
|
|
4523
|
+
import { existsSync as existsSync18, readdirSync as readdirSync7, readFileSync as readFileSync17, statSync as statSync4 } from "fs";
|
|
4524
|
+
import { join as join20, relative as relative3 } from "path";
|
|
4287
4525
|
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".next", "dist", "out", ".DS_Store"]);
|
|
4288
4526
|
function buildExport(ws, org, parseYaml, opts = {}) {
|
|
4289
4527
|
const staff = [];
|
|
4290
4528
|
for (const entry of org.staff ?? []) {
|
|
4291
4529
|
const dir = entry.dir ?? entry.handle;
|
|
4292
|
-
const root =
|
|
4293
|
-
if (!
|
|
4530
|
+
const root = join20(ws.root, dir);
|
|
4531
|
+
if (!existsSync18(root)) continue;
|
|
4294
4532
|
const manifest = readManifest2(root, parseYaml);
|
|
4295
|
-
const memDir =
|
|
4296
|
-
const doc =
|
|
4533
|
+
const memDir = join20(root, "memory");
|
|
4534
|
+
const doc = existsSync18(join20(memDir, "INDEX.md")) ? parseMemory(memDir) : { facts: [], sections: [], notes: [], preamble: "", links: [] };
|
|
4297
4535
|
const commits = gitLog(root, 25);
|
|
4298
4536
|
staff.push({
|
|
4299
4537
|
handle: entry.handle,
|
|
@@ -4313,7 +4551,7 @@ function buildExport(ws, org, parseYaml, opts = {}) {
|
|
|
4313
4551
|
sections: doc.sections,
|
|
4314
4552
|
notes: doc.notes,
|
|
4315
4553
|
links: doc.links,
|
|
4316
|
-
problems:
|
|
4554
|
+
problems: existsSync18(join20(memDir, "INDEX.md")) ? lintMemory(doc, memDir, budgetsFor(org, manifest)) : [],
|
|
4317
4555
|
surfaces: readSurfaces(root, manifest.surfaces ?? []),
|
|
4318
4556
|
recentCommits: commits,
|
|
4319
4557
|
factsChanged: factsChanged(root, opts.since ?? "14 days ago"),
|
|
@@ -4341,25 +4579,25 @@ function botName(raw) {
|
|
|
4341
4579
|
return String(raw).replace(/\[bot\]$/, "");
|
|
4342
4580
|
}
|
|
4343
4581
|
function readManifest2(root, parseYaml) {
|
|
4344
|
-
const path =
|
|
4345
|
-
if (!
|
|
4582
|
+
const path = join20(root, "staff.yaml");
|
|
4583
|
+
if (!existsSync18(path)) return {};
|
|
4346
4584
|
try {
|
|
4347
|
-
return parseYaml(
|
|
4585
|
+
return parseYaml(readFileSync17(path, "utf8"), "staff.yaml");
|
|
4348
4586
|
} catch {
|
|
4349
4587
|
return {};
|
|
4350
4588
|
}
|
|
4351
4589
|
}
|
|
4352
4590
|
function readRig(root, manifest, commits) {
|
|
4353
|
-
const wfDir =
|
|
4591
|
+
const wfDir = join20(root, ".github", "workflows");
|
|
4354
4592
|
const declared = manifest.surfaces ?? [];
|
|
4355
4593
|
return {
|
|
4356
|
-
workflows:
|
|
4357
|
-
hasCharter:
|
|
4358
|
-
charterStub: !
|
|
4359
|
-
hasManifest:
|
|
4360
|
-
missingSurfaces: declared.filter((s) => s?.path && !
|
|
4361
|
-
memoryBytes: bytesOf(
|
|
4362
|
-
notesBytes: dirBytes(
|
|
4594
|
+
workflows: existsSync18(wfDir) ? readdirSync7(wfDir).filter((f) => /\.ya?ml$/.test(f)).sort() : [],
|
|
4595
|
+
hasCharter: existsSync18(join20(root, "CHARTER.md")),
|
|
4596
|
+
charterStub: !existsSync18(join20(root, "CHARTER.md")) || looksUnwritten(readFileSync17(join20(root, "CHARTER.md"), "utf8")),
|
|
4597
|
+
hasManifest: existsSync18(join20(root, "staff.yaml")),
|
|
4598
|
+
missingSurfaces: declared.filter((s) => s?.path && !existsSync18(join20(root, s.path))).map((s) => s.path),
|
|
4599
|
+
memoryBytes: bytesOf(join20(root, "memory", "INDEX.md")),
|
|
4600
|
+
notesBytes: dirBytes(join20(root, "memory", "notes")),
|
|
4363
4601
|
lastCommit: commits[0],
|
|
4364
4602
|
lastMemoryCommit: gitLog(root, 1, ["--", "memory"])[0]
|
|
4365
4603
|
};
|
|
@@ -4372,20 +4610,20 @@ function bytesOf(path) {
|
|
|
4372
4610
|
}
|
|
4373
4611
|
}
|
|
4374
4612
|
function dirBytes(dir) {
|
|
4375
|
-
if (!
|
|
4613
|
+
if (!existsSync18(dir)) return 0;
|
|
4376
4614
|
return walk2(dir, dir).reduce((n, f) => n + f.bytes, 0);
|
|
4377
4615
|
}
|
|
4378
4616
|
function readSurfaces(root, declared) {
|
|
4379
|
-
return declared.filter((s) => s?.path &&
|
|
4617
|
+
return declared.filter((s) => s?.path && existsSync18(join20(root, s.path))).map((s) => ({
|
|
4380
4618
|
path: s.path,
|
|
4381
4619
|
render: s.render ?? "doc",
|
|
4382
|
-
files: walk2(
|
|
4620
|
+
files: walk2(join20(root, s.path), root)
|
|
4383
4621
|
}));
|
|
4384
4622
|
}
|
|
4385
4623
|
function walk2(dir, root, out = []) {
|
|
4386
4624
|
for (const name of readdirSync7(dir)) {
|
|
4387
4625
|
if (SKIP.has(name)) continue;
|
|
4388
|
-
const full =
|
|
4626
|
+
const full = join20(dir, name);
|
|
4389
4627
|
const st = statSync4(full);
|
|
4390
4628
|
if (st.isDirectory()) walk2(full, root, out);
|
|
4391
4629
|
else {
|
|
@@ -4482,15 +4720,15 @@ async function exportCommand(argv) {
|
|
|
4482
4720
|
}
|
|
4483
4721
|
|
|
4484
4722
|
// src/lib/audit.ts
|
|
4485
|
-
import { existsSync as
|
|
4486
|
-
import { join as
|
|
4723
|
+
import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
|
|
4724
|
+
import { join as join21 } from "path";
|
|
4487
4725
|
var LONG_WORDS = 3500;
|
|
4488
4726
|
var ECHO_CHARS = 60;
|
|
4489
4727
|
function auditPrompt(ws, view, roots) {
|
|
4490
4728
|
const out = [];
|
|
4491
4729
|
const text = (rel) => {
|
|
4492
|
-
const full =
|
|
4493
|
-
return
|
|
4730
|
+
const full = join21(ws.root, rel);
|
|
4731
|
+
return existsSync19(full) ? readFileSync18(full, "utf8") : "";
|
|
4494
4732
|
};
|
|
4495
4733
|
for (const layer of [...view.layers, ...view.runtime]) {
|
|
4496
4734
|
if (layer.missing) continue;
|
|
@@ -4540,12 +4778,12 @@ Decide which layer each one belongs in and remove it from the others.`
|
|
|
4540
4778
|
level: "warning",
|
|
4541
4779
|
title: `${words.toLocaleString()} words, read in full on every run`,
|
|
4542
4780
|
detail: `Past about ${LONG_WORDS.toLocaleString()} words a prompt is usually carrying paragraphs nobody has re-read since they were written. This is paid on every run.`,
|
|
4543
|
-
want: `This prompt is ${words.toLocaleString()} words and is read in full on every run. Find what can go: anything restated, anything that was true once, anything an agent would do anyway.
|
|
4781
|
+
want: `This prompt is ${words.toLocaleString()} words and is read in full on every run. Find what can go: anything restated, anything that was true once, anything an agent would do anyway. Cut more than you add, and tell me what you cut.`
|
|
4544
4782
|
});
|
|
4545
4783
|
}
|
|
4546
|
-
const bases = [ws.root, ...roots.map((r) =>
|
|
4784
|
+
const bases = [ws.root, ...roots.map((r) => join21(ws.root, r))];
|
|
4547
4785
|
for (const rel of namedPaths(view.composed)) {
|
|
4548
|
-
if (bases.some((b) =>
|
|
4786
|
+
if (bases.some((b) => existsSync19(join21(b, rel)))) continue;
|
|
4549
4787
|
out.push({
|
|
4550
4788
|
id: "dangling",
|
|
4551
4789
|
level: "warning",
|
|
@@ -4824,8 +5062,8 @@ function parseFlags7(argv) {
|
|
|
4824
5062
|
|
|
4825
5063
|
// src/commands/init.ts
|
|
4826
5064
|
import { execFileSync as execFileSync7 } from "child_process";
|
|
4827
|
-
import { existsSync as
|
|
4828
|
-
import { dirname as dirname7, join as
|
|
5065
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync4, readFileSync as readFileSync19, writeFileSync as writeFileSync7 } from "fs";
|
|
5066
|
+
import { dirname as dirname7, join as join22, resolve as resolve3 } from "path";
|
|
4829
5067
|
var initHelp = `
|
|
4830
5068
|
roster init --org <github-org> [--name "Acme"] [--human <login>] [--apply]
|
|
4831
5069
|
|
|
@@ -4870,9 +5108,9 @@ async function initCommand(argv) {
|
|
|
4870
5108
|
const name = opts.name ?? opts.org;
|
|
4871
5109
|
const opsName = opts.ops ?? "roster-ops";
|
|
4872
5110
|
const root = resolve3(opts.dir ?? process.cwd());
|
|
4873
|
-
const opsDir =
|
|
5111
|
+
const opsDir = join22(root, opsName);
|
|
4874
5112
|
const marker = opts.marker ?? human.split("-")[0];
|
|
4875
|
-
if (
|
|
5113
|
+
if (existsSync20(opsDir)) {
|
|
4876
5114
|
process.stderr.write(`roster: ${opsDir} already exists. This org is already initialised.
|
|
4877
5115
|
`);
|
|
4878
5116
|
return 2;
|
|
@@ -4919,14 +5157,14 @@ async function initCommand(argv) {
|
|
|
4919
5157
|
}
|
|
4920
5158
|
mkdirSync4(opsDir, { recursive: true });
|
|
4921
5159
|
for (const [rel, text] of files) {
|
|
4922
|
-
const dest =
|
|
5160
|
+
const dest = join22(opsDir, rel);
|
|
4923
5161
|
mkdirSync4(dirname7(dest), { recursive: true });
|
|
4924
5162
|
writeFileSync7(dest, text);
|
|
4925
5163
|
}
|
|
4926
5164
|
for (const rel of templateFiles(opsTemplateDir())) {
|
|
4927
|
-
const dest =
|
|
5165
|
+
const dest = join22(opsDir, ".roster", "seed", rel);
|
|
4928
5166
|
mkdirSync4(dirname7(dest), { recursive: true });
|
|
4929
|
-
writeFileSync7(dest,
|
|
5167
|
+
writeFileSync7(dest, readFileSync19(join22(opsTemplateDir(), rel), "utf8"));
|
|
4930
5168
|
}
|
|
4931
5169
|
process.stdout.write(` wrote ${files.size} files and the recorded base
|
|
4932
5170
|
`);
|
|
@@ -4976,7 +5214,7 @@ async function initCommand(argv) {
|
|
|
4976
5214
|
async function initFiles(o) {
|
|
4977
5215
|
const files = /* @__PURE__ */ new Map();
|
|
4978
5216
|
for (const rel of templateFiles(opsTemplateDir())) {
|
|
4979
|
-
files.set(rel,
|
|
5217
|
+
files.set(rel, readFileSync19(join22(opsTemplateDir(), rel), "utf8"));
|
|
4980
5218
|
}
|
|
4981
5219
|
const agent = o.agent ?? "claude-code-action";
|
|
4982
5220
|
const preset = await agentPreset(agent);
|
|
@@ -5001,7 +5239,7 @@ async function initFiles(o) {
|
|
|
5001
5239
|
return files;
|
|
5002
5240
|
}
|
|
5003
5241
|
async function agentPreset(id) {
|
|
5004
|
-
const path =
|
|
5242
|
+
const path = join22(opsTemplateDir(), "agents.mjs");
|
|
5005
5243
|
const { PRESETS } = await import(`file://${path}`);
|
|
5006
5244
|
const preset = PRESETS[id];
|
|
5007
5245
|
if (!preset) {
|
|
@@ -5135,7 +5373,7 @@ function stamp2() {
|
|
|
5135
5373
|
try {
|
|
5136
5374
|
return execFileSync7(
|
|
5137
5375
|
"git",
|
|
5138
|
-
["-C",
|
|
5376
|
+
["-C", join22(opsTemplateDir(), "..", ".."), "rev-parse", "--short", "HEAD"],
|
|
5139
5377
|
{
|
|
5140
5378
|
encoding: "utf8",
|
|
5141
5379
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -5171,7 +5409,7 @@ function parseFlags8(argv) {
|
|
|
5171
5409
|
}
|
|
5172
5410
|
|
|
5173
5411
|
// src/commands/lint.ts
|
|
5174
|
-
import { join as
|
|
5412
|
+
import { join as join23 } from "path";
|
|
5175
5413
|
var lintHelp = `
|
|
5176
5414
|
roster lint [handle]
|
|
5177
5415
|
|
|
@@ -5199,8 +5437,8 @@ async function lintCommand(argv) {
|
|
|
5199
5437
|
let errors = 0;
|
|
5200
5438
|
let warnings = 0;
|
|
5201
5439
|
for (const s of staff) {
|
|
5202
|
-
const root =
|
|
5203
|
-
const dir =
|
|
5440
|
+
const root = join23(ws.root, s.dir ?? s.handle);
|
|
5441
|
+
const dir = join23(root, "memory");
|
|
5204
5442
|
const budgets = budgetsFor(org, readManifest2(root, parseYaml));
|
|
5205
5443
|
let problems;
|
|
5206
5444
|
let factCount = 0;
|
|
@@ -5303,14 +5541,14 @@ function askBody(req) {
|
|
|
5303
5541
|
if (anchor?.path) {
|
|
5304
5542
|
out.push("", `They were looking at \`${anchor.path}\`.`);
|
|
5305
5543
|
const hunk = clip(anchor.patch ?? "");
|
|
5306
|
-
if (hunk) out.push("",
|
|
5544
|
+
if (hunk) out.push("", fence4(hunk, "diff"));
|
|
5307
5545
|
else if (anchor.patch) out.push("", "The diff for it is on the pull request.");
|
|
5308
5546
|
}
|
|
5309
5547
|
out.push(
|
|
5310
5548
|
"",
|
|
5311
5549
|
`**Answer on the ${it}, not here.** That is where it will be read${isPr ? ", and it is where the diff is" : ""}:`,
|
|
5312
5550
|
"",
|
|
5313
|
-
|
|
5551
|
+
fence4(`gh issue comment ${pr.number} --repo ${pr.repo} --body "\u2026"`, ""),
|
|
5314
5552
|
"",
|
|
5315
5553
|
...isPr ? [
|
|
5316
5554
|
pr.head ? `Push any change to \`${pr.head}\`. Close this issue once the reply is up.` : "Push any change to the pull request's branch. Close this issue once the reply is up."
|
|
@@ -5334,7 +5572,7 @@ function clip(patch) {
|
|
|
5334
5572
|
`\u2026 ${lines.length - MAX_PATCH_LINES} more lines, on the pull request`
|
|
5335
5573
|
].join("\n");
|
|
5336
5574
|
}
|
|
5337
|
-
function
|
|
5575
|
+
function fence4(text, lang) {
|
|
5338
5576
|
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
|
|
5339
5577
|
const ticks = "`".repeat(Math.max(3, longest + 1));
|
|
5340
5578
|
return `${ticks}${lang}
|
|
@@ -5538,13 +5776,13 @@ function execWithStdin(args, input) {
|
|
|
5538
5776
|
|
|
5539
5777
|
// src/lib/attach.ts
|
|
5540
5778
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
5541
|
-
import { existsSync as
|
|
5542
|
-
import { basename, extname, join as
|
|
5779
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
5780
|
+
import { basename, extname, join as join24 } from "path";
|
|
5543
5781
|
var MAX_UPLOAD = 25 * 1024 * 1024;
|
|
5544
5782
|
var ATTACH_DIR = "attachments";
|
|
5545
5783
|
function attach(opts) {
|
|
5546
5784
|
const { repoDir, repo, data, today } = opts;
|
|
5547
|
-
if (!
|
|
5785
|
+
if (!existsSync21(join24(repoDir, ".git"))) {
|
|
5548
5786
|
throw new Error(`${repo} is not checked out here, so there is nowhere to put the file`);
|
|
5549
5787
|
}
|
|
5550
5788
|
if (!data.length) throw new Error("that file is empty");
|
|
@@ -5552,8 +5790,8 @@ function attach(opts) {
|
|
|
5552
5790
|
throw new Error(`${basename(opts.name)} is larger than ${MAX_UPLOAD / 1024 / 1024}MB`);
|
|
5553
5791
|
}
|
|
5554
5792
|
const rel = free(repoDir, `${ATTACH_DIR}/${today}-${safeName(opts.name)}`);
|
|
5555
|
-
const full =
|
|
5556
|
-
mkdirSync5(
|
|
5793
|
+
const full = join24(repoDir, rel);
|
|
5794
|
+
mkdirSync5(join24(repoDir, ATTACH_DIR), { recursive: true });
|
|
5557
5795
|
writeFileSync8(full, data);
|
|
5558
5796
|
const git4 = (args) => execFileSync8("git", ["-C", repoDir, ...args], {
|
|
5559
5797
|
encoding: "utf8",
|
|
@@ -5581,12 +5819,12 @@ function safeName(raw) {
|
|
|
5581
5819
|
return stem + ext.replace(/[^a-z0-9.]/g, "");
|
|
5582
5820
|
}
|
|
5583
5821
|
function free(repoDir, rel) {
|
|
5584
|
-
if (!
|
|
5822
|
+
if (!existsSync21(join24(repoDir, rel))) return rel;
|
|
5585
5823
|
const ext = extname(rel);
|
|
5586
5824
|
const stem = rel.slice(0, rel.length - ext.length);
|
|
5587
5825
|
for (let n = 2; n < 500; n++) {
|
|
5588
5826
|
const next = `${stem}-${n}${ext}`;
|
|
5589
|
-
if (!
|
|
5827
|
+
if (!existsSync21(join24(repoDir, next))) return next;
|
|
5590
5828
|
}
|
|
5591
5829
|
throw new Error("too many files by that name today");
|
|
5592
5830
|
}
|
|
@@ -6006,7 +6244,7 @@ function short4(e) {
|
|
|
6006
6244
|
var FINISHED_HOURS = 6;
|
|
6007
6245
|
function parseTitle(handle, title, workflow = "") {
|
|
6008
6246
|
const m = new RegExp(
|
|
6009
|
-
`^${handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} (daily|manual|follow-on|mention|peer)(?: #(\\d+))?\\s*$`
|
|
6247
|
+
`^${handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} (daily|manual|follow-on|mention|peer|ignored)(?: #(\\d+))?\\s*$`
|
|
6010
6248
|
).exec(title.trim());
|
|
6011
6249
|
if (m) return { trigger: m[1], issue: m[2] ? Number(m[2]) : null };
|
|
6012
6250
|
if (/daily/i.test(workflow)) return { trigger: "daily", issue: null };
|
|
@@ -6020,6 +6258,7 @@ function shapeLive(handle, brain, raw, limit, now = Date.now()) {
|
|
|
6020
6258
|
for (const r of raw) {
|
|
6021
6259
|
if (r.conclusion === "skipped") continue;
|
|
6022
6260
|
const { trigger, issue } = parseTitle(handle, r.displayTitle ?? "", r.workflowName ?? "");
|
|
6261
|
+
if (trigger === "ignored") continue;
|
|
6023
6262
|
const run6 = {
|
|
6024
6263
|
id: Number(r.databaseId),
|
|
6025
6264
|
trigger,
|
|
@@ -6096,216 +6335,6 @@ async function markRead(thread) {
|
|
|
6096
6335
|
return res.ok;
|
|
6097
6336
|
}
|
|
6098
6337
|
|
|
6099
|
-
// src/lib/paste.ts
|
|
6100
|
-
var BEGIN = (path) => `<<<ROSTER FILE ${path}>>>`;
|
|
6101
|
-
var END = "<<<ROSTER END>>>";
|
|
6102
|
-
var BLOCK = /^[ \t]*<<<ROSTER FILE ([^>\n]+)>>>[ \t]*\n([\s\S]*?)\n?[ \t]*<<<ROSTER END>>>/gm;
|
|
6103
|
-
var MIN_WORDS = 40;
|
|
6104
|
-
function parsePaste(raw, targets) {
|
|
6105
|
-
const files = [];
|
|
6106
|
-
const problems = [];
|
|
6107
|
-
const text = raw.replace(/\r\n/g, "\n");
|
|
6108
|
-
if (!text.trim()) {
|
|
6109
|
-
return {
|
|
6110
|
-
files,
|
|
6111
|
-
problems: [{ id: "empty", message: "Nothing was pasted.", retry: "" }]
|
|
6112
|
-
};
|
|
6113
|
-
}
|
|
6114
|
-
const known = new Map(targets.map((t) => [t.path, t]));
|
|
6115
|
-
BLOCK.lastIndex = 0;
|
|
6116
|
-
for (const m of text.matchAll(BLOCK)) {
|
|
6117
|
-
const written = m[1].trim();
|
|
6118
|
-
const body = strip(m[2] ?? "");
|
|
6119
|
-
const target = resolveTarget(written, known);
|
|
6120
|
-
if (!target) {
|
|
6121
|
-
problems.push({
|
|
6122
|
-
id: "unknown-path",
|
|
6123
|
-
path: written,
|
|
6124
|
-
message: `The answer writes ${written}, which this brief did not ask for.`,
|
|
6125
|
-
retry: `Write only ${[...known.keys()].join(", ")}, using the sentinels exactly as given.`
|
|
6126
|
-
});
|
|
6127
|
-
continue;
|
|
6128
|
-
}
|
|
6129
|
-
files.push({
|
|
6130
|
-
path: target.path,
|
|
6131
|
-
text: ensureNewline(body),
|
|
6132
|
-
unchanged: body.trim() === target.before.trim()
|
|
6133
|
-
});
|
|
6134
|
-
}
|
|
6135
|
-
if (!files.length && !problems.length) {
|
|
6136
|
-
problems.push({
|
|
6137
|
-
id: "no-envelope",
|
|
6138
|
-
message: "Your AI answered in prose. The reply has no <<<ROSTER FILE ...>>> block, so there is nothing here that can be saved as a file.",
|
|
6139
|
-
retry: `Send that again as the finished file only, wrapped exactly like this and with nothing outside it:
|
|
6140
|
-
|
|
6141
|
-
${BEGIN(targets[0]?.path ?? "<path>")}
|
|
6142
|
-
...the whole file...
|
|
6143
|
-
${END}`
|
|
6144
|
-
});
|
|
6145
|
-
}
|
|
6146
|
-
for (const file of files) {
|
|
6147
|
-
const words = file.text.split(/\s+/).filter(Boolean).length;
|
|
6148
|
-
if (file.unchanged) {
|
|
6149
|
-
problems.push({
|
|
6150
|
-
id: "brief-echoed",
|
|
6151
|
-
path: file.path,
|
|
6152
|
-
message: `${file.path} came back byte for byte. Some models restate a long prompt before working.`,
|
|
6153
|
-
retry: "That is the file I sent you. Write the new version."
|
|
6154
|
-
});
|
|
6155
|
-
continue;
|
|
6156
|
-
}
|
|
6157
|
-
if (words < MIN_WORDS) {
|
|
6158
|
-
problems.push({
|
|
6159
|
-
id: "too-short",
|
|
6160
|
-
path: file.path,
|
|
6161
|
-
message: `${file.path} is ${words} words. That is a failed answer, not a short one.`,
|
|
6162
|
-
retry: "That is far too short. Interview me properly first, then write the whole file."
|
|
6163
|
-
});
|
|
6164
|
-
continue;
|
|
6165
|
-
}
|
|
6166
|
-
if (looksUnwritten(file.text)) {
|
|
6167
|
-
problems.push({
|
|
6168
|
-
id: "stub-echoed",
|
|
6169
|
-
path: file.path,
|
|
6170
|
-
message: `${file.path} still carries the scaffold's own headings, so the questions were never answered.`,
|
|
6171
|
-
retry: "You returned the template with its instructions still in it. Answer the questions and write the real file, with none of the scaffold's guidance left in it."
|
|
6172
|
-
});
|
|
6173
|
-
}
|
|
6174
|
-
}
|
|
6175
|
-
return { files, problems };
|
|
6176
|
-
}
|
|
6177
|
-
function resolveTarget(written, known) {
|
|
6178
|
-
const exact = known.get(written);
|
|
6179
|
-
if (exact) return exact;
|
|
6180
|
-
const tail2 = written.replace(/^(\.\/)+/, "").replace(/^\/+/, "");
|
|
6181
|
-
if (!tail2 || tail2.split("/").includes("..")) return void 0;
|
|
6182
|
-
const matches = [...known.values()].filter((t) => t.path === tail2 || t.path.endsWith(`/${tail2}`));
|
|
6183
|
-
return matches.length === 1 ? matches[0] : void 0;
|
|
6184
|
-
}
|
|
6185
|
-
function strip(body) {
|
|
6186
|
-
const trimmed = body.replace(/^\n+/, "").replace(/\s+$/, "");
|
|
6187
|
-
const fenced = /^(`{3,}|~{3,})[^\n]*\n([\s\S]*)\n\1\s*$/.exec(trimmed);
|
|
6188
|
-
return fenced ? fenced[2] : trimmed;
|
|
6189
|
-
}
|
|
6190
|
-
function ensureNewline(text) {
|
|
6191
|
-
return text.endsWith("\n") ? text : `${text}
|
|
6192
|
-
`;
|
|
6193
|
-
}
|
|
6194
|
-
|
|
6195
|
-
// src/lib/pastebrief.ts
|
|
6196
|
-
import { existsSync as existsSync21, readFileSync as readFileSync19 } from "fs";
|
|
6197
|
-
import { join as join24 } from "path";
|
|
6198
|
-
var WRITES = {
|
|
6199
|
-
discover: (c) => [`${c.opsName}/org/business.md`],
|
|
6200
|
-
voice: (c) => [`${c.opsName}/org/voice.md`],
|
|
6201
|
-
priorities: (c) => [`${c.opsName}/org/priorities.md`],
|
|
6202
|
-
charter: (c) => [`${c.dir}/CHARTER.md`]
|
|
6203
|
-
};
|
|
6204
|
-
var READS = {
|
|
6205
|
-
discover: (c) => [
|
|
6206
|
-
`${c.opsName}/org/operating.md`,
|
|
6207
|
-
`${c.opsName}/org/voice.md`,
|
|
6208
|
-
`${c.opsName}/org/guardrails.md`
|
|
6209
|
-
],
|
|
6210
|
-
voice: (c) => [`${c.opsName}/org/business.md`, `${c.opsName}/org/guardrails.md`],
|
|
6211
|
-
priorities: (c) => [`${c.opsName}/org/business.md`],
|
|
6212
|
-
/* A charter is the difference between this staff member and everyone else, so the peers'
|
|
6213
|
-
charters are the most useful thing in the room: without them the model writes a second
|
|
6214
|
-
copy of whoever it was shown. */
|
|
6215
|
-
charter: (c) => [
|
|
6216
|
-
`${c.opsName}/org/business.md`,
|
|
6217
|
-
`${c.opsName}/org/operating.md`,
|
|
6218
|
-
`${c.opsName}/org/voice.md`,
|
|
6219
|
-
`${c.opsName}/org/guardrails.md`,
|
|
6220
|
-
...c.peers.map((p) => `${p}/CHARTER.md`)
|
|
6221
|
-
]
|
|
6222
|
-
};
|
|
6223
|
-
function pasteable(kind) {
|
|
6224
|
-
return kind in WRITES;
|
|
6225
|
-
}
|
|
6226
|
-
function pasteBrief(ws, kind, rendered, ctx) {
|
|
6227
|
-
if (!pasteable(kind)) throw new Error(`there is no paste-mode brief for "${kind}"`);
|
|
6228
|
-
const c = { opsName: ws.opsName, dir: ctx.dir ?? "", peers: ctx.peers ?? [] };
|
|
6229
|
-
const writes = WRITES[kind](c);
|
|
6230
|
-
const reads = READS[kind](c).filter((rel) => !writes.includes(rel));
|
|
6231
|
-
const targets = writes.map((rel) => ({ path: rel, before: read2(ws, rel) }));
|
|
6232
|
-
const parts = [rendered.trimEnd(), "", "---", "", head(writes)];
|
|
6233
|
-
parts.push("", "## The files this refers to", "");
|
|
6234
|
-
parts.push(
|
|
6235
|
-
"You cannot open anything, so everything the brief mentions is below. Do not ask for files;",
|
|
6236
|
-
"if something you want is genuinely not here, say what and why, and work without it.",
|
|
6237
|
-
""
|
|
6238
|
-
);
|
|
6239
|
-
for (const rel of reads) parts.push(...fileBlock(ws, rel, "read this, do not rewrite it"));
|
|
6240
|
-
parts.push("## What you are writing", "");
|
|
6241
|
-
for (const rel of targets) {
|
|
6242
|
-
parts.push(...fileBlock(ws, rel.path, rel.before.trim() ? "replace this" : "currently empty"));
|
|
6243
|
-
}
|
|
6244
|
-
parts.push(...tail(writes));
|
|
6245
|
-
return { kind, text: `${parts.join("\n").replace(/\n{3,}/g, "\n\n")}
|
|
6246
|
-
`, targets };
|
|
6247
|
-
}
|
|
6248
|
-
function head(writes) {
|
|
6249
|
-
return [
|
|
6250
|
-
"## How to answer",
|
|
6251
|
-
"",
|
|
6252
|
-
"**You are talking to a person, not to a filesystem.** You cannot read or write anything.",
|
|
6253
|
-
"Everything you need is in this message, and your answer comes back by being pasted into a",
|
|
6254
|
-
"tool that turns it into " + (writes.length === 1 ? "a file" : "files") + ".",
|
|
6255
|
-
"",
|
|
6256
|
-
"**Interview first.** Ask one question at a time and wait. Do not produce the file in your",
|
|
6257
|
-
"first reply, and do not present a form. When you have enough, say so and write it."
|
|
6258
|
-
].join("\n");
|
|
6259
|
-
}
|
|
6260
|
-
function tail(writes) {
|
|
6261
|
-
const example = writes.map((rel) => `${BEGIN(rel)}
|
|
6262
|
-
...the whole file, exactly as it should be saved...
|
|
6263
|
-
${END}`).join("\n\n");
|
|
6264
|
-
return [
|
|
6265
|
-
"---",
|
|
6266
|
-
"",
|
|
6267
|
-
"## How to hand it back",
|
|
6268
|
-
"",
|
|
6269
|
-
"When the interview is done and you are writing the final version, wrap it like this:",
|
|
6270
|
-
"",
|
|
6271
|
-
"```",
|
|
6272
|
-
example,
|
|
6273
|
-
"```",
|
|
6274
|
-
"",
|
|
6275
|
-
`**Rules**, because the answer is parsed rather than read:`,
|
|
6276
|
-
"",
|
|
6277
|
-
"- The sentinel lines go on their own lines, spelled exactly as above.",
|
|
6278
|
-
"- Between them goes the **whole file**, exactly as it should be saved. No fences around it,",
|
|
6279
|
-
" no commentary inside it, no summary of what you changed.",
|
|
6280
|
-
"- Anything you want to say to the person goes **outside** the block. That part is ignored by",
|
|
6281
|
-
" the tool and read by them, so it is the right place for what you were unsure about.",
|
|
6282
|
-
`- Write only ${writes.join(" and ")}. Nothing else.`,
|
|
6283
|
-
"",
|
|
6284
|
-
"Nothing is saved automatically. They will see a diff and decide."
|
|
6285
|
-
];
|
|
6286
|
-
}
|
|
6287
|
-
function fileBlock(ws, rel, note) {
|
|
6288
|
-
const body = read2(ws, rel);
|
|
6289
|
-
if (!body.trim()) return [`### \`${rel}\``, "", `*Empty or absent (${note}).*`, ""];
|
|
6290
|
-
return [`### \`${rel}\``, "", `*${note}*`, "", fence4(body), ""];
|
|
6291
|
-
}
|
|
6292
|
-
function read2(ws, rel) {
|
|
6293
|
-
const full = join24(ws.root, rel);
|
|
6294
|
-
return existsSync21(full) ? readFileSync19(full, "utf8") : "";
|
|
6295
|
-
}
|
|
6296
|
-
function fence4(text) {
|
|
6297
|
-
const longest = (text.match(/`{3,}/g) ?? []).reduce((n, m) => Math.max(n, m.length), 2);
|
|
6298
|
-
const bars = "`".repeat(longest + 1);
|
|
6299
|
-
return `${bars}
|
|
6300
|
-
${text.trimEnd()}
|
|
6301
|
-
${bars}`;
|
|
6302
|
-
}
|
|
6303
|
-
function briefTemplate(kind) {
|
|
6304
|
-
const path = join24(briefTemplateDir(), `${kind}.md`);
|
|
6305
|
-
if (!existsSync21(path)) throw new Error(`no brief template for "${kind}"`);
|
|
6306
|
-
return readFileSync19(path, "utf8");
|
|
6307
|
-
}
|
|
6308
|
-
|
|
6309
6338
|
// src/lib/runonce.ts
|
|
6310
6339
|
import { execFile as execFile6 } from "child_process";
|
|
6311
6340
|
function dailyWorkflow(handle) {
|
|
@@ -6761,6 +6790,11 @@ async function portalCommand(argv) {
|
|
|
6761
6790
|
const progressCache = /* @__PURE__ */ new Map();
|
|
6762
6791
|
const LABEL_TTL = 3e5;
|
|
6763
6792
|
const brainDirs = () => (readOrg(ws.opsDir, parseYaml).staff ?? []).map((s) => s.dir ?? s.handle);
|
|
6793
|
+
const amendTargets = (view) => [
|
|
6794
|
+
...new Set(
|
|
6795
|
+
view.layers.filter((l) => !l.missing && isWritable(ws, l.path, brainDirs())).map((l) => l.path)
|
|
6796
|
+
)
|
|
6797
|
+
];
|
|
6764
6798
|
const knownRepos = () => {
|
|
6765
6799
|
const org = readOrg(ws.opsDir, parseYaml);
|
|
6766
6800
|
return (org.repos ?? []).map((r) => ({
|
|
@@ -7487,7 +7521,41 @@ async function portalCommand(argv) {
|
|
|
7487
7521
|
"content-type": "text/plain; charset=utf-8",
|
|
7488
7522
|
"cache-control": "no-store"
|
|
7489
7523
|
});
|
|
7490
|
-
res.end(amendBrief(w, view, tokens, want));
|
|
7524
|
+
res.end(amendBrief(w, view, tokens, want, amendTargets(view)));
|
|
7525
|
+
return;
|
|
7526
|
+
}
|
|
7527
|
+
if (url.pathname === "/api/amend/check") {
|
|
7528
|
+
if (req.method !== "POST") {
|
|
7529
|
+
res.writeHead(405).end("POST only");
|
|
7530
|
+
return;
|
|
7531
|
+
}
|
|
7532
|
+
body(req).then((raw) => {
|
|
7533
|
+
const payload = JSON.parse(raw || "{}");
|
|
7534
|
+
const org = readOrg(w.opsDir, parseYaml);
|
|
7535
|
+
const entry = (org.staff ?? []).find((s) => s.handle === payload.staff);
|
|
7536
|
+
const kind = payload.kind ?? "daily";
|
|
7537
|
+
if (!entry || !KINDS.includes(kind))
|
|
7538
|
+
throw new Error("bad request");
|
|
7539
|
+
const dir = entry.dir ?? entry.handle;
|
|
7540
|
+
const view = promptView(w, compose, entry.handle, join28(w.root, dir), kind);
|
|
7541
|
+
const targets = amendTargets(view).map((path) => ({
|
|
7542
|
+
path,
|
|
7543
|
+
before: existsSync25(join28(w.root, path)) ? readFileSync22(join28(w.root, path), "utf8") : ""
|
|
7544
|
+
}));
|
|
7545
|
+
const result = parsePaste(payload.answer ?? "", targets);
|
|
7546
|
+
const before = new Map(targets.map((t) => [t.path, t.before]));
|
|
7547
|
+
json(res, {
|
|
7548
|
+
...result,
|
|
7549
|
+
files: result.files.map((f) => ({
|
|
7550
|
+
...f,
|
|
7551
|
+
before: before.get(f.path) ?? "",
|
|
7552
|
+
writable: isWritable(w, f.path, brainDirs())
|
|
7553
|
+
}))
|
|
7554
|
+
});
|
|
7555
|
+
}).catch((err) => {
|
|
7556
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
7557
|
+
res.end(JSON.stringify({ error: String(err?.message ?? err) }));
|
|
7558
|
+
});
|
|
7491
7559
|
return;
|
|
7492
7560
|
}
|
|
7493
7561
|
if (url.pathname === "/api/save") {
|