@nanocollective/roster 0.1.0-alpha.3 → 0.1.0-alpha.4
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 +848 -164
- package/docs/README.md +10 -5
- package/docs/agents.md +320 -9
- package/docs/commands.md +5 -6
- package/docs/concepts.md +27 -9
- package/docs/cost.md +3 -2
- package/docs/doctor-codes.md +13 -4
- package/docs/export.md +2 -1
- package/docs/extending.md +11 -2
- package/docs/getting-started.md +89 -84
- package/docs/images/brain.jpg +0 -0
- package/docs/images/org.jpg +0 -0
- package/docs/images/prompt.jpg +0 -0
- package/docs/images/setup-org.jpg +0 -0
- package/docs/images/setup-plan.jpg +0 -0
- package/docs/images/staff.jpg +0 -0
- package/docs/manual-steps.md +36 -13
- package/docs/memory.md +9 -6
- package/docs/org-yaml.md +37 -9
- package/docs/portal.md +197 -31
- package/docs/prompts.md +50 -11
- package/docs/security.md +19 -7
- package/docs/session-workflow.md +8 -10
- package/docs/staff-yaml.md +3 -5
- package/docs/troubleshooting.md +17 -14
- package/docs/writing-a-charter.md +18 -17
- package/package.json +1 -1
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +1 -0
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +10 -4
- package/templates/brain/staff.yaml +0 -1
- package/templates/ops/.github/workflows/session.yaml +9 -26
- package/templates/ops/agents.mjs +121 -6
- package/templates/ops/compose.mjs +61 -4
- package/templates/ops/org/operating.md +0 -6
- package/templates/ops/prompts/_identity.md +8 -1
- package/templates/ops/prompts/mention.md +16 -2
- package/templates/portal/css/base.css +116 -8
- package/templates/portal/css/brain.css +8 -1
- package/templates/portal/css/diff.css +6 -2
- package/templates/portal/css/health.css +21 -2
- package/templates/portal/css/inbox.css +93 -5
- package/templates/portal/css/layout.css +26 -4
- package/templates/portal/css/markdown.css +23 -3
- package/templates/portal/css/setup.css +11 -6
- package/templates/portal/index.html +7 -1
- package/templates/portal/js/api.js +33 -0
- package/templates/portal/js/app.js +28 -8
- package/templates/portal/js/dialog.js +47 -4
- package/templates/portal/js/dom.js +25 -0
- package/templates/portal/js/icons.js +8 -1
- package/templates/portal/js/lightbox.js +273 -0
- package/templates/portal/js/md.js +23 -6
- package/templates/portal/js/mention.js +264 -0
- package/templates/portal/js/refresh.js +136 -6
- package/templates/portal/js/state.js +47 -5
- package/templates/portal/js/views/checklist.js +20 -7
- package/templates/portal/js/views/docs.js +94 -4
- package/templates/portal/js/views/files.js +58 -14
- package/templates/portal/js/views/health.js +163 -35
- package/templates/portal/js/views/inbox.js +882 -96
- package/templates/portal/js/views/memory.js +16 -1
- package/templates/portal/js/views/org.js +142 -62
- package/templates/portal/js/views/prompt.js +50 -63
- package/templates/portal/js/views/staff.js +62 -2
- package/templates/portal/js/yaml.js +134 -0
- package/templates/brain/.github/workflows/%%STAFF%%-pr-mention.yaml +0 -50
- package/templates/ops/prompts/pr-mention.md +0 -57
package/dist/cli.js
CHANGED
|
@@ -244,6 +244,11 @@ function opsTemplateDir() {
|
|
|
244
244
|
// src/lib/render.ts
|
|
245
245
|
var NOTE = "%%TOKENS%%";
|
|
246
246
|
var TOKEN = /%%([A-Z_]+)%%/g;
|
|
247
|
+
function toolsOf(org) {
|
|
248
|
+
const list = org?.defaults?.allowed_tools;
|
|
249
|
+
const asked = Array.isArray(list) ? list.join(",") : typeof list === "string" ? list : "";
|
|
250
|
+
return asked.replace(/\s+/g, "") || "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch";
|
|
251
|
+
}
|
|
247
252
|
function orgTokens(org) {
|
|
248
253
|
return {
|
|
249
254
|
ORG: org.org,
|
|
@@ -251,7 +256,11 @@ function orgTokens(org) {
|
|
|
251
256
|
OPS_REPO: org.opsRepo,
|
|
252
257
|
OPS_REPO_DIR: org.opsDirName,
|
|
253
258
|
HUMAN: org.human,
|
|
254
|
-
HUMAN_MARKER: org.humanMarker
|
|
259
|
+
HUMAN_MARKER: org.humanMarker,
|
|
260
|
+
/* A JSON array, spelled into a GitHub expression as `fromJSON('%%HUMAN_LOGINS%%')`. One
|
|
261
|
+
login is still a one-element list, so the generated gate has one shape rather than two. */
|
|
262
|
+
HUMAN_LOGINS: JSON.stringify((org.humanLogins ?? [org.human]).filter(Boolean)),
|
|
263
|
+
ALLOWED_TOOLS: org.allowedTools
|
|
255
264
|
};
|
|
256
265
|
}
|
|
257
266
|
function tokensFor(org, s) {
|
|
@@ -269,7 +278,6 @@ function tokensFor(org, s) {
|
|
|
269
278
|
MODEL: s.model,
|
|
270
279
|
TIMEOUT: String(s.timeout),
|
|
271
280
|
MENTION_TIMEOUT: String(s.mentionTimeout),
|
|
272
|
-
PR_MENTION_TIMEOUT: String(s.prMentionTimeout),
|
|
273
281
|
SECRET_PREFIX: s.secretPrefix,
|
|
274
282
|
PUBLIC_SECRET_PREFIX: s.publicSecretPrefix,
|
|
275
283
|
APP: s.app,
|
|
@@ -354,11 +362,11 @@ function specFromManifest(m, dir) {
|
|
|
354
362
|
worksIn: (m.works_in ?? []).map((w) => String(w?.repo)).filter(Boolean),
|
|
355
363
|
schedule: String(m.schedule ?? ""),
|
|
356
364
|
model: String(m.model ?? ""),
|
|
357
|
-
timeout: Number(m.timeout_minutes ??
|
|
358
|
-
// Absent from the manifests written before these were separate fields.
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
365
|
+
timeout: Number(m.timeout_minutes ?? 90),
|
|
366
|
+
// Absent from the manifests written before these were separate fields. 90 either way: a
|
|
367
|
+
// mention that ends in a build needs a session's room, and the 30 these once fell back to
|
|
368
|
+
// killed four runs mid-gate at a ceiling nobody had chosen.
|
|
369
|
+
mentionTimeout: Number(m.mention_timeout_minutes ?? 90),
|
|
362
370
|
secretPrefix: String(priv.secret_prefix ?? String(m.handle ?? dir).toUpperCase()),
|
|
363
371
|
publicSecretPrefix: String(pub.secret_prefix ?? "BOT"),
|
|
364
372
|
app: String(priv.app ?? ""),
|
|
@@ -622,11 +630,37 @@ ${text.trimEnd()}
|
|
|
622
630
|
${bars}`;
|
|
623
631
|
}
|
|
624
632
|
|
|
633
|
+
// src/lib/humans.ts
|
|
634
|
+
function readHumans(org) {
|
|
635
|
+
const listed = Array.isArray(org?.humans) ? org.humans : org?.humans ? [org.humans] : [];
|
|
636
|
+
const raw = [...listed, org?.human].filter(Boolean);
|
|
637
|
+
const out = [];
|
|
638
|
+
for (const entry of raw) {
|
|
639
|
+
const github = String(entry.github ?? entry.login ?? "").trim();
|
|
640
|
+
const name = String(entry.name ?? github).trim();
|
|
641
|
+
if (!github && !name) continue;
|
|
642
|
+
if (github && out.some((h) => h.github.toLowerCase() === github.toLowerCase())) continue;
|
|
643
|
+
out.push({
|
|
644
|
+
github,
|
|
645
|
+
name: name || github,
|
|
646
|
+
marker: String(entry.marker ?? "").trim() || defaultMarker(name || github),
|
|
647
|
+
role: entry.role === void 0 || entry.role === null ? void 0 : String(entry.role)
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
function humanLogins(org) {
|
|
653
|
+
return readHumans(org).map((h) => h.github).filter(Boolean);
|
|
654
|
+
}
|
|
655
|
+
function defaultMarker(name) {
|
|
656
|
+
return String(name).trim().split(/[\s-]+/)[0].toLowerCase().replace(/[^a-z0-9]/g, "") || "human";
|
|
657
|
+
}
|
|
658
|
+
|
|
625
659
|
// src/lib/prompt.ts
|
|
626
660
|
import { execFileSync } from "child_process";
|
|
627
661
|
import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync2, writeFileSync } from "fs";
|
|
628
662
|
import { dirname as dirname3, join as join6, relative as relative2, resolve as resolve2 } from "path";
|
|
629
|
-
var KINDS = ["daily", "mention"
|
|
663
|
+
var KINDS = ["daily", "mention"];
|
|
630
664
|
function promptView(ws, compose, staffHandle, brainDir, kind) {
|
|
631
665
|
const had = process.env.ROSTER_CONTEXT;
|
|
632
666
|
if (kind !== "daily" && !had) {
|
|
@@ -774,12 +808,15 @@ function validateOrgYaml(text, parseYaml) {
|
|
|
774
808
|
return `org.yaml needs a "${key}", and every prompt is composed against it`;
|
|
775
809
|
}
|
|
776
810
|
}
|
|
777
|
-
for (const key of ["staff", "repos"]) {
|
|
811
|
+
for (const key of ["staff", "repos", "humans"]) {
|
|
778
812
|
if (doc[key] !== void 0 && !Array.isArray(doc[key])) return `"${key}" has to be a list`;
|
|
779
813
|
}
|
|
780
814
|
for (const s of doc.staff ?? []) {
|
|
781
815
|
if (!s?.handle) return "every staff entry needs a handle";
|
|
782
816
|
}
|
|
817
|
+
for (const h of doc.humans ?? []) {
|
|
818
|
+
if (!h?.github) return "every entry in humans needs a github login";
|
|
819
|
+
}
|
|
783
820
|
return null;
|
|
784
821
|
}
|
|
785
822
|
|
|
@@ -814,7 +851,7 @@ roster brief <kind> [handle]
|
|
|
814
851
|
these briefs by \`roster init\` and \`roster hire\`. There is nothing in them that is
|
|
815
852
|
specific to any agent.
|
|
816
853
|
|
|
817
|
-
--kind <k> for amend: daily | mention
|
|
854
|
+
--kind <k> for amend: daily | mention (default: daily)
|
|
818
855
|
--want <text> for amend: what you want changed
|
|
819
856
|
--ops <dir> ops repo directory (default: found by walking up)
|
|
820
857
|
`;
|
|
@@ -876,14 +913,19 @@ function staffTokens(ws, org, handle, parseYaml) {
|
|
|
876
913
|
return tokensFor(spec(org, ws), specFromManifest(manifest, dir));
|
|
877
914
|
}
|
|
878
915
|
function spec(org, ws) {
|
|
879
|
-
const
|
|
916
|
+
const humans = readHumans(org);
|
|
917
|
+
const first = humans[0];
|
|
880
918
|
return {
|
|
881
919
|
org: org.org,
|
|
882
920
|
name: org.name,
|
|
883
921
|
opsRepo: `${org.org}/${ws.opsName}`,
|
|
884
922
|
opsDirName: ws.opsName,
|
|
885
|
-
|
|
886
|
-
|
|
923
|
+
/* A brief is prose addressed to one person, so it names the first. The rest are in the
|
|
924
|
+
manifest and in the gate; a brief that said "Will and Sam" would read as a committee. */
|
|
925
|
+
human: first?.name ?? first?.github ?? "the human",
|
|
926
|
+
humanMarker: first?.marker ?? "human",
|
|
927
|
+
humanLogins: humans.map((h) => h.github).filter(Boolean),
|
|
928
|
+
allowedTools: toolsOf(org)
|
|
887
929
|
};
|
|
888
930
|
}
|
|
889
931
|
function renderBrief(text, tokens, where) {
|
|
@@ -921,7 +963,7 @@ function parseFlags2(argv) {
|
|
|
921
963
|
}
|
|
922
964
|
|
|
923
965
|
// src/commands/doctor.ts
|
|
924
|
-
import { existsSync as existsSync10, readdirSync as
|
|
966
|
+
import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync9 } from "fs";
|
|
925
967
|
import { join as join11 } from "path";
|
|
926
968
|
|
|
927
969
|
// src/lib/memory.ts
|
|
@@ -1124,7 +1166,15 @@ function looksUnwritten(body) {
|
|
|
1124
1166
|
|
|
1125
1167
|
// src/commands/upgrade.ts
|
|
1126
1168
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
1127
|
-
import {
|
|
1169
|
+
import {
|
|
1170
|
+
existsSync as existsSync9,
|
|
1171
|
+
mkdirSync,
|
|
1172
|
+
mkdtempSync as mkdtempSync2,
|
|
1173
|
+
readdirSync as readdirSync4,
|
|
1174
|
+
readFileSync as readFileSync8,
|
|
1175
|
+
rmSync as rmSync2,
|
|
1176
|
+
writeFileSync as writeFileSync3
|
|
1177
|
+
} from "fs";
|
|
1128
1178
|
import { tmpdir as tmpdir2 } from "os";
|
|
1129
1179
|
import { dirname as dirname4, join as join10 } from "path";
|
|
1130
1180
|
|
|
@@ -1454,8 +1504,10 @@ function planBrains(ws, parseYaml) {
|
|
|
1454
1504
|
name: org.name,
|
|
1455
1505
|
opsRepo: `${org.org}/${ws.opsName}`,
|
|
1456
1506
|
opsDirName: ws.opsName,
|
|
1457
|
-
human: org
|
|
1458
|
-
humanMarker: org
|
|
1507
|
+
human: readHumans(org)[0]?.github ?? "",
|
|
1508
|
+
humanMarker: readHumans(org)[0]?.marker ?? "human",
|
|
1509
|
+
humanLogins: humanLogins(org),
|
|
1510
|
+
allowedTools: toolsOf(org)
|
|
1459
1511
|
};
|
|
1460
1512
|
const out = [];
|
|
1461
1513
|
for (const entry of org.staff ?? []) {
|
|
@@ -1494,6 +1546,12 @@ function planBrains(ws, parseYaml) {
|
|
|
1494
1546
|
have === text ? { rel, kind, verdict: "same" } : { rel, kind, verdict: "regenerate", next: text, diff: unified(have, text) }
|
|
1495
1547
|
);
|
|
1496
1548
|
}
|
|
1549
|
+
for (const rel of callerFiles(root)) {
|
|
1550
|
+
if (want.has(rel)) continue;
|
|
1551
|
+
const text = readFileSync8(join10(root, rel), "utf8");
|
|
1552
|
+
if (!text.includes(`${orgSpec2.opsRepo}/.github/workflows/session.yaml`)) continue;
|
|
1553
|
+
base2.files.push({ rel, kind: "generated", verdict: "obsolete" });
|
|
1554
|
+
}
|
|
1497
1555
|
out.push(base2);
|
|
1498
1556
|
}
|
|
1499
1557
|
return out;
|
|
@@ -1518,6 +1576,11 @@ function unified(before, after) {
|
|
|
1518
1576
|
rmSync2(dir, { recursive: true, force: true });
|
|
1519
1577
|
}
|
|
1520
1578
|
}
|
|
1579
|
+
function callerFiles(root) {
|
|
1580
|
+
const dir = join10(root, ".github", "workflows");
|
|
1581
|
+
if (!existsSync9(dir)) return [];
|
|
1582
|
+
return readdirSync4(dir).filter((n) => n.endsWith(".yaml") || n.endsWith(".yml")).map((n) => `.github/workflows/${n}`);
|
|
1583
|
+
}
|
|
1521
1584
|
function reportBrains(brains, opts) {
|
|
1522
1585
|
for (const b of brains) {
|
|
1523
1586
|
const interesting = b.files.filter((f) => f.verdict !== "same");
|
|
@@ -1541,6 +1604,13 @@ function reportBrains(brains, opts) {
|
|
|
1541
1604
|
`);
|
|
1542
1605
|
continue;
|
|
1543
1606
|
}
|
|
1607
|
+
if (f.verdict === "obsolete") {
|
|
1608
|
+
process.stdout.write(
|
|
1609
|
+
` - ${f.rel} the framework no longer generates this; will be removed
|
|
1610
|
+
`
|
|
1611
|
+
);
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1544
1614
|
process.stdout.write(` \u2191 ${f.rel} differs from the template
|
|
1545
1615
|
`);
|
|
1546
1616
|
if (f.diff) {
|
|
@@ -1555,6 +1625,11 @@ function applyBrains(brains) {
|
|
|
1555
1625
|
let wrote = 0;
|
|
1556
1626
|
for (const b of brains) {
|
|
1557
1627
|
for (const f of b.files) {
|
|
1628
|
+
if (f.verdict === "obsolete") {
|
|
1629
|
+
rmSync2(join10(b.root, f.rel), { force: true });
|
|
1630
|
+
wrote++;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1558
1633
|
if (f.next === void 0) continue;
|
|
1559
1634
|
const dest = join10(b.root, f.rel);
|
|
1560
1635
|
mkdirSync(dirname4(dest), { recursive: true });
|
|
@@ -1602,11 +1677,12 @@ async function collect(opts) {
|
|
|
1602
1677
|
const composer = await loadComposer(ws.opsDir);
|
|
1603
1678
|
const org = readOrg(ws.opsDir, composer.parseYaml);
|
|
1604
1679
|
const staff = (org.staff ?? []).filter((s) => !opts.only || s.handle === opts.only);
|
|
1605
|
-
if (!staff.length) return null;
|
|
1680
|
+
if (opts.only && !staff.length) return null;
|
|
1606
1681
|
const findings = [];
|
|
1607
1682
|
const online = !opts.offline && await gate(findings);
|
|
1608
1683
|
findings.push(...checkWorkspace(ws, org));
|
|
1609
1684
|
findings.push(...checkBusiness(ws));
|
|
1685
|
+
findings.push(...await checkAgent(ws, org));
|
|
1610
1686
|
if (online) findings.push(...await checkOrgOnline(ws, org));
|
|
1611
1687
|
const perStaff = await Promise.all(staff.map((s) => checkStaff(ws, org, s, composer, online)));
|
|
1612
1688
|
for (const f of perStaff) findings.push(...f);
|
|
@@ -1659,13 +1735,22 @@ function checkWorkspace(ws, org) {
|
|
|
1659
1735
|
id: "org.yaml",
|
|
1660
1736
|
title: `org.yaml names ${org.repos?.length ?? 0} repos and ${org.staff?.length ?? 0} staff`
|
|
1661
1737
|
});
|
|
1662
|
-
|
|
1738
|
+
const humans = readHumans(org);
|
|
1739
|
+
if (!humans.some((h) => h.github)) {
|
|
1663
1740
|
out.push({
|
|
1664
1741
|
scope,
|
|
1665
1742
|
level: "fail",
|
|
1666
1743
|
id: "human",
|
|
1667
|
-
title: "org.yaml
|
|
1668
|
-
fix: "The mention callers gate on
|
|
1744
|
+
title: "org.yaml names no human with a github login",
|
|
1745
|
+
fix: "The mention callers gate on these logins. Without one nothing can wake an agent. Write `human: { github: you }`, or a `humans:` list for more than one."
|
|
1746
|
+
});
|
|
1747
|
+
} else if (humans.some((h) => !h.github)) {
|
|
1748
|
+
out.push({
|
|
1749
|
+
scope,
|
|
1750
|
+
level: "warn",
|
|
1751
|
+
id: "human.login",
|
|
1752
|
+
title: `${humans.filter((h) => !h.github).map((h) => h.name).join(", ")} has no github login`,
|
|
1753
|
+
fix: "Give them one, or the mention gate silently ignores everything they write."
|
|
1669
1754
|
});
|
|
1670
1755
|
}
|
|
1671
1756
|
try {
|
|
@@ -1714,6 +1799,57 @@ function checkWorkspace(ws, org) {
|
|
|
1714
1799
|
}
|
|
1715
1800
|
return out;
|
|
1716
1801
|
}
|
|
1802
|
+
async function checkAgent(ws, org) {
|
|
1803
|
+
const scope = "workspace";
|
|
1804
|
+
let agent;
|
|
1805
|
+
try {
|
|
1806
|
+
const { resolveAgent } = await import(`file://${join11(ws.opsDir, "agents.mjs")}`);
|
|
1807
|
+
agent = resolveAgent(org, {});
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
return [
|
|
1810
|
+
{
|
|
1811
|
+
scope,
|
|
1812
|
+
level: "fail",
|
|
1813
|
+
id: "agent",
|
|
1814
|
+
title: `no runner resolves: ${firstLine(err)}`,
|
|
1815
|
+
fix: "Nothing can run until org.yaml names an agent this tenant's agents.mjs knows."
|
|
1816
|
+
}
|
|
1817
|
+
];
|
|
1818
|
+
}
|
|
1819
|
+
const out = [{ scope, level: "ok", id: "agent", title: `runs on ${agent.id}` }];
|
|
1820
|
+
const orgFile = join11(ws.opsDir, "org.yaml");
|
|
1821
|
+
if (existsSync10(orgFile) && /FILL IN/.test(readFileSync9(orgFile, "utf8"))) {
|
|
1822
|
+
out.push({
|
|
1823
|
+
scope,
|
|
1824
|
+
level: "fail",
|
|
1825
|
+
id: "agent.config",
|
|
1826
|
+
title: "org.yaml still has a blank in it",
|
|
1827
|
+
fix: "Fill in the FILL IN. A run cannot pick a model from a placeholder."
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
if (!agent.config) return out;
|
|
1831
|
+
const path = join11(ws.opsDir, agent.config.path);
|
|
1832
|
+
if (!existsSync10(path)) {
|
|
1833
|
+
out.push({
|
|
1834
|
+
scope,
|
|
1835
|
+
level: "fail",
|
|
1836
|
+
id: "agent.config",
|
|
1837
|
+
title: `${agent.id} needs ${agent.config.path} and there is none`,
|
|
1838
|
+
fix: "roster init writes it for a new tenant. See docs/agents.md for what goes in it."
|
|
1839
|
+
});
|
|
1840
|
+
return out;
|
|
1841
|
+
}
|
|
1842
|
+
if (/FILL IN/.test(readFileSync9(path, "utf8"))) {
|
|
1843
|
+
out.push({
|
|
1844
|
+
scope,
|
|
1845
|
+
level: "fail",
|
|
1846
|
+
id: "agent.config",
|
|
1847
|
+
title: `${agent.config.path} still has a blank in it`,
|
|
1848
|
+
fix: `Fill in the FILL IN. ${agent.id} cannot pick a model without it.`
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
return out;
|
|
1852
|
+
}
|
|
1717
1853
|
function checkBusiness(ws) {
|
|
1718
1854
|
const path = join11(ws.opsDir, "org", "business.md");
|
|
1719
1855
|
if (!existsSync10(path)) {
|
|
@@ -1847,11 +1983,10 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
1847
1983
|
const CONTEXT = JSON.stringify({
|
|
1848
1984
|
issue_number: "1",
|
|
1849
1985
|
comment_id: "1",
|
|
1850
|
-
pr_number: "1",
|
|
1851
1986
|
repo: `${org.org}/example`
|
|
1852
1987
|
});
|
|
1853
1988
|
const before = process.env.ROSTER_CONTEXT;
|
|
1854
|
-
for (const kind of ["daily", "mention"
|
|
1989
|
+
for (const kind of ["daily", "mention"]) {
|
|
1855
1990
|
try {
|
|
1856
1991
|
if (kind === "daily") delete process.env.ROSTER_CONTEXT;
|
|
1857
1992
|
else process.env.ROSTER_CONTEXT = CONTEXT;
|
|
@@ -1879,16 +2014,16 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
1879
2014
|
scope,
|
|
1880
2015
|
level: "ok",
|
|
1881
2016
|
id: "compose",
|
|
1882
|
-
title: "prompts compose for daily
|
|
2017
|
+
title: "prompts compose for daily and mention"
|
|
1883
2018
|
});
|
|
1884
2019
|
}
|
|
1885
|
-
const callers = readCallers(root);
|
|
1886
|
-
if (callers.length !==
|
|
2020
|
+
const callers = readCallers(root, entry.handle);
|
|
2021
|
+
if (callers.length !== 2) {
|
|
1887
2022
|
out.push({
|
|
1888
2023
|
scope,
|
|
1889
2024
|
level: callers.length ? "warn" : "fail",
|
|
1890
2025
|
id: "callers",
|
|
1891
|
-
title: `${callers.length} caller workflow${callers.length === 1 ? "" : "s"}, expected
|
|
2026
|
+
title: `${callers.length} caller workflow${callers.length === 1 ? "" : "s"}, expected 2 (daily, mention)`,
|
|
1892
2027
|
fix: "A missing caller is a route that silently never fires. Compare against roster's templates/brain."
|
|
1893
2028
|
});
|
|
1894
2029
|
}
|
|
@@ -1926,8 +2061,13 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
1926
2061
|
});
|
|
1927
2062
|
}
|
|
1928
2063
|
}
|
|
1929
|
-
if (callers.length ===
|
|
1930
|
-
out.push({
|
|
2064
|
+
if (callers.length === 2 && !out.some((f) => f.id.startsWith("callers"))) {
|
|
2065
|
+
out.push({
|
|
2066
|
+
scope,
|
|
2067
|
+
level: "ok",
|
|
2068
|
+
id: "callers",
|
|
2069
|
+
title: `2 callers, both pointing at ${opsRepo}`
|
|
2070
|
+
});
|
|
1931
2071
|
}
|
|
1932
2072
|
const missing = (manifest.surfaces ?? []).map((s) => s.path).filter((p) => p && !existsSync10(join11(root, p)));
|
|
1933
2073
|
if (missing.length) {
|
|
@@ -1946,7 +2086,7 @@ async function checkStaff(ws, org, entry, composer, online) {
|
|
|
1946
2086
|
}
|
|
1947
2087
|
function timeoutOf(callerText) {
|
|
1948
2088
|
const m = /timeout_minutes:\s*(\d+)/.exec(callerText);
|
|
1949
|
-
return m ? Number(m[1]) :
|
|
2089
|
+
return m ? Number(m[1]) : 90;
|
|
1950
2090
|
}
|
|
1951
2091
|
function runMinutes(run6) {
|
|
1952
2092
|
return (new Date(run6.updatedAt).getTime() - new Date(run6.createdAt).getTime()) / 6e4;
|
|
@@ -1971,10 +2111,11 @@ function inferredCeiling(cancelled) {
|
|
|
1971
2111
|
}
|
|
1972
2112
|
return most >= 2 ? best : null;
|
|
1973
2113
|
}
|
|
1974
|
-
function readCallers(root) {
|
|
2114
|
+
function readCallers(root, handle) {
|
|
1975
2115
|
const dir = join11(root, ".github", "workflows");
|
|
1976
2116
|
if (!existsSync10(dir)) return [];
|
|
1977
|
-
|
|
2117
|
+
const mine = new RegExp(`^${handle}-(daily|mention)\\.ya?ml$`);
|
|
2118
|
+
return readdirSync5(dir).filter((f) => mine.test(f)).sort().map((name) => ({ name, text: readFileSync9(join11(dir, name), "utf8") }));
|
|
1978
2119
|
}
|
|
1979
2120
|
async function checkOrgOnline(ws, org) {
|
|
1980
2121
|
const out = [];
|
|
@@ -2165,6 +2306,9 @@ async function checkStaffOnline(scope, manifest, callers) {
|
|
|
2165
2306
|
}
|
|
2166
2307
|
);
|
|
2167
2308
|
}
|
|
2309
|
+
const proven = runs.some(
|
|
2310
|
+
({ res }) => (res.data ?? []).some((r) => r.status === "completed" && r.conclusion === "success")
|
|
2311
|
+
);
|
|
2168
2312
|
for (const { name, res } of runs) {
|
|
2169
2313
|
if (!res.ok) {
|
|
2170
2314
|
out.push({
|
|
@@ -2189,13 +2333,20 @@ async function checkStaffOnline(scope, manifest, callers) {
|
|
|
2189
2333
|
continue;
|
|
2190
2334
|
}
|
|
2191
2335
|
if (!real.length) {
|
|
2192
|
-
out.push(
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2336
|
+
out.push(
|
|
2337
|
+
proven ? {
|
|
2338
|
+
scope,
|
|
2339
|
+
level: "ok",
|
|
2340
|
+
id: "runs",
|
|
2341
|
+
title: `${name}: ${all.length} recent triggers, all gated out, which is its normal state`
|
|
2342
|
+
} : {
|
|
2343
|
+
scope,
|
|
2344
|
+
level: "warn",
|
|
2345
|
+
id: "runs",
|
|
2346
|
+
title: `${name}: ${all.length} recent triggers, all gated out before doing anything`,
|
|
2347
|
+
fix: "Nothing in this repo has exercised the app grant. A skipped run proves only the trigger, so trigger one workflow here by hand before trusting any of them."
|
|
2348
|
+
}
|
|
2349
|
+
);
|
|
2199
2350
|
continue;
|
|
2200
2351
|
}
|
|
2201
2352
|
const timeout = timeoutOf(callers.find((c) => c.name === name)?.text ?? "");
|
|
@@ -2208,7 +2359,15 @@ async function checkStaffOnline(scope, manifest, callers) {
|
|
|
2208
2359
|
const cancelled = real.filter((r) => !killed(r) && r.conclusion === "cancelled");
|
|
2209
2360
|
const ok = real.filter((r) => r.conclusion === "success");
|
|
2210
2361
|
const ceiling = timedOut.length ? Math.round(runMinutes(timedOut[0])) : timeout;
|
|
2211
|
-
|
|
2362
|
+
const provedSince = ceiling !== timeout && ok.some((r) => r.createdAt > timedOut[0].createdAt);
|
|
2363
|
+
if (timedOut.length && provedSince) {
|
|
2364
|
+
out.push({
|
|
2365
|
+
scope,
|
|
2366
|
+
level: "ok",
|
|
2367
|
+
id: "runs.timeout",
|
|
2368
|
+
title: `${name}: the ${ceiling}m ceiling that killed ${timedOut.length} of the last ${real.length} was raised to ${timeout}m, and a run has finished since`
|
|
2369
|
+
});
|
|
2370
|
+
} else if (timedOut.length) {
|
|
2212
2371
|
out.push({
|
|
2213
2372
|
scope,
|
|
2214
2373
|
level: "fail",
|
|
@@ -2301,7 +2460,7 @@ import { writeFileSync as writeFileSync4 } from "fs";
|
|
|
2301
2460
|
|
|
2302
2461
|
// src/lib/export.ts
|
|
2303
2462
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
2304
|
-
import { existsSync as existsSync11, readdirSync as
|
|
2463
|
+
import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
|
|
2305
2464
|
import { join as join12, relative as relative3 } from "path";
|
|
2306
2465
|
var SKIP = /* @__PURE__ */ new Set([".git", "node_modules", ".next", "dist", "out", ".DS_Store"]);
|
|
2307
2466
|
function buildExport(ws, org, parseYaml, opts = {}) {
|
|
@@ -2344,11 +2503,13 @@ function buildExport(ws, org, parseYaml, opts = {}) {
|
|
|
2344
2503
|
s.soloBots = s.bots.filter((b) => times.get(b) === 1);
|
|
2345
2504
|
s.sharedBots = s.bots.filter((b) => (times.get(b) ?? 0) > 1);
|
|
2346
2505
|
}
|
|
2506
|
+
const humans = readHumans(org);
|
|
2347
2507
|
return {
|
|
2348
2508
|
org: org.org,
|
|
2349
2509
|
name: org.name,
|
|
2350
2510
|
opsName: ws.opsName,
|
|
2351
|
-
human: org.human ?? {},
|
|
2511
|
+
human: humans[0] ?? org.human ?? {},
|
|
2512
|
+
humans,
|
|
2352
2513
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2353
2514
|
staff
|
|
2354
2515
|
};
|
|
@@ -2369,7 +2530,7 @@ function readRig(root, manifest, commits) {
|
|
|
2369
2530
|
const wfDir = join12(root, ".github", "workflows");
|
|
2370
2531
|
const declared = manifest.surfaces ?? [];
|
|
2371
2532
|
return {
|
|
2372
|
-
workflows: existsSync11(wfDir) ?
|
|
2533
|
+
workflows: existsSync11(wfDir) ? readdirSync6(wfDir).filter((f) => /\.ya?ml$/.test(f)).sort() : [],
|
|
2373
2534
|
hasCharter: existsSync11(join12(root, "CHARTER.md")),
|
|
2374
2535
|
hasManifest: existsSync11(join12(root, "staff.yaml")),
|
|
2375
2536
|
missingSurfaces: declared.filter((s) => s?.path && !existsSync11(join12(root, s.path))).map((s) => s.path),
|
|
@@ -2398,7 +2559,7 @@ function readSurfaces(root, declared) {
|
|
|
2398
2559
|
}));
|
|
2399
2560
|
}
|
|
2400
2561
|
function walk2(dir, root, out = []) {
|
|
2401
|
-
for (const name of
|
|
2562
|
+
for (const name of readdirSync6(dir)) {
|
|
2402
2563
|
if (SKIP.has(name)) continue;
|
|
2403
2564
|
const full = join12(dir, name);
|
|
2404
2565
|
const st = statSync3(full);
|
|
@@ -2839,7 +3000,7 @@ import { dirname as dirname5, join as join14 } from "path";
|
|
|
2839
3000
|
var hireHelp = `
|
|
2840
3001
|
roster hire <handle> [--name "Chief Financial Officer"] [--apply]
|
|
2841
3002
|
|
|
2842
|
-
Scaffold a new staff member: their brain repo, the
|
|
3003
|
+
Scaffold a new staff member: their brain repo, the two caller workflows, a manifest, a
|
|
2843
3004
|
memory index, a charter stub, labels, a pinned status issue, and the peer wiring in both
|
|
2844
3005
|
directions.
|
|
2845
3006
|
|
|
@@ -2856,7 +3017,6 @@ roster hire <handle> [--name "Chief Financial Officer"] [--apply]
|
|
|
2856
3017
|
--model <id> defaults to org.yaml's
|
|
2857
3018
|
--timeout <n> daily run ceiling, minutes
|
|
2858
3019
|
--mention-timeout <n> mention run ceiling, minutes
|
|
2859
|
-
--pr-timeout <n> PR-amendment run ceiling, minutes
|
|
2860
3020
|
--secret-prefix <X> secrets are <X>_APP_ID and <X>_APP_PRIVATE_KEY. Defaults to HANDLE.
|
|
2861
3021
|
--app <slug> this staff member's GitHub App. Defaults to the pattern its peers use.
|
|
2862
3022
|
--public-app <slug> the shared public identity. Defaults to whatever the peers use.
|
|
@@ -2937,9 +3097,11 @@ function buildPlan(ws, org, handle, opts, parseYaml) {
|
|
|
2937
3097
|
worksIn: (org.repos ?? []).filter((r) => r.role === "product").map((r) => `${org.org}/${r.name}`),
|
|
2938
3098
|
schedule,
|
|
2939
3099
|
model,
|
|
2940
|
-
timeout: opts.timeout ?? org.defaults?.timeout_minutes ??
|
|
2941
|
-
|
|
2942
|
-
|
|
3100
|
+
timeout: opts.timeout ?? org.defaults?.timeout_minutes ?? 90,
|
|
3101
|
+
// A mention that ends in a build needs a session's room: the product repo's gate alone
|
|
3102
|
+
// can outlast a short ceiling, and a hire that has to discover that costs a run. An org
|
|
3103
|
+
// that wants them split says so; absent that, one number governs both.
|
|
3104
|
+
mentionTimeout: opts.mentionTimeout ?? org.defaults?.mention_timeout_minutes ?? org.defaults?.timeout_minutes ?? 90,
|
|
2943
3105
|
secretPrefix: opts.secretPrefix ?? handle.toUpperCase(),
|
|
2944
3106
|
publicSecretPrefix: publicIdentity?.secret_prefix ?? "BOT",
|
|
2945
3107
|
app: app ?? `${handle}`,
|
|
@@ -2956,16 +3118,24 @@ function buildPlan(ws, org, handle, opts, parseYaml) {
|
|
|
2956
3118
|
"status_issue is 0 until --apply opens the pinned issue; prompts will not compose before then"
|
|
2957
3119
|
);
|
|
2958
3120
|
}
|
|
3121
|
+
const humans = readHumans(org);
|
|
2959
3122
|
const orgSpec2 = {
|
|
2960
3123
|
org: org.org,
|
|
2961
3124
|
name: org.name,
|
|
2962
3125
|
opsRepo: `${org.org}/${ws.opsName}`,
|
|
2963
3126
|
opsDirName: ws.opsName,
|
|
2964
|
-
human:
|
|
2965
|
-
humanMarker:
|
|
3127
|
+
human: humans[0]?.github ?? "",
|
|
3128
|
+
humanMarker: humans[0]?.marker ?? "human",
|
|
3129
|
+
humanLogins: humans.map((h) => h.github).filter(Boolean),
|
|
3130
|
+
allowedTools: toolsOf(org)
|
|
2966
3131
|
};
|
|
2967
3132
|
if (!orgSpec2.human)
|
|
2968
3133
|
warnings.push("org.yaml has no human.github, so the mention gate will never match");
|
|
3134
|
+
if (humans.length > 1) {
|
|
3135
|
+
warnings.push(
|
|
3136
|
+
`the mention gate accepts ${humans.map((h) => h.github).join(", ")} \u2014 everyone in org.yaml`
|
|
3137
|
+
);
|
|
3138
|
+
}
|
|
2969
3139
|
const tokens = tokensFor(orgSpec2, staff);
|
|
2970
3140
|
const files = renderTree(brainTemplateDir(), tokens);
|
|
2971
3141
|
for (const [rel, text] of briefCommands(["charter"], tokens)) files.set(rel, text);
|
|
@@ -3301,7 +3471,6 @@ function parseFlags6(argv) {
|
|
|
3301
3471
|
else if (flag === "--model") out.model = value;
|
|
3302
3472
|
else if (flag === "--timeout") out.timeout = Number(value);
|
|
3303
3473
|
else if (flag === "--mention-timeout") out.mentionTimeout = Number(value);
|
|
3304
|
-
else if (flag === "--pr-timeout") out.prMentionTimeout = Number(value);
|
|
3305
3474
|
else if (flag === "--secret-prefix") out.secretPrefix = value;
|
|
3306
3475
|
else if (flag === "--app") out.app = value;
|
|
3307
3476
|
else if (flag === "--public-app") out.publicApp = value;
|
|
@@ -3362,7 +3531,7 @@ async function initCommand(argv) {
|
|
|
3362
3531
|
`);
|
|
3363
3532
|
return 2;
|
|
3364
3533
|
}
|
|
3365
|
-
const files = initFiles({ org: opts.org, name, human, marker, opsName, agent: opts.agent });
|
|
3534
|
+
const files = await initFiles({ org: opts.org, name, human, marker, opsName, agent: opts.agent });
|
|
3366
3535
|
process.stdout.write(`
|
|
3367
3536
|
roster init \u2014 ${name} (${opts.org})
|
|
3368
3537
|
|
|
@@ -3438,12 +3607,18 @@ async function initCommand(argv) {
|
|
|
3438
3607
|
`);
|
|
3439
3608
|
return 0;
|
|
3440
3609
|
}
|
|
3441
|
-
function initFiles(o) {
|
|
3610
|
+
async function initFiles(o) {
|
|
3442
3611
|
const files = /* @__PURE__ */ new Map();
|
|
3443
3612
|
for (const rel of templateFiles(opsTemplateDir())) {
|
|
3444
3613
|
files.set(rel, readFileSync13(join15(opsTemplateDir(), rel), "utf8"));
|
|
3445
3614
|
}
|
|
3446
|
-
|
|
3615
|
+
const agent = o.agent ?? "claude-code-action";
|
|
3616
|
+
const preset = await agentPreset(agent);
|
|
3617
|
+
const model = preset.model || '"FILL IN: a model the provider below serves"';
|
|
3618
|
+
files.set("org.yaml", orgYaml({ ...o, agent, model }));
|
|
3619
|
+
if (preset.config) {
|
|
3620
|
+
files.set(preset.config.path, JSON.stringify(preset.config.contents, null, 2) + "\n");
|
|
3621
|
+
}
|
|
3447
3622
|
files.set("org/business.md", businessStub(o.name, o.org));
|
|
3448
3623
|
for (const [rel, text] of briefCommands(["discover", "voice"], {
|
|
3449
3624
|
ORG: o.org,
|
|
@@ -3458,6 +3633,18 @@ function initFiles(o) {
|
|
|
3458
3633
|
files.set(".roster-version", stamp2() + "\n");
|
|
3459
3634
|
return files;
|
|
3460
3635
|
}
|
|
3636
|
+
async function agentPreset(id) {
|
|
3637
|
+
const path = join15(opsTemplateDir(), "agents.mjs");
|
|
3638
|
+
const { PRESETS } = await import(`file://${path}`);
|
|
3639
|
+
const preset = PRESETS[id];
|
|
3640
|
+
if (!preset) {
|
|
3641
|
+
throw new Error(
|
|
3642
|
+
`unknown agent "${id}". Known: ${Object.keys(PRESETS).join(", ")}.
|
|
3643
|
+
Anything else works by writing install, run and token_env into org.yaml by hand.`
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
return preset;
|
|
3647
|
+
}
|
|
3461
3648
|
function orgYaml(o) {
|
|
3462
3649
|
return `# The org manifest. Read at the top of every composed prompt.
|
|
3463
3650
|
# Kept deliberately simple: compose.mjs parses a small, strict YAML subset, and a manifest
|
|
@@ -3467,6 +3654,11 @@ org: ${o.org}
|
|
|
3467
3654
|
name: ${o.name}
|
|
3468
3655
|
ops_dir: ${o.opsName}
|
|
3469
3656
|
|
|
3657
|
+
# Who the staff answer to. More than one person? Use a "humans:" list instead \u2014 the mention
|
|
3658
|
+
# gate accepts every login in it, and the first is the one the prompts address:
|
|
3659
|
+
# humans:
|
|
3660
|
+
# - { name: ${o.human}, github: ${o.human}, marker: ${o.marker}, role: founder }
|
|
3661
|
+
# - { name: Sam, github: sam, marker: sam, role: operations }
|
|
3470
3662
|
human:
|
|
3471
3663
|
name: ${o.human}
|
|
3472
3664
|
github: ${o.human}
|
|
@@ -3481,11 +3673,18 @@ experiment_private: true
|
|
|
3481
3673
|
# works by writing the three fields out longhand. See agents.mjs.
|
|
3482
3674
|
agent:
|
|
3483
3675
|
id: ${o.agent}
|
|
3676
|
+
# How much it may do without stopping to ask: full, workspace, or read-only. One word here,
|
|
3677
|
+
# translated into each agent's own vocabulary by agents.mjs \u2014 a tool list for Claude, a
|
|
3678
|
+
# sandbox and an approval policy for Codex, a development mode for nanocoder.
|
|
3679
|
+
permissions: full
|
|
3680
|
+
# Anything else, in that agent's own words, passed through untranslated:
|
|
3681
|
+
# options:
|
|
3682
|
+
# provider: openrouter
|
|
3484
3683
|
|
|
3485
3684
|
defaults:
|
|
3486
|
-
model:
|
|
3487
|
-
timeout_minutes:
|
|
3488
|
-
|
|
3685
|
+
model: ${o.model}
|
|
3686
|
+
timeout_minutes: 90
|
|
3687
|
+
mention_timeout_minutes: 90
|
|
3489
3688
|
|
|
3490
3689
|
# Every staff member, and where their brain lands in the runner checkout.
|
|
3491
3690
|
# Written by \`roster hire\`.
|
|
@@ -3659,16 +3858,81 @@ function parseFlags8(argv) {
|
|
|
3659
3858
|
}
|
|
3660
3859
|
|
|
3661
3860
|
// src/commands/portal.ts
|
|
3662
|
-
import { execFileSync as
|
|
3861
|
+
import { execFileSync as execFileSync8 } from "child_process";
|
|
3663
3862
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
3664
|
-
import { existsSync as
|
|
3863
|
+
import { createReadStream, existsSync as existsSync22, readdirSync as readdirSync9, readFileSync as readFileSync19, statSync as statSync5 } from "fs";
|
|
3665
3864
|
import { createServer as createServer2 } from "http";
|
|
3666
|
-
import { extname as
|
|
3865
|
+
import { extname as extname3, join as join24, resolve as resolve5 } from "path";
|
|
3667
3866
|
|
|
3668
3867
|
// src/lib/act.ts
|
|
3669
3868
|
import { execFile as execFile3 } from "child_process";
|
|
3670
3869
|
import { promisify as promisify3 } from "util";
|
|
3870
|
+
|
|
3871
|
+
// src/lib/ask.ts
|
|
3872
|
+
var MAX_PATCH_LINES = 40;
|
|
3873
|
+
function askTitle(req) {
|
|
3874
|
+
const name = req.pr.repo.split("/")[1] ?? req.pr.repo;
|
|
3875
|
+
const head2 = `${name}#${req.pr.number} \u2014 `;
|
|
3876
|
+
const room = 120 - head2.length;
|
|
3877
|
+
const title = req.pr.title.trim() || "a pull request";
|
|
3878
|
+
return head2 + (title.length > room ? `${title.slice(0, room - 1).trimEnd()}\u2026` : title);
|
|
3879
|
+
}
|
|
3880
|
+
function askBody(req) {
|
|
3881
|
+
const { staff, pr, anchor } = req;
|
|
3882
|
+
const said = req.body.trim();
|
|
3883
|
+
const isPr = pr.kind !== "issue";
|
|
3884
|
+
const it = isPr ? "pull request" : "issue";
|
|
3885
|
+
const out = [];
|
|
3886
|
+
out.push(stripLeadingMention(said, staff.mention) ? `${staff.mention} ${said}` : said);
|
|
3887
|
+
out.push("", "---", "");
|
|
3888
|
+
out.push(`**This is about ${pr.repo}#${pr.number} \u2014 "${pr.title.trim()}".**`);
|
|
3889
|
+
out.push("", pr.url);
|
|
3890
|
+
if (pr.head && pr.base) out.push("", `Branch \`${pr.head}\` \u2192 \`${pr.base}\`.`);
|
|
3891
|
+
if (anchor?.path) {
|
|
3892
|
+
out.push("", `They were looking at \`${anchor.path}\`.`);
|
|
3893
|
+
const hunk = clip(anchor.patch ?? "");
|
|
3894
|
+
if (hunk) out.push("", fence2(hunk, "diff"));
|
|
3895
|
+
else if (anchor.patch) out.push("", "The diff for it is on the pull request.");
|
|
3896
|
+
}
|
|
3897
|
+
out.push(
|
|
3898
|
+
"",
|
|
3899
|
+
`**Answer on the ${it}, not here.** That is where it will be read${isPr ? ", and it is where the diff is" : ""}:`,
|
|
3900
|
+
"",
|
|
3901
|
+
fence2(`gh issue comment ${pr.number} --repo ${pr.repo} --body "\u2026"`, ""),
|
|
3902
|
+
"",
|
|
3903
|
+
...isPr ? [
|
|
3904
|
+
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."
|
|
3905
|
+
] : ["Close this issue once the reply is up."]
|
|
3906
|
+
);
|
|
3907
|
+
return `${out.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}
|
|
3908
|
+
`;
|
|
3909
|
+
}
|
|
3910
|
+
function stripLeadingMention(said, mention) {
|
|
3911
|
+
return !new RegExp(`(^|\\s)${escapeRe(mention)}(\\s|$)`).test(said);
|
|
3912
|
+
}
|
|
3913
|
+
function escapeRe(s) {
|
|
3914
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3915
|
+
}
|
|
3916
|
+
function clip(patch) {
|
|
3917
|
+
const lines = patch.replace(/\s+$/, "").split("\n");
|
|
3918
|
+
if (!lines[0]) return "";
|
|
3919
|
+
if (lines.length <= MAX_PATCH_LINES) return lines.join("\n");
|
|
3920
|
+
return [
|
|
3921
|
+
...lines.slice(0, MAX_PATCH_LINES),
|
|
3922
|
+
`\u2026 ${lines.length - MAX_PATCH_LINES} more lines, on the pull request`
|
|
3923
|
+
].join("\n");
|
|
3924
|
+
}
|
|
3925
|
+
function fence2(text, lang) {
|
|
3926
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
|
|
3927
|
+
const ticks = "`".repeat(Math.max(3, longest + 1));
|
|
3928
|
+
return `${ticks}${lang}
|
|
3929
|
+
${text}
|
|
3930
|
+
${ticks}`;
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
// src/lib/act.ts
|
|
3671
3934
|
var run3 = promisify3(execFile3);
|
|
3935
|
+
var MERGE_FLAG = { squash: "--squash", merge: "--merge", rebase: "--rebase" };
|
|
3672
3936
|
async function act(req) {
|
|
3673
3937
|
const { action, repo } = req;
|
|
3674
3938
|
if (action === "create") {
|
|
@@ -3679,6 +3943,37 @@ async function act(req) {
|
|
|
3679
3943
|
const { stdout } = await run3("gh", args, { encoding: "utf8" });
|
|
3680
3944
|
return { ok: true, action, url: stdout.trim().split("\n").pop() };
|
|
3681
3945
|
}
|
|
3946
|
+
if (action === "ask") {
|
|
3947
|
+
const ask = req.ask;
|
|
3948
|
+
if (!ask) throw new Error("an ask needs to say who it is for and what it is about");
|
|
3949
|
+
if (!ask.staff?.brain) throw new Error(`${ask.staff?.name ?? "they"} has no brain repo`);
|
|
3950
|
+
if (repo !== ask.staff.brain)
|
|
3951
|
+
throw new Error(`an ask goes to ${ask.staff.brain}, not to ${repo}`);
|
|
3952
|
+
if (!ask.body?.trim()) throw new Error("an empty ask is not an ask");
|
|
3953
|
+
const title = (req.title ?? "").trim() || askTitle(ask);
|
|
3954
|
+
const args = ["issue", "create", "--repo", repo, "--title", title, "--body-file", "-"];
|
|
3955
|
+
for (const l of req.labels ?? []) args.push("--label", l);
|
|
3956
|
+
const { stdout } = await execWithStdin(args, askBody(ask));
|
|
3957
|
+
const url = stdout.trim().split("\n").pop();
|
|
3958
|
+
if (!req.alsoOnPr) return { ok: true, action, url };
|
|
3959
|
+
const note = `${ask.body.trim()}
|
|
3960
|
+
|
|
3961
|
+
\u2014 asked ${ask.staff.name} (${ask.staff.mention}): ${url ?? ask.staff.brain}`;
|
|
3962
|
+
try {
|
|
3963
|
+
const posted = await execWithStdin(
|
|
3964
|
+
["issue", "comment", String(ask.pr.number), "--repo", ask.pr.repo, "--body-file", "-"],
|
|
3965
|
+
note
|
|
3966
|
+
);
|
|
3967
|
+
return { ok: true, action, url, prUrl: posted.stdout.trim().split("\n").pop() };
|
|
3968
|
+
} catch (err) {
|
|
3969
|
+
return {
|
|
3970
|
+
ok: true,
|
|
3971
|
+
action,
|
|
3972
|
+
url,
|
|
3973
|
+
warning: `${ask.staff.name} was asked, but the copy on ${ask.pr.repo}#${ask.pr.number} did not go up: ${String(err?.message ?? err).split("\n")[0]}`
|
|
3974
|
+
};
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3682
3977
|
const number = req.number;
|
|
3683
3978
|
if (!Number.isInteger(number) || number <= 0)
|
|
3684
3979
|
throw new Error("a valid issue number is required");
|
|
@@ -3703,8 +3998,37 @@ async function act(req) {
|
|
|
3703
3998
|
await run3("gh", ["issue", "reopen", n, "--repo", repo], { encoding: "utf8" });
|
|
3704
3999
|
return { ok: true, action };
|
|
3705
4000
|
}
|
|
4001
|
+
if (action === "merge") {
|
|
4002
|
+
const how = req.mergeMethod ?? await mergeMethodFor(repo);
|
|
4003
|
+
const flag = MERGE_FLAG[how];
|
|
4004
|
+
if (!flag) throw new Error(`unknown merge method "${req.mergeMethod}"`);
|
|
4005
|
+
const args = ["pr", "merge", n, "--repo", repo, flag];
|
|
4006
|
+
if (req.body?.trim() && how !== "rebase") args.push("--body", req.body.trim());
|
|
4007
|
+
await run3("gh", args, { encoding: "utf8" });
|
|
4008
|
+
return { ok: true, action, mergedBy: how };
|
|
4009
|
+
}
|
|
3706
4010
|
throw new Error(`unknown action "${action}"`);
|
|
3707
4011
|
}
|
|
4012
|
+
async function mergeMethodFor(repo) {
|
|
4013
|
+
try {
|
|
4014
|
+
const { stdout } = await run3(
|
|
4015
|
+
"gh",
|
|
4016
|
+
[
|
|
4017
|
+
"api",
|
|
4018
|
+
`repos/${repo}`,
|
|
4019
|
+
"--jq",
|
|
4020
|
+
"[.allow_squash_merge,.allow_merge_commit,.allow_rebase_merge]"
|
|
4021
|
+
],
|
|
4022
|
+
{ encoding: "utf8" }
|
|
4023
|
+
);
|
|
4024
|
+
const [squash, commit, rebase] = JSON.parse(stdout);
|
|
4025
|
+
if (squash) return "squash";
|
|
4026
|
+
if (commit) return "merge";
|
|
4027
|
+
if (rebase) return "rebase";
|
|
4028
|
+
} catch {
|
|
4029
|
+
}
|
|
4030
|
+
return "squash";
|
|
4031
|
+
}
|
|
3708
4032
|
function execWithStdin(args, input) {
|
|
3709
4033
|
return new Promise((resolve6, reject) => {
|
|
3710
4034
|
const child = execFile3(
|
|
@@ -3717,26 +4041,144 @@ function execWithStdin(args, input) {
|
|
|
3717
4041
|
});
|
|
3718
4042
|
}
|
|
3719
4043
|
|
|
4044
|
+
// src/lib/attach.ts
|
|
4045
|
+
import { execFileSync as execFileSync7 } from "child_process";
|
|
4046
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
4047
|
+
import { basename, extname, join as join17 } from "path";
|
|
4048
|
+
var MAX_UPLOAD = 25 * 1024 * 1024;
|
|
4049
|
+
var ATTACH_DIR = "attachments";
|
|
4050
|
+
function attach(opts) {
|
|
4051
|
+
const { repoDir, repo, data, today } = opts;
|
|
4052
|
+
if (!existsSync15(join17(repoDir, ".git"))) {
|
|
4053
|
+
throw new Error(`${repo} is not checked out here, so there is nowhere to put the file`);
|
|
4054
|
+
}
|
|
4055
|
+
if (!data.length) throw new Error("that file is empty");
|
|
4056
|
+
if (data.length > MAX_UPLOAD) {
|
|
4057
|
+
throw new Error(`${basename(opts.name)} is larger than ${MAX_UPLOAD / 1024 / 1024}MB`);
|
|
4058
|
+
}
|
|
4059
|
+
const rel = free(repoDir, `${ATTACH_DIR}/${today}-${safeName(opts.name)}`);
|
|
4060
|
+
const full = join17(repoDir, rel);
|
|
4061
|
+
mkdirSync4(join17(repoDir, ATTACH_DIR), { recursive: true });
|
|
4062
|
+
writeFileSync7(full, data);
|
|
4063
|
+
const git4 = (args) => execFileSync7("git", ["-C", repoDir, ...args], {
|
|
4064
|
+
encoding: "utf8",
|
|
4065
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4066
|
+
});
|
|
4067
|
+
try {
|
|
4068
|
+
git4(["add", "--", rel]);
|
|
4069
|
+
git4(["commit", "-m", `portal: attach ${rel}`, "--", rel]);
|
|
4070
|
+
} catch (err) {
|
|
4071
|
+
throw new Error(`commit failed: ${short2(err)}`);
|
|
4072
|
+
}
|
|
4073
|
+
const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]).trim();
|
|
4074
|
+
const url = `https://github.com/${repo}/blob/${branch}/${rel.split("/").map(encodeURIComponent).join("/")}`;
|
|
4075
|
+
try {
|
|
4076
|
+
git4(["push"]);
|
|
4077
|
+
} catch (err) {
|
|
4078
|
+
return { path: rel, url, bytes: data.length, pushed: false, note: short2(err) };
|
|
4079
|
+
}
|
|
4080
|
+
return { path: rel, url, bytes: data.length, pushed: true };
|
|
4081
|
+
}
|
|
4082
|
+
function safeName(raw) {
|
|
4083
|
+
const name = basename(String(raw ?? "")).replace(/^\.+/, "");
|
|
4084
|
+
const ext = extname(name).toLowerCase().slice(0, 12);
|
|
4085
|
+
const stem = name.slice(0, name.length - extname(name).length).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "file";
|
|
4086
|
+
return stem + ext.replace(/[^a-z0-9.]/g, "");
|
|
4087
|
+
}
|
|
4088
|
+
function free(repoDir, rel) {
|
|
4089
|
+
if (!existsSync15(join17(repoDir, rel))) return rel;
|
|
4090
|
+
const ext = extname(rel);
|
|
4091
|
+
const stem = rel.slice(0, rel.length - ext.length);
|
|
4092
|
+
for (let n = 2; n < 500; n++) {
|
|
4093
|
+
const next = `${stem}-${n}${ext}`;
|
|
4094
|
+
if (!existsSync15(join17(repoDir, next))) return next;
|
|
4095
|
+
}
|
|
4096
|
+
throw new Error("too many files by that name today");
|
|
4097
|
+
}
|
|
4098
|
+
function short2(e) {
|
|
4099
|
+
const msg = e instanceof Error ? e.stderr?.toString() || e.message : String(e);
|
|
4100
|
+
const line = msg.split("\n").find((l) => l.trim()) ?? msg;
|
|
4101
|
+
return line.length > 200 ? `${line.slice(0, 199)}\u2026` : line;
|
|
4102
|
+
}
|
|
4103
|
+
|
|
3720
4104
|
// src/lib/docs.ts
|
|
3721
|
-
import { existsSync as
|
|
3722
|
-
import { dirname as dirname7, join as
|
|
4105
|
+
import { existsSync as existsSync16, readdirSync as readdirSync7, readFileSync as readFileSync14 } from "fs";
|
|
4106
|
+
import { dirname as dirname7, join as join18 } from "path";
|
|
3723
4107
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3724
4108
|
function docsDir() {
|
|
3725
4109
|
const here = dirname7(fileURLToPath2(import.meta.url));
|
|
3726
|
-
const candidates = [
|
|
3727
|
-
for (const path of candidates) if (
|
|
4110
|
+
const candidates = [join18(here, "..", "..", "docs"), join18(here, "..", "docs")];
|
|
4111
|
+
for (const path of candidates) if (existsSync16(path)) return path;
|
|
3728
4112
|
throw new Error(`docs not found. Looked in:
|
|
3729
4113
|
${candidates.join("\n ")}`);
|
|
3730
4114
|
}
|
|
3731
4115
|
function docPages() {
|
|
3732
4116
|
const dir = docsDir();
|
|
3733
|
-
const all =
|
|
3734
|
-
const index = readFileSync14(
|
|
4117
|
+
const all = readdirSync7(dir).filter((f) => f.endsWith(".md"));
|
|
4118
|
+
const index = readFileSync14(join18(dir, "README.md"), "utf8");
|
|
3735
4119
|
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);
|
|
3736
4120
|
const listed = new Set(ordered.map((d) => d.file));
|
|
3737
|
-
const rest = all.filter((f) => f !== "README.md" && !listed.has(f)).map((file) => ({ file, title: titleOf(
|
|
4121
|
+
const rest = all.filter((f) => f !== "README.md" && !listed.has(f)).map((file) => ({ file, title: titleOf(join18(dir, file)) }));
|
|
3738
4122
|
return [{ file: "README.md", title: "Overview" }, ...ordered, ...rest];
|
|
3739
4123
|
}
|
|
4124
|
+
var ASSET_TYPES = {
|
|
4125
|
+
".png": "image/png",
|
|
4126
|
+
".jpg": "image/jpeg",
|
|
4127
|
+
".jpeg": "image/jpeg",
|
|
4128
|
+
".gif": "image/gif",
|
|
4129
|
+
".svg": "image/svg+xml",
|
|
4130
|
+
".webp": "image/webp"
|
|
4131
|
+
};
|
|
4132
|
+
function docAsset(name) {
|
|
4133
|
+
const dir = join18(docsDir(), "images");
|
|
4134
|
+
if (!existsSync16(dir)) return null;
|
|
4135
|
+
const wanted = String(name ?? "").replace(/^images\//, "");
|
|
4136
|
+
const found = readdirSync7(dir).find((f) => f === wanted);
|
|
4137
|
+
if (!found) return null;
|
|
4138
|
+
const type = ASSET_TYPES[found.slice(found.lastIndexOf(".")).toLowerCase()];
|
|
4139
|
+
return type ? { path: join18(dir, found), type } : null;
|
|
4140
|
+
}
|
|
4141
|
+
function searchDocs(query2, snippets = 3) {
|
|
4142
|
+
const terms = String(query2 ?? "").toLowerCase().split(/\s+/).map((t) => t.trim()).filter(Boolean);
|
|
4143
|
+
if (!terms.length) return [];
|
|
4144
|
+
const dir = docsDir();
|
|
4145
|
+
const out = [];
|
|
4146
|
+
for (const page2 of docPages()) {
|
|
4147
|
+
const text = readFileSync14(join18(dir, page2.file), "utf8").replace(/^---\n[\s\S]*?\n---\n/, "");
|
|
4148
|
+
const haystack = (page2.title + "\n" + text).toLowerCase();
|
|
4149
|
+
if (!terms.every((t) => haystack.includes(t))) continue;
|
|
4150
|
+
const title = page2.title.toLowerCase();
|
|
4151
|
+
const matches = [];
|
|
4152
|
+
let body = 0;
|
|
4153
|
+
text.split("\n").forEach((raw, i) => {
|
|
4154
|
+
const line = raw.trim();
|
|
4155
|
+
if (!line) return;
|
|
4156
|
+
const hits = terms.filter((t) => line.toLowerCase().includes(t)).length;
|
|
4157
|
+
if (!hits) return;
|
|
4158
|
+
body += hits === terms.length ? 2 : 1;
|
|
4159
|
+
if (line.startsWith("#")) body += 8;
|
|
4160
|
+
if (matches.length < snippets) {
|
|
4161
|
+
matches.push({ line: i + 1, text: snippet(line, terms) });
|
|
4162
|
+
}
|
|
4163
|
+
});
|
|
4164
|
+
out.push({
|
|
4165
|
+
...page2,
|
|
4166
|
+
score: terms.filter((t) => title.includes(t)).length * 100 + Math.min(body, 60),
|
|
4167
|
+
matches
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
return out.sort((a, b) => b.score - a.score);
|
|
4171
|
+
}
|
|
4172
|
+
function snippet(line, terms, width = 150) {
|
|
4173
|
+
const clean = line.replace(/^#+\s*/, "").replace(/^[-*|]\s*/, "");
|
|
4174
|
+
if (clean.length <= width) return clean;
|
|
4175
|
+
const at = Math.min(
|
|
4176
|
+
...terms.map((t) => clean.toLowerCase().indexOf(t)).filter((n) => n >= 0),
|
|
4177
|
+
clean.length
|
|
4178
|
+
);
|
|
4179
|
+
const from = Math.max(0, at - 40);
|
|
4180
|
+
return (from ? "\u2026" : "") + clean.slice(from, from + width).trim() + "\u2026";
|
|
4181
|
+
}
|
|
3740
4182
|
function titleOf(path) {
|
|
3741
4183
|
const first = readFileSync14(path, "utf8").split("\n").find((l) => l.startsWith("# "));
|
|
3742
4184
|
return first ? first.slice(2).trim() : path.split("/").pop();
|
|
@@ -3746,9 +4188,15 @@ function titleOf(path) {
|
|
|
3746
4188
|
import { execFile as execFile4 } from "child_process";
|
|
3747
4189
|
import { promisify as promisify4 } from "util";
|
|
3748
4190
|
var run4 = promisify4(execFile4);
|
|
4191
|
+
var REACTIONS = `
|
|
4192
|
+
reactionGroups {
|
|
4193
|
+
content
|
|
4194
|
+
reactors(first:6) { totalCount nodes { ... on User { login } ... on Bot { login } } }
|
|
4195
|
+
}
|
|
4196
|
+
`;
|
|
3749
4197
|
var TIMELINE_COMMON = `
|
|
3750
4198
|
__typename
|
|
3751
|
-
... on IssueComment { author { login } createdAt body url }
|
|
4199
|
+
... on IssueComment { author { login } createdAt body url ${REACTIONS} }
|
|
3752
4200
|
... on CrossReferencedEvent {
|
|
3753
4201
|
actor { login } createdAt
|
|
3754
4202
|
source {
|
|
@@ -3804,6 +4252,7 @@ query($owner:String!, $name:String!) {
|
|
|
3804
4252
|
nodes {
|
|
3805
4253
|
number title body url state createdAt updatedAt
|
|
3806
4254
|
author { login }
|
|
4255
|
+
${REACTIONS}
|
|
3807
4256
|
labels(first:12) { nodes { name } }
|
|
3808
4257
|
assignees(first:8) { nodes { login } }
|
|
3809
4258
|
timelineItems(last:80, itemTypes:${ISSUE_TYPES}) { nodes { ${TIMELINE_COMMON} } }
|
|
@@ -3813,6 +4262,7 @@ query($owner:String!, $name:String!) {
|
|
|
3813
4262
|
nodes {
|
|
3814
4263
|
number title body url state createdAt updatedAt isDraft
|
|
3815
4264
|
author { login }
|
|
4265
|
+
${REACTIONS}
|
|
3816
4266
|
labels(first:12) { nodes { name } }
|
|
3817
4267
|
assignees(first:8) { nodes { login } }
|
|
3818
4268
|
commits(last:1) { nodes { commit { statusCheckRollup { state } } } }
|
|
@@ -3827,6 +4277,7 @@ query($owner:String!, $name:String!) {
|
|
|
3827
4277
|
nodes {
|
|
3828
4278
|
number title body url state createdAt updatedAt
|
|
3829
4279
|
author { login }
|
|
4280
|
+
${REACTIONS}
|
|
3830
4281
|
labels(first:12) { nodes { name } }
|
|
3831
4282
|
assignees(first:8) { nodes { login } }
|
|
3832
4283
|
timelineItems(last:${CLOSED_TIMELINE}, itemTypes:${ISSUE_TYPES}) {
|
|
@@ -3840,6 +4291,7 @@ query($owner:String!, $name:String!) {
|
|
|
3840
4291
|
nodes {
|
|
3841
4292
|
number title body url state createdAt updatedAt isDraft
|
|
3842
4293
|
author { login }
|
|
4294
|
+
${REACTIONS}
|
|
3843
4295
|
labels(first:12) { nodes { name } }
|
|
3844
4296
|
assignees(first:8) { nodes { login } }
|
|
3845
4297
|
timelineItems(last:${CLOSED_TIMELINE}, itemTypes:${PR_TYPES}) {
|
|
@@ -3876,7 +4328,7 @@ async function fetchInbox(repos) {
|
|
|
3876
4328
|
items.push(item);
|
|
3877
4329
|
}
|
|
3878
4330
|
} catch (e) {
|
|
3879
|
-
errors.push(`${full}: ${
|
|
4331
|
+
errors.push(`${full}: ${short3(e)}`);
|
|
3880
4332
|
}
|
|
3881
4333
|
})
|
|
3882
4334
|
);
|
|
@@ -3908,15 +4360,30 @@ function shape(n, repo, role, kind) {
|
|
|
3908
4360
|
url: n.url,
|
|
3909
4361
|
state: n.state ?? "OPEN",
|
|
3910
4362
|
comments: events.filter((e) => e.type === "comment").map((e) => ({ author: e.actor, createdAt: e.createdAt, body: e.body ?? "" })),
|
|
3911
|
-
events
|
|
4363
|
+
events,
|
|
4364
|
+
reactions: reactions(n)
|
|
3912
4365
|
};
|
|
3913
4366
|
}
|
|
4367
|
+
function reactions(n) {
|
|
4368
|
+
return (n.reactionGroups ?? []).filter((g) => (g.reactors?.totalCount ?? 0) > 0).map((g) => ({
|
|
4369
|
+
content: g.content,
|
|
4370
|
+
count: g.reactors.totalCount,
|
|
4371
|
+
by: (g.reactors.nodes ?? []).map((u) => u?.login).filter(Boolean)
|
|
4372
|
+
}));
|
|
4373
|
+
}
|
|
3914
4374
|
function event(e) {
|
|
3915
4375
|
const who = e.actor?.login ?? e.author?.login ?? "";
|
|
3916
4376
|
const at = e.createdAt;
|
|
3917
4377
|
switch (e.__typename) {
|
|
3918
4378
|
case "IssueComment":
|
|
3919
|
-
return {
|
|
4379
|
+
return {
|
|
4380
|
+
type: "comment",
|
|
4381
|
+
actor: who,
|
|
4382
|
+
createdAt: at,
|
|
4383
|
+
body: e.body ?? "",
|
|
4384
|
+
url: e.url,
|
|
4385
|
+
reactions: reactions(e)
|
|
4386
|
+
};
|
|
3920
4387
|
case "CrossReferencedEvent": {
|
|
3921
4388
|
const s = e.source;
|
|
3922
4389
|
if (!s?.number) return null;
|
|
@@ -4021,7 +4488,7 @@ function rollup(state) {
|
|
|
4021
4488
|
if (/FAILURE|ERROR/i.test(state)) return "failing";
|
|
4022
4489
|
return "pending";
|
|
4023
4490
|
}
|
|
4024
|
-
function
|
|
4491
|
+
function short3(e) {
|
|
4025
4492
|
const msg = e instanceof Error ? e.message : String(e);
|
|
4026
4493
|
const line = msg.split("\n").find((l) => l.trim() && !/^\s*$/.test(l)) ?? msg;
|
|
4027
4494
|
return line.length > 200 ? line.slice(0, 199) + "\u2026" : line;
|
|
@@ -4116,8 +4583,8 @@ function ensureNewline(text) {
|
|
|
4116
4583
|
}
|
|
4117
4584
|
|
|
4118
4585
|
// src/lib/pastebrief.ts
|
|
4119
|
-
import { existsSync as
|
|
4120
|
-
import { join as
|
|
4586
|
+
import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
|
|
4587
|
+
import { join as join19 } from "path";
|
|
4121
4588
|
var WRITES = {
|
|
4122
4589
|
discover: (c) => [`${c.opsName}/org/business.md`],
|
|
4123
4590
|
voice: (c) => [`${c.opsName}/org/voice.md`],
|
|
@@ -4208,13 +4675,13 @@ ${END}`).join("\n\n");
|
|
|
4208
4675
|
function fileBlock(ws, rel, note) {
|
|
4209
4676
|
const body = read2(ws, rel);
|
|
4210
4677
|
if (!body.trim()) return [`### \`${rel}\``, "", `*Empty or absent (${note}).*`, ""];
|
|
4211
|
-
return [`### \`${rel}\``, "", `*${note}*`, "",
|
|
4678
|
+
return [`### \`${rel}\``, "", `*${note}*`, "", fence3(body), ""];
|
|
4212
4679
|
}
|
|
4213
4680
|
function read2(ws, rel) {
|
|
4214
|
-
const full =
|
|
4215
|
-
return
|
|
4681
|
+
const full = join19(ws.root, rel);
|
|
4682
|
+
return existsSync17(full) ? readFileSync15(full, "utf8") : "";
|
|
4216
4683
|
}
|
|
4217
|
-
function
|
|
4684
|
+
function fence3(text) {
|
|
4218
4685
|
const longest = (text.match(/`{3,}/g) ?? []).reduce((n, m) => Math.max(n, m.length), 2);
|
|
4219
4686
|
const bars = "`".repeat(longest + 1);
|
|
4220
4687
|
return `${bars}
|
|
@@ -4222,15 +4689,15 @@ ${text.trimEnd()}
|
|
|
4222
4689
|
${bars}`;
|
|
4223
4690
|
}
|
|
4224
4691
|
function briefTemplate(kind) {
|
|
4225
|
-
const path =
|
|
4226
|
-
if (!
|
|
4692
|
+
const path = join19(briefTemplateDir(), `${kind}.md`);
|
|
4693
|
+
if (!existsSync17(path)) throw new Error(`no brief template for "${kind}"`);
|
|
4227
4694
|
return readFileSync15(path, "utf8");
|
|
4228
4695
|
}
|
|
4229
4696
|
|
|
4230
4697
|
// src/lib/setup.ts
|
|
4231
4698
|
import { execFile as execFile5 } from "child_process";
|
|
4232
|
-
import { existsSync as
|
|
4233
|
-
import { join as
|
|
4699
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync16 } from "fs";
|
|
4700
|
+
import { join as join20 } from "path";
|
|
4234
4701
|
var AGENTS = [
|
|
4235
4702
|
{
|
|
4236
4703
|
id: "claude-code-action",
|
|
@@ -4294,14 +4761,14 @@ async function setupStatus(startedIn) {
|
|
|
4294
4761
|
};
|
|
4295
4762
|
}
|
|
4296
4763
|
function readOrgName(opsDir) {
|
|
4297
|
-
const path =
|
|
4298
|
-
if (!
|
|
4764
|
+
const path = join20(opsDir, "org.yaml");
|
|
4765
|
+
if (!existsSync18(path)) return void 0;
|
|
4299
4766
|
const m = /^org:\s*(\S+)\s*$/m.exec(readFileSync16(path, "utf8"));
|
|
4300
4767
|
return m?.[1];
|
|
4301
4768
|
}
|
|
4302
4769
|
function readRepoNames(opsDir) {
|
|
4303
|
-
const path =
|
|
4304
|
-
if (!
|
|
4770
|
+
const path = join20(opsDir, "org.yaml");
|
|
4771
|
+
if (!existsSync18(path)) return [];
|
|
4305
4772
|
const names = [];
|
|
4306
4773
|
let inside = false;
|
|
4307
4774
|
for (const line of readFileSync16(path, "utf8").split("\n")) {
|
|
@@ -4317,29 +4784,29 @@ function readRepoNames(opsDir) {
|
|
|
4317
4784
|
return names;
|
|
4318
4785
|
}
|
|
4319
4786
|
async function loadFrameworkComposer() {
|
|
4320
|
-
const path =
|
|
4787
|
+
const path = join20(templatesRoot(), "ops", "compose.mjs");
|
|
4321
4788
|
return await import(`file://${path}`);
|
|
4322
4789
|
}
|
|
4323
4790
|
async function joinTenant(root, org) {
|
|
4324
|
-
|
|
4791
|
+
mkdirSync5(root, { recursive: true });
|
|
4325
4792
|
const cloned = [];
|
|
4326
|
-
const opsDir =
|
|
4327
|
-
if (!
|
|
4793
|
+
const opsDir = join20(root, "roster-ops");
|
|
4794
|
+
if (!existsSync18(opsDir)) {
|
|
4328
4795
|
await clone(`https://github.com/${org}/roster-ops.git`, opsDir);
|
|
4329
4796
|
cloned.push("roster-ops");
|
|
4330
4797
|
}
|
|
4331
|
-
if (!
|
|
4798
|
+
if (!existsSync18(join20(opsDir, "org.yaml"))) {
|
|
4332
4799
|
throw new Error(`${org}/roster-ops has no org.yaml, so it is not a roster tenant`);
|
|
4333
4800
|
}
|
|
4334
|
-
const { parseYaml } = await import(`file://${
|
|
4801
|
+
const { parseYaml } = await import(`file://${join20(opsDir, "compose.mjs")}`).then(
|
|
4335
4802
|
(m) => m
|
|
4336
4803
|
);
|
|
4337
|
-
const orgFile = parseYaml(readFileSync16(
|
|
4804
|
+
const orgFile = parseYaml(readFileSync16(join20(opsDir, "org.yaml"), "utf8"), "org.yaml");
|
|
4338
4805
|
for (const person of orgFile.staff ?? []) {
|
|
4339
4806
|
const dir = person.dir ?? person.handle;
|
|
4340
|
-
if (
|
|
4807
|
+
if (existsSync18(join20(root, dir))) continue;
|
|
4341
4808
|
try {
|
|
4342
|
-
await clone(`https://github.com/${org}/${dir}.git`,
|
|
4809
|
+
await clone(`https://github.com/${org}/${dir}.git`, join20(root, dir));
|
|
4343
4810
|
cloned.push(dir);
|
|
4344
4811
|
} catch {
|
|
4345
4812
|
}
|
|
@@ -4354,16 +4821,16 @@ function clone(url, into) {
|
|
|
4354
4821
|
|
|
4355
4822
|
// src/lib/sync.ts
|
|
4356
4823
|
import { execFile as execFile6 } from "child_process";
|
|
4357
|
-
import { existsSync as
|
|
4358
|
-
import { join as
|
|
4824
|
+
import { existsSync as existsSync19 } from "fs";
|
|
4825
|
+
import { join as join21 } from "path";
|
|
4359
4826
|
import { promisify as promisify5 } from "util";
|
|
4360
4827
|
var run5 = promisify5(execFile6);
|
|
4361
4828
|
async function syncRepos(root, dirs) {
|
|
4362
|
-
return Promise.all(dirs.map((dir) => syncOne(
|
|
4829
|
+
return Promise.all(dirs.map((dir) => syncOne(join21(root, dir), dir)));
|
|
4363
4830
|
}
|
|
4364
4831
|
async function syncOne(path, dir) {
|
|
4365
4832
|
const base2 = { dir, behind: 0, ahead: 0, dirty: false, pulled: false };
|
|
4366
|
-
if (!
|
|
4833
|
+
if (!existsSync19(join21(path, ".git"))) return { ...base2, skipped: "no-remote" };
|
|
4367
4834
|
const git4 = async (...args) => (await run5("git", ["-C", path, ...args], { encoding: "utf8" })).stdout.trim();
|
|
4368
4835
|
try {
|
|
4369
4836
|
const remotes = await git4("remote");
|
|
@@ -4384,7 +4851,7 @@ async function syncOne(path, dir) {
|
|
|
4384
4851
|
if (dirty) return { ...base2, ahead, behind, dirty, skipped: "dirty" };
|
|
4385
4852
|
if (ahead > 0) return { ...base2, ahead, behind, dirty, skipped: "diverged" };
|
|
4386
4853
|
await git4("pull", "--ff-only", "--quiet");
|
|
4387
|
-
return { ...base2, ahead, behind, dirty, pulled: true };
|
|
4854
|
+
return { ...base2, ahead, behind: 0, dirty, pulled: true };
|
|
4388
4855
|
} catch (err) {
|
|
4389
4856
|
return { ...base2, error: firstLine2(err) };
|
|
4390
4857
|
}
|
|
@@ -4396,23 +4863,23 @@ function firstLine2(e) {
|
|
|
4396
4863
|
}
|
|
4397
4864
|
|
|
4398
4865
|
// src/portal/assets.ts
|
|
4399
|
-
import { existsSync as
|
|
4400
|
-
import { dirname as dirname8, extname, join as
|
|
4866
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17, statSync as statSync4 } from "fs";
|
|
4867
|
+
import { dirname as dirname8, extname as extname2, join as join22, resolve as resolve4 } from "path";
|
|
4401
4868
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4402
4869
|
function portalDir() {
|
|
4403
4870
|
const here = dirname8(fileURLToPath3(import.meta.url));
|
|
4404
4871
|
const candidates = [
|
|
4405
|
-
|
|
4872
|
+
join22(here, "..", "..", "templates", "portal"),
|
|
4406
4873
|
// src/portal → package root
|
|
4407
|
-
|
|
4874
|
+
join22(here, "..", "templates", "portal")
|
|
4408
4875
|
// dist → package root
|
|
4409
4876
|
];
|
|
4410
|
-
for (const path of candidates) if (
|
|
4877
|
+
for (const path of candidates) if (existsSync20(join22(path, "index.html"))) return path;
|
|
4411
4878
|
throw new Error(`portal UI not found. Looked in:
|
|
4412
4879
|
${candidates.join("\n ")}`);
|
|
4413
4880
|
}
|
|
4414
4881
|
function portalIndex() {
|
|
4415
|
-
return readFileSync17(
|
|
4882
|
+
return readFileSync17(join22(portalDir(), "index.html"), "utf8");
|
|
4416
4883
|
}
|
|
4417
4884
|
var ASSET_MIME = {
|
|
4418
4885
|
".css": "text/css; charset=utf-8",
|
|
@@ -4426,15 +4893,15 @@ function portalAsset(rel) {
|
|
|
4426
4893
|
const root = portalDir();
|
|
4427
4894
|
const full = resolve4(root, rel);
|
|
4428
4895
|
if (!full.startsWith(resolve4(root) + "/")) return null;
|
|
4429
|
-
const type = ASSET_MIME[
|
|
4896
|
+
const type = ASSET_MIME[extname2(full).toLowerCase()];
|
|
4430
4897
|
if (!type) return null;
|
|
4431
|
-
if (!
|
|
4898
|
+
if (!existsSync20(full) || statSync4(full).isDirectory()) return null;
|
|
4432
4899
|
return { body: readFileSync17(full), type };
|
|
4433
4900
|
}
|
|
4434
4901
|
|
|
4435
4902
|
// src/commands/retire.ts
|
|
4436
|
-
import { existsSync as
|
|
4437
|
-
import { join as
|
|
4903
|
+
import { existsSync as existsSync21, readdirSync as readdirSync8, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
|
|
4904
|
+
import { join as join23 } from "path";
|
|
4438
4905
|
var retireHelp = `
|
|
4439
4906
|
roster retire <handle>
|
|
4440
4907
|
|
|
@@ -4485,23 +4952,23 @@ async function retireCommand(argv) {
|
|
|
4485
4952
|
function buildRetirePlan(ws, org, handle, parseYaml) {
|
|
4486
4953
|
const entry = (org.staff ?? []).find((s) => s.handle === handle);
|
|
4487
4954
|
const dir = entry.dir ?? entry.handle;
|
|
4488
|
-
const root =
|
|
4955
|
+
const root = join23(ws.root, dir);
|
|
4489
4956
|
const warnings = [];
|
|
4490
4957
|
const manifest = readManifest3(root, parseYaml);
|
|
4491
4958
|
const brain = manifest?.brain;
|
|
4492
|
-
if (!
|
|
4493
|
-
const wfDir =
|
|
4494
|
-
const workflows =
|
|
4959
|
+
if (!existsSync21(root)) warnings.push(`${dir}/ is not checked out, so its files cannot be read`);
|
|
4960
|
+
const wfDir = join23(root, ".github", "workflows");
|
|
4961
|
+
const workflows = existsSync21(wfDir) ? readdirSync8(wfDir).filter((f) => /\.ya?ml$/.test(f)).sort() : [];
|
|
4495
4962
|
if (!workflows.length) warnings.push("no workflows found, so nothing to disable");
|
|
4496
4963
|
const peers = [];
|
|
4497
4964
|
for (const other of org.staff ?? []) {
|
|
4498
4965
|
if (other.handle === handle) continue;
|
|
4499
4966
|
const otherDir = other.dir ?? other.handle;
|
|
4500
|
-
const path =
|
|
4501
|
-
if (!
|
|
4967
|
+
const path = join23(ws.root, otherDir, "staff.yaml");
|
|
4968
|
+
if (!existsSync21(path)) continue;
|
|
4502
4969
|
const text = readFileSync18(path, "utf8");
|
|
4503
4970
|
if (!new RegExp(`handle: ${handle}[,\\s}]`).test(text)) continue;
|
|
4504
|
-
const theirs = readManifest3(
|
|
4971
|
+
const theirs = readManifest3(join23(ws.root, otherDir), parseYaml);
|
|
4505
4972
|
peers.push({
|
|
4506
4973
|
handle: other.handle,
|
|
4507
4974
|
dir: otherDir,
|
|
@@ -4509,7 +4976,7 @@ function buildRetirePlan(ws, org, handle, parseYaml) {
|
|
|
4509
4976
|
label: `from-${handle}`
|
|
4510
4977
|
});
|
|
4511
4978
|
}
|
|
4512
|
-
const facts = countFacts(
|
|
4979
|
+
const facts = countFacts(join23(root, "memory", "INDEX.md"));
|
|
4513
4980
|
return {
|
|
4514
4981
|
handle,
|
|
4515
4982
|
name: entry.name ?? handle,
|
|
@@ -4567,9 +5034,9 @@ async function applyRetirePlan(ws, plan2) {
|
|
|
4567
5034
|
removeFromOrgYaml(ws, plan2);
|
|
4568
5035
|
process.stdout.write(" removed from org.yaml\n");
|
|
4569
5036
|
for (const p of plan2.peers) {
|
|
4570
|
-
const path =
|
|
5037
|
+
const path = join23(ws.root, p.dir, "staff.yaml");
|
|
4571
5038
|
const text = readFileSync18(path, "utf8");
|
|
4572
|
-
|
|
5039
|
+
writeFileSync8(path, removePeerLine(text, plan2.handle));
|
|
4573
5040
|
process.stdout.write(` removed from ${p.dir}/staff.yaml
|
|
4574
5041
|
`);
|
|
4575
5042
|
if (!p.brain) continue;
|
|
@@ -4595,9 +5062,9 @@ async function applyRetirePlan(ws, plan2) {
|
|
|
4595
5062
|
return failed ? 1 : 0;
|
|
4596
5063
|
}
|
|
4597
5064
|
function removeFromOrgYaml(ws, plan2) {
|
|
4598
|
-
const path =
|
|
5065
|
+
const path = join23(ws.opsDir, "org.yaml");
|
|
4599
5066
|
const text = readFileSync18(path, "utf8");
|
|
4600
|
-
|
|
5067
|
+
writeFileSync8(path, removeOrgLines(text, plan2.handle, plan2.dir));
|
|
4601
5068
|
}
|
|
4602
5069
|
function removeOrgLines(text, handle, dir) {
|
|
4603
5070
|
return text.split("\n").filter((line) => {
|
|
@@ -4615,8 +5082,8 @@ function removePeerLine(text, handle) {
|
|
|
4615
5082
|
}).join("\n");
|
|
4616
5083
|
}
|
|
4617
5084
|
function readManifest3(root, parseYaml) {
|
|
4618
|
-
const path =
|
|
4619
|
-
if (!
|
|
5085
|
+
const path = join23(root, "staff.yaml");
|
|
5086
|
+
if (!existsSync21(path)) return null;
|
|
4620
5087
|
try {
|
|
4621
5088
|
return parseYaml(readFileSync18(path, "utf8"), "staff.yaml");
|
|
4622
5089
|
} catch {
|
|
@@ -4624,7 +5091,7 @@ function readManifest3(root, parseYaml) {
|
|
|
4624
5091
|
}
|
|
4625
5092
|
}
|
|
4626
5093
|
function countFacts(indexPath) {
|
|
4627
|
-
if (!
|
|
5094
|
+
if (!existsSync21(indexPath)) return 0;
|
|
4628
5095
|
return (readFileSync18(indexPath, "utf8").match(/^- \*\*`/gm) ?? []).length;
|
|
4629
5096
|
}
|
|
4630
5097
|
function parseFlags9(argv) {
|
|
@@ -4682,8 +5149,20 @@ var MIME = {
|
|
|
4682
5149
|
".svg": "image/svg+xml",
|
|
4683
5150
|
".webp": "image/webp",
|
|
4684
5151
|
".ico": "image/x-icon",
|
|
4685
|
-
".pdf": "application/pdf"
|
|
5152
|
+
".pdf": "application/pdf",
|
|
5153
|
+
// A brain holds recordings as often as it holds screenshots, and a video served as
|
|
5154
|
+
// application/octet-stream is a download prompt rather than something you can watch.
|
|
5155
|
+
".mp4": "video/mp4",
|
|
5156
|
+
".m4v": "video/mp4",
|
|
5157
|
+
".webm": "video/webm",
|
|
5158
|
+
".mov": "video/quicktime",
|
|
5159
|
+
".ogv": "video/ogg",
|
|
5160
|
+
".mp3": "audio/mpeg",
|
|
5161
|
+
".m4a": "audio/mp4",
|
|
5162
|
+
".wav": "audio/wav",
|
|
5163
|
+
".oga": "audio/ogg"
|
|
4686
5164
|
};
|
|
5165
|
+
var SEEKABLE = /^(video|audio)\//;
|
|
4687
5166
|
var pendingApps = /* @__PURE__ */ new Map();
|
|
4688
5167
|
var appResults = /* @__PURE__ */ new Map();
|
|
4689
5168
|
async function portalCommand(argv) {
|
|
@@ -4704,6 +5183,8 @@ async function portalCommand(argv) {
|
|
|
4704
5183
|
};
|
|
4705
5184
|
let cache = null;
|
|
4706
5185
|
const TTL = 45e3;
|
|
5186
|
+
const labelCache = /* @__PURE__ */ new Map();
|
|
5187
|
+
const LABEL_TTL = 3e5;
|
|
4707
5188
|
const brainDirs = () => (readOrg(ws.opsDir, parseYaml).staff ?? []).map((s) => s.dir ?? s.handle);
|
|
4708
5189
|
const knownRepos = () => {
|
|
4709
5190
|
const org = readOrg(ws.opsDir, parseYaml);
|
|
@@ -4741,7 +5222,7 @@ async function portalCommand(argv) {
|
|
|
4741
5222
|
}
|
|
4742
5223
|
const dir = entry.dir ?? entry.handle;
|
|
4743
5224
|
const spec2 = specFromManifest(
|
|
4744
|
-
parseYaml(readFileSync19(
|
|
5225
|
+
parseYaml(readFileSync19(join24(ws.root, dir, "staff.yaml"), "utf8"), "staff.yaml"),
|
|
4745
5226
|
dir
|
|
4746
5227
|
);
|
|
4747
5228
|
const isPublic = payload.scope === "public";
|
|
@@ -4845,7 +5326,7 @@ async function portalCommand(argv) {
|
|
|
4845
5326
|
res.end(JSON.stringify({ error: "a repo name and a known role are needed" }));
|
|
4846
5327
|
return;
|
|
4847
5328
|
}
|
|
4848
|
-
const path =
|
|
5329
|
+
const path = join24(ws.opsDir, "org.yaml");
|
|
4849
5330
|
const text = readFileSync19(path, "utf8");
|
|
4850
5331
|
if (new RegExp(`name: ${name}[,\\s}]`).test(text)) {
|
|
4851
5332
|
json(res, { ok: true, note: "already there" });
|
|
@@ -4883,16 +5364,16 @@ async function portalCommand(argv) {
|
|
|
4883
5364
|
if (route === "plan") {
|
|
4884
5365
|
json(res, {
|
|
4885
5366
|
org,
|
|
4886
|
-
dir:
|
|
5367
|
+
dir: join24(startedIn, "roster-ops"),
|
|
4887
5368
|
files: [
|
|
4888
|
-
...initFiles({
|
|
5369
|
+
...(await initFiles({
|
|
4889
5370
|
org,
|
|
4890
5371
|
name: String(params.name ?? org),
|
|
4891
5372
|
human: String(params.human ?? ""),
|
|
4892
5373
|
marker: String(params.marker ?? "human"),
|
|
4893
5374
|
opsName: "roster-ops",
|
|
4894
5375
|
agent: String(params.agent ?? "claude-code-action")
|
|
4895
|
-
}).keys()
|
|
5376
|
+
})).keys()
|
|
4896
5377
|
].sort()
|
|
4897
5378
|
});
|
|
4898
5379
|
return;
|
|
@@ -4914,11 +5395,11 @@ async function portalCommand(argv) {
|
|
|
4914
5395
|
}
|
|
4915
5396
|
res.writeHead(404).end("not found");
|
|
4916
5397
|
};
|
|
4917
|
-
const body = (req) => new Promise((resolve6, reject) => {
|
|
5398
|
+
const body = (req, limit = 1e6) => new Promise((resolve6, reject) => {
|
|
4918
5399
|
let buf = "";
|
|
4919
5400
|
req.on("data", (c) => {
|
|
4920
5401
|
buf += c;
|
|
4921
|
-
if (buf.length >
|
|
5402
|
+
if (buf.length > limit) reject(new Error("body too large"));
|
|
4922
5403
|
});
|
|
4923
5404
|
req.on("end", () => resolve6(buf));
|
|
4924
5405
|
req.on("error", reject);
|
|
@@ -4949,6 +5430,10 @@ async function portalCommand(argv) {
|
|
|
4949
5430
|
res.end(JSON.stringify(docPages()));
|
|
4950
5431
|
return;
|
|
4951
5432
|
}
|
|
5433
|
+
if (url.pathname === "/api/docsearch") {
|
|
5434
|
+
json(res, { hits: searchDocs(url.searchParams.get("q") ?? "") });
|
|
5435
|
+
return;
|
|
5436
|
+
}
|
|
4952
5437
|
if (url.pathname === "/api/doc") {
|
|
4953
5438
|
const page2 = url.searchParams.get("page") ?? "";
|
|
4954
5439
|
if (!docPages().some((d) => d.file === page2)) {
|
|
@@ -4956,7 +5441,20 @@ async function portalCommand(argv) {
|
|
|
4956
5441
|
return;
|
|
4957
5442
|
}
|
|
4958
5443
|
res.writeHead(200, { "content-type": "text/plain; charset=utf-8" });
|
|
4959
|
-
res.end(readFileSync19(
|
|
5444
|
+
res.end(readFileSync19(join24(docsDir(), page2), "utf8"));
|
|
5445
|
+
return;
|
|
5446
|
+
}
|
|
5447
|
+
if (url.pathname === "/api/docasset") {
|
|
5448
|
+
const asset = docAsset(url.searchParams.get("path") ?? "");
|
|
5449
|
+
if (!asset) {
|
|
5450
|
+
res.writeHead(404).end("not found");
|
|
5451
|
+
return;
|
|
5452
|
+
}
|
|
5453
|
+
res.writeHead(200, {
|
|
5454
|
+
"content-type": asset.type,
|
|
5455
|
+
"cache-control": "public, max-age=86400"
|
|
5456
|
+
});
|
|
5457
|
+
res.end(readFileSync19(asset.path));
|
|
4960
5458
|
return;
|
|
4961
5459
|
}
|
|
4962
5460
|
if (url.pathname === "/setup/app/start") {
|
|
@@ -5041,6 +5539,23 @@ async function portalCommand(argv) {
|
|
|
5041
5539
|
res.end(JSON.stringify(data));
|
|
5042
5540
|
return;
|
|
5043
5541
|
}
|
|
5542
|
+
if (url.pathname === "/api/orglayer") {
|
|
5543
|
+
const listMd = (dir, prefix) => existsSync22(join24(w.opsDir, dir)) ? readdirSync9(join24(w.opsDir, dir)).filter((f) => f.endsWith(".md")).sort().map((f) => prefix + f) : [];
|
|
5544
|
+
const files = ["org.yaml", ...listMd("org", "org/"), ...listMd("prompts", "prompts/")].filter((rel) => isWritable(w, `${w.opsName}/${rel}`, brainDirs())).map((rel) => {
|
|
5545
|
+
const full = join24(w.opsDir, rel);
|
|
5546
|
+
const text = readFileSync19(full, "utf8");
|
|
5547
|
+
return {
|
|
5548
|
+
path: rel,
|
|
5549
|
+
bytes: statSync5(full).size,
|
|
5550
|
+
group: rel.startsWith("prompts/") ? "prompts" : "org",
|
|
5551
|
+
/* The first heading, for anything the page has no description of. Markdown
|
|
5552
|
+
only: `# ` opens a comment in YAML, and the first line of org.yaml is one. */
|
|
5553
|
+
title: rel.endsWith(".md") ? text.split("\n").find((l) => l.startsWith("# "))?.slice(2).trim() ?? "" : ""
|
|
5554
|
+
};
|
|
5555
|
+
});
|
|
5556
|
+
json(res, { files, opsName: w.opsName });
|
|
5557
|
+
return;
|
|
5558
|
+
}
|
|
5044
5559
|
if (url.pathname === "/api/prompt") {
|
|
5045
5560
|
const handle = url.searchParams.get("staff") ?? "";
|
|
5046
5561
|
const kind = url.searchParams.get("kind") ?? "daily";
|
|
@@ -5050,7 +5565,7 @@ async function portalCommand(argv) {
|
|
|
5050
5565
|
res.writeHead(400).end("bad request");
|
|
5051
5566
|
return;
|
|
5052
5567
|
}
|
|
5053
|
-
const brainDir =
|
|
5568
|
+
const brainDir = join24(w.root, entry.dir ?? entry.handle);
|
|
5054
5569
|
try {
|
|
5055
5570
|
const view = promptView(w, compose, handle, brainDir, kind);
|
|
5056
5571
|
view.problems = auditPrompt(w, view, workspaceRoots(org));
|
|
@@ -5168,6 +5683,34 @@ async function portalCommand(argv) {
|
|
|
5168
5683
|
});
|
|
5169
5684
|
return;
|
|
5170
5685
|
}
|
|
5686
|
+
if (url.pathname === "/api/promptaudit") {
|
|
5687
|
+
const handle = url.searchParams.get("staff") ?? "";
|
|
5688
|
+
const org = readOrg(w.opsDir, parseYaml);
|
|
5689
|
+
const entry = (org.staff ?? []).find((s) => s.handle === handle);
|
|
5690
|
+
if (!entry) {
|
|
5691
|
+
res.writeHead(400).end("bad request");
|
|
5692
|
+
return;
|
|
5693
|
+
}
|
|
5694
|
+
const brainDir = join24(w.root, entry.dir ?? entry.handle);
|
|
5695
|
+
const roots = workspaceRoots(org);
|
|
5696
|
+
const found = /* @__PURE__ */ new Map();
|
|
5697
|
+
const errors = [];
|
|
5698
|
+
for (const kind of KINDS) {
|
|
5699
|
+
try {
|
|
5700
|
+
const view = promptView(w, compose, handle, brainDir, kind);
|
|
5701
|
+
for (const p of auditPrompt(w, view, roots)) {
|
|
5702
|
+
const key = `${p.id}|${p.path ?? ""}|${p.title}`;
|
|
5703
|
+
const seen = found.get(key);
|
|
5704
|
+
if (seen) seen.kinds.push(kind);
|
|
5705
|
+
else found.set(key, { ...p, kind, kinds: [kind] });
|
|
5706
|
+
}
|
|
5707
|
+
} catch (err) {
|
|
5708
|
+
errors.push({ kind, error: String(err.message) });
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
json(res, { staff: handle, problems: [...found.values()], errors });
|
|
5712
|
+
return;
|
|
5713
|
+
}
|
|
5171
5714
|
if (url.pathname === "/api/amend") {
|
|
5172
5715
|
const handle = url.searchParams.get("staff") ?? "";
|
|
5173
5716
|
const kind = url.searchParams.get("kind") ?? "daily";
|
|
@@ -5179,9 +5722,9 @@ async function portalCommand(argv) {
|
|
|
5179
5722
|
return;
|
|
5180
5723
|
}
|
|
5181
5724
|
const dir = entry.dir ?? entry.handle;
|
|
5182
|
-
const view = promptView(w, compose, handle,
|
|
5183
|
-
const manifestPath =
|
|
5184
|
-
const tokens =
|
|
5725
|
+
const view = promptView(w, compose, handle, join24(w.root, dir), kind);
|
|
5726
|
+
const manifestPath = join24(w.root, dir, "staff.yaml");
|
|
5727
|
+
const tokens = existsSync22(manifestPath) ? tokensFor(
|
|
5185
5728
|
orgSpec(org, w),
|
|
5186
5729
|
specFromManifest(
|
|
5187
5730
|
parseYaml(readFileSync19(manifestPath, "utf8"), "staff.yaml"),
|
|
@@ -5242,6 +5785,10 @@ async function portalCommand(argv) {
|
|
|
5242
5785
|
const known = (org.repos ?? []).map((r) => `${org.org}/${r.name}`);
|
|
5243
5786
|
if (!known.includes(payload.repo))
|
|
5244
5787
|
throw new Error(`${payload.repo} is not a repo in org.yaml`);
|
|
5788
|
+
if (payload.action === "ask") {
|
|
5789
|
+
const prRepo = payload.ask?.pr?.repo ?? "";
|
|
5790
|
+
if (!known.includes(prRepo)) throw new Error(`${prRepo} is not a repo in org.yaml`);
|
|
5791
|
+
}
|
|
5245
5792
|
const result = await act(payload);
|
|
5246
5793
|
cache = null;
|
|
5247
5794
|
res.writeHead(200, {
|
|
@@ -5255,11 +5802,38 @@ async function portalCommand(argv) {
|
|
|
5255
5802
|
});
|
|
5256
5803
|
return;
|
|
5257
5804
|
}
|
|
5805
|
+
if (url.pathname === "/api/upload") {
|
|
5806
|
+
if (!writeAllowed(req)) {
|
|
5807
|
+
refuseWrite(res);
|
|
5808
|
+
return;
|
|
5809
|
+
}
|
|
5810
|
+
body(req, Math.ceil(MAX_UPLOAD * 1.4)).then((raw) => {
|
|
5811
|
+
const payload = JSON.parse(raw || "{}");
|
|
5812
|
+
const repo = payload.repo ?? "";
|
|
5813
|
+
const known = knownRepos().find((r) => `${r.owner}/${r.name}` === repo);
|
|
5814
|
+
if (!known) throw new Error(`${repo} is not a repo in org.yaml`);
|
|
5815
|
+
if (!payload.name) throw new Error("a file needs a name");
|
|
5816
|
+
json(
|
|
5817
|
+
res,
|
|
5818
|
+
attach({
|
|
5819
|
+
repoDir: join24(w.root, known.name),
|
|
5820
|
+
repo,
|
|
5821
|
+
name: payload.name,
|
|
5822
|
+
data: Buffer.from(payload.data ?? "", "base64"),
|
|
5823
|
+
today: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
5824
|
+
})
|
|
5825
|
+
);
|
|
5826
|
+
}).catch((err) => {
|
|
5827
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
5828
|
+
res.end(JSON.stringify({ error: String(err?.message ?? err) }));
|
|
5829
|
+
});
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
5258
5832
|
if (url.pathname === "/api/sync") {
|
|
5259
5833
|
const org = readOrg(w.opsDir, parseYaml);
|
|
5260
5834
|
const dirs = [w.opsName, ...(org.staff ?? []).map((s) => s.dir ?? s.handle)];
|
|
5261
5835
|
syncRepos(w.root, dirs).then((results) => {
|
|
5262
|
-
cache = null;
|
|
5836
|
+
if (results.some((r) => r.pulled)) cache = null;
|
|
5263
5837
|
res.writeHead(200, {
|
|
5264
5838
|
"content-type": "application/json; charset=utf-8",
|
|
5265
5839
|
"cache-control": "no-store"
|
|
@@ -5271,6 +5845,38 @@ async function portalCommand(argv) {
|
|
|
5271
5845
|
});
|
|
5272
5846
|
return;
|
|
5273
5847
|
}
|
|
5848
|
+
if (url.pathname === "/api/repos") {
|
|
5849
|
+
json(res, { repos: knownRepos() });
|
|
5850
|
+
return;
|
|
5851
|
+
}
|
|
5852
|
+
if (url.pathname === "/api/labels") {
|
|
5853
|
+
const repo = url.searchParams.get("repo") ?? "";
|
|
5854
|
+
if (!knownRepos().some((r) => `${r.owner}/${r.name}` === repo)) {
|
|
5855
|
+
res.writeHead(400).end("bad request");
|
|
5856
|
+
return;
|
|
5857
|
+
}
|
|
5858
|
+
const hit = labelCache.get(repo);
|
|
5859
|
+
if (hit && Date.now() - hit.at < LABEL_TTL) {
|
|
5860
|
+
json(res, { labels: hit.labels });
|
|
5861
|
+
return;
|
|
5862
|
+
}
|
|
5863
|
+
api(
|
|
5864
|
+
`repos/${repo}/labels?per_page=100`
|
|
5865
|
+
).then((result) => {
|
|
5866
|
+
if (!result.ok) {
|
|
5867
|
+
json(res, { labels: [], error: result.error });
|
|
5868
|
+
return;
|
|
5869
|
+
}
|
|
5870
|
+
const labels = (result.data ?? []).map((l) => ({
|
|
5871
|
+
name: l.name,
|
|
5872
|
+
color: l.color,
|
|
5873
|
+
description: l.description ?? ""
|
|
5874
|
+
}));
|
|
5875
|
+
labelCache.set(repo, { at: Date.now(), labels });
|
|
5876
|
+
json(res, { labels });
|
|
5877
|
+
}).catch((err) => json(res, { labels: [], error: String(err?.message ?? err) }));
|
|
5878
|
+
return;
|
|
5879
|
+
}
|
|
5274
5880
|
if (url.pathname === "/api/inbox") {
|
|
5275
5881
|
const fresh = url.searchParams.get("refresh") === "1";
|
|
5276
5882
|
if (!fresh && cache && Date.now() - cache.at < TTL) {
|
|
@@ -5296,6 +5902,55 @@ async function portalCommand(argv) {
|
|
|
5296
5902
|
});
|
|
5297
5903
|
return;
|
|
5298
5904
|
}
|
|
5905
|
+
if (url.pathname === "/api/pr") {
|
|
5906
|
+
const repo = url.searchParams.get("repo") ?? "";
|
|
5907
|
+
const number = Number(url.searchParams.get("number"));
|
|
5908
|
+
const allowed = knownRepos().some((r) => `${r.owner}/${r.name}` === repo);
|
|
5909
|
+
if (!allowed || !Number.isInteger(number) || number <= 0) {
|
|
5910
|
+
res.writeHead(400).end("bad request");
|
|
5911
|
+
return;
|
|
5912
|
+
}
|
|
5913
|
+
Promise.all([
|
|
5914
|
+
api(`repos/${repo}/pulls/${number}`),
|
|
5915
|
+
api(`repos/${repo}/pulls/${number}/commits?per_page=100`),
|
|
5916
|
+
api(`repos/${repo}/pulls/${number}/files?per_page=100`)
|
|
5917
|
+
]).then(([pr, commits, files]) => {
|
|
5918
|
+
if (!pr.ok) {
|
|
5919
|
+
json(res, { error: pr.error });
|
|
5920
|
+
return;
|
|
5921
|
+
}
|
|
5922
|
+
json(res, {
|
|
5923
|
+
base: pr.data.base?.ref,
|
|
5924
|
+
head: pr.data.head?.ref,
|
|
5925
|
+
draft: !!pr.data.draft,
|
|
5926
|
+
/* GitHub computes mergeability in the background, so the first read of a fresh
|
|
5927
|
+
PR can honestly answer "I do not know yet". Passed through as null rather
|
|
5928
|
+
than flattened to false, which would read as a conflict. */
|
|
5929
|
+
mergeable: pr.data.mergeable,
|
|
5930
|
+
mergeState: pr.data.mergeable_state,
|
|
5931
|
+
additions: pr.data.additions,
|
|
5932
|
+
deletions: pr.data.deletions,
|
|
5933
|
+
changedFiles: pr.data.changed_files,
|
|
5934
|
+
commits: (commits.data ?? []).map((c) => ({
|
|
5935
|
+
sha: String(c.sha).slice(0, 7),
|
|
5936
|
+
subject: String(c.commit?.message ?? "").split("\n")[0],
|
|
5937
|
+
author: c.author?.login ?? c.commit?.author?.name ?? "",
|
|
5938
|
+
date: c.commit?.author?.date,
|
|
5939
|
+
url: c.html_url
|
|
5940
|
+
})),
|
|
5941
|
+
files: (files.data ?? []).map((f) => ({
|
|
5942
|
+
path: f.filename,
|
|
5943
|
+
status: f.status,
|
|
5944
|
+
additions: f.additions,
|
|
5945
|
+
deletions: f.deletions,
|
|
5946
|
+
// Absent on binaries and on files too large for the API to patch.
|
|
5947
|
+
patch: f.patch ?? ""
|
|
5948
|
+
})),
|
|
5949
|
+
errors: [commits, files].filter((r) => !r.ok).map((r) => r.error)
|
|
5950
|
+
});
|
|
5951
|
+
}).catch((err) => json(res, { error: String(err?.message ?? err) }));
|
|
5952
|
+
return;
|
|
5953
|
+
}
|
|
5299
5954
|
if (url.pathname === "/api/thread") {
|
|
5300
5955
|
const repo = url.searchParams.get("repo") ?? "";
|
|
5301
5956
|
const number = Number(url.searchParams.get("number"));
|
|
@@ -5328,9 +5983,9 @@ async function portalCommand(argv) {
|
|
|
5328
5983
|
res.writeHead(400).end("bad request");
|
|
5329
5984
|
return;
|
|
5330
5985
|
}
|
|
5331
|
-
const out =
|
|
5986
|
+
const out = execFileSync8(
|
|
5332
5987
|
"git",
|
|
5333
|
-
["-C",
|
|
5988
|
+
["-C", join24(w.root, dir), "show", "--format=%an%x1f%aI%x1f%s", sha, "--", path],
|
|
5334
5989
|
{
|
|
5335
5990
|
encoding: "utf8",
|
|
5336
5991
|
maxBuffer: 8 * 1024 * 1024
|
|
@@ -5343,15 +5998,42 @@ async function portalCommand(argv) {
|
|
|
5343
5998
|
if (url.pathname === "/api/file") {
|
|
5344
5999
|
const rel = url.searchParams.get("path") ?? "";
|
|
5345
6000
|
const full = resolve5(w.root, rel);
|
|
5346
|
-
if (!full.startsWith(resolve5(w.root) + "/") || !
|
|
6001
|
+
if (!full.startsWith(resolve5(w.root) + "/") || !existsSync22(full) || statSync5(full).isDirectory()) {
|
|
5347
6002
|
res.writeHead(404).end("not found");
|
|
5348
6003
|
return;
|
|
5349
6004
|
}
|
|
5350
|
-
const ext =
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
6005
|
+
const ext = extname3(full).toLowerCase();
|
|
6006
|
+
const type = MIME[ext] ?? "application/octet-stream";
|
|
6007
|
+
if (SEEKABLE.test(type)) {
|
|
6008
|
+
const size2 = statSync5(full).size;
|
|
6009
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec(String(req.headers.range ?? ""));
|
|
6010
|
+
if (range) {
|
|
6011
|
+
const start = range[1] ? Number(range[1]) : 0;
|
|
6012
|
+
const end = range[2] ? Math.min(Number(range[2]), size2 - 1) : size2 - 1;
|
|
6013
|
+
if (!(start <= end && start < size2)) {
|
|
6014
|
+
res.writeHead(416, { "content-range": `bytes */${size2}` }).end();
|
|
6015
|
+
return;
|
|
6016
|
+
}
|
|
6017
|
+
res.writeHead(206, {
|
|
6018
|
+
"content-type": type,
|
|
6019
|
+
"content-length": end - start + 1,
|
|
6020
|
+
"content-range": `bytes ${start}-${end}/${size2}`,
|
|
6021
|
+
"accept-ranges": "bytes",
|
|
6022
|
+
"cache-control": "no-store"
|
|
6023
|
+
});
|
|
6024
|
+
createReadStream(full, { start, end }).pipe(res);
|
|
6025
|
+
return;
|
|
6026
|
+
}
|
|
6027
|
+
res.writeHead(200, {
|
|
6028
|
+
"content-type": type,
|
|
6029
|
+
"content-length": size2,
|
|
6030
|
+
"accept-ranges": "bytes",
|
|
6031
|
+
"cache-control": "no-store"
|
|
6032
|
+
});
|
|
6033
|
+
createReadStream(full).pipe(res);
|
|
6034
|
+
return;
|
|
6035
|
+
}
|
|
6036
|
+
res.writeHead(200, { "content-type": type, "cache-control": "no-store" });
|
|
5355
6037
|
res.end(readFileSync19(full));
|
|
5356
6038
|
return;
|
|
5357
6039
|
}
|
|
@@ -5403,7 +6085,7 @@ function buildPasteBrief(ws, parseYaml, kind, handle) {
|
|
|
5403
6085
|
throw new Error(`unknown staff handle "${handle}". org.yaml knows: ${known}`);
|
|
5404
6086
|
}
|
|
5405
6087
|
dir = entry.dir ?? entry.handle;
|
|
5406
|
-
const manifestPath =
|
|
6088
|
+
const manifestPath = join24(ws.root, dir, "staff.yaml");
|
|
5407
6089
|
tokens = tokensFor(
|
|
5408
6090
|
orgSpec(org, ws),
|
|
5409
6091
|
specFromManifest(parseYaml(readFileSync19(manifestPath, "utf8"), "staff.yaml"), dir)
|
|
@@ -5437,7 +6119,6 @@ function hireFlags(params) {
|
|
|
5437
6119
|
model: params.get("model") ?? void 0,
|
|
5438
6120
|
timeout: num("timeout"),
|
|
5439
6121
|
mentionTimeout: num("mentionTimeout"),
|
|
5440
|
-
prMentionTimeout: num("prMentionTimeout"),
|
|
5441
6122
|
secretPrefix: params.get("secretPrefix") ?? void 0,
|
|
5442
6123
|
app: params.get("app") ?? void 0,
|
|
5443
6124
|
publicApp: params.get("publicApp") ?? void 0,
|
|
@@ -5463,14 +6144,17 @@ function capture() {
|
|
|
5463
6144
|
};
|
|
5464
6145
|
}
|
|
5465
6146
|
function orgSpec(org, w) {
|
|
5466
|
-
const
|
|
6147
|
+
const humans = readHumans(org);
|
|
6148
|
+
const first = humans[0];
|
|
5467
6149
|
return {
|
|
5468
6150
|
org: org.org,
|
|
5469
6151
|
name: org.name,
|
|
5470
6152
|
opsRepo: `${org.org}/${w.opsName}`,
|
|
5471
6153
|
opsDirName: w.opsName,
|
|
5472
|
-
human:
|
|
5473
|
-
humanMarker:
|
|
6154
|
+
human: first?.name ?? first?.github ?? "the human",
|
|
6155
|
+
humanMarker: first?.marker ?? "human",
|
|
6156
|
+
humanLogins: humans.map((h) => h.github).filter(Boolean),
|
|
6157
|
+
allowedTools: toolsOf(org)
|
|
5474
6158
|
};
|
|
5475
6159
|
}
|
|
5476
6160
|
function workspaceRoots(org) {
|
|
@@ -5495,10 +6179,10 @@ function parseFlags10(argv) {
|
|
|
5495
6179
|
}
|
|
5496
6180
|
|
|
5497
6181
|
// src/commands/prompt.ts
|
|
5498
|
-
import { execFileSync as
|
|
5499
|
-
import { mkdtempSync as mkdtempSync3, writeFileSync as
|
|
6182
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
6183
|
+
import { mkdtempSync as mkdtempSync3, writeFileSync as writeFileSync9 } from "fs";
|
|
5500
6184
|
import { tmpdir as tmpdir3 } from "os";
|
|
5501
|
-
import { join as
|
|
6185
|
+
import { join as join25 } from "path";
|
|
5502
6186
|
|
|
5503
6187
|
// src/lib/livePrompt.ts
|
|
5504
6188
|
import { readFileSync as readFileSync20 } from "fs";
|
|
@@ -5529,7 +6213,7 @@ roster prompt <handle> [options]
|
|
|
5529
6213
|
agent is sent \u2014 the CLI imports the tenant's own compose.mjs, so there is no second
|
|
5530
6214
|
implementation to drift.
|
|
5531
6215
|
|
|
5532
|
-
--kind <k> daily | mention
|
|
6216
|
+
--kind <k> daily | mention (default: daily)
|
|
5533
6217
|
--diff <file> diff the composed prompt against the prompt: block in a workflow file
|
|
5534
6218
|
--stat with --diff, print a summary instead of the full diff
|
|
5535
6219
|
--ops <dir> ops repo directory (default: found by walking up)
|
|
@@ -5559,12 +6243,12 @@ async function promptCommand(argv) {
|
|
|
5559
6243
|
process.stdout.write(composed);
|
|
5560
6244
|
return 0;
|
|
5561
6245
|
}
|
|
5562
|
-
const live = extractLivePrompt(
|
|
5563
|
-
const dir = mkdtempSync3(
|
|
5564
|
-
const a =
|
|
5565
|
-
const b =
|
|
5566
|
-
|
|
5567
|
-
|
|
6246
|
+
const live = extractLivePrompt(join25(ws.root, opts.diff));
|
|
6247
|
+
const dir = mkdtempSync3(join25(tmpdir3(), "roster-diff-"));
|
|
6248
|
+
const a = join25(dir, "live.md");
|
|
6249
|
+
const b = join25(dir, "composed.md");
|
|
6250
|
+
writeFileSync9(a, normalise(live));
|
|
6251
|
+
writeFileSync9(b, normalise(composed));
|
|
5568
6252
|
const entry = org.staff?.find((s) => s.handle === handle);
|
|
5569
6253
|
process.stdout.write(`
|
|
5570
6254
|
${entry?.name ?? handle} \xB7 ${kind}
|
|
@@ -5579,7 +6263,7 @@ async function promptCommand(argv) {
|
|
|
5579
6263
|
else args.push("--unified=2");
|
|
5580
6264
|
args.push(a, b);
|
|
5581
6265
|
try {
|
|
5582
|
-
|
|
6266
|
+
execFileSync9("git", args, { stdio: "inherit" });
|
|
5583
6267
|
process.stdout.write(" identical\n\n");
|
|
5584
6268
|
return 0;
|
|
5585
6269
|
} catch {
|