@mmerterden/multi-agent-pipeline 20.29.0 → 20.33.0
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/CHANGELOG.md +86 -0
- package/README.md +50 -5
- package/README.tr.md +64 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +9 -9
- package/install/_unattended-profile.mjs +61 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +19 -3
- package/install/codex.mjs +2 -0
- package/install/copilot.mjs +13 -0
- package/install/index.mjs +10 -1
- package/install/templates/claude-hooks.json +12 -1
- package/manifest.json +230 -88
- package/package.json +3 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +13 -15
- package/pipeline/commands/multi-agent/office-off/SKILL.md +27 -0
- package/pipeline/commands/multi-agent/office-on/SKILL.md +51 -0
- package/pipeline/commands/multi-agent/office-status/SKILL.md +29 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +4 -3
- package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/serve/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/stack/SKILL.md +15 -8
- package/pipeline/commands/multi-agent/steer/SKILL.md +10 -9
- package/pipeline/commands/multi-agent/sync/SKILL.md +15 -3
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
- package/pipeline/contract/CHANGELOG.md +108 -0
- package/pipeline/contract/README.md +72 -12
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/fixtures/launch-plan.json +1 -1
- package/pipeline/contract/fixtures/runs-awaiting-question.json +6 -1
- package/pipeline/contract/fixtures/runs-empty.json +1 -1
- package/pipeline/contract/fixtures/runs-failed.json +6 -1
- package/pipeline/contract/fixtures/runs-old-schema.json +6 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +5 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +6 -1
- package/pipeline/contract/fixtures/runs-running.json +6 -1
- package/pipeline/contract/manifest.json +506 -27
- package/pipeline/contract/types/index.d.ts +829 -4
- package/pipeline/lib/bg-launch.mjs +106 -0
- package/pipeline/lib/code-digest.mjs +49 -0
- package/pipeline/lib/credential-resolve.mjs +42 -2
- package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -4
- package/pipeline/multi-agent-refs/features/office-threat-model.md +109 -0
- package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
- package/pipeline/multi-agent-refs/help/en.md +3 -0
- package/pipeline/multi-agent-refs/help/tr.md +3 -0
- package/pipeline/multi-agent-refs/phases/modes.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +16 -10
- package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
- package/pipeline/office/NOTICE.md +45 -0
- package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
- package/pipeline/office/assets/BIN.png +0 -0
- package/pipeline/office/assets/BOOKSHELF.png +0 -0
- package/pipeline/office/assets/CACTUS.png +0 -0
- package/pipeline/office/assets/CLOCK.png +0 -0
- package/pipeline/office/assets/COFFEE.png +0 -0
- package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
- package/pipeline/office/assets/COFFEE_TABLE.png +0 -0
- package/pipeline/office/assets/COUNTER_SINK.png +0 -0
- package/pipeline/office/assets/CUSHIONED_BENCH.png +0 -0
- package/pipeline/office/assets/CUSHIONED_CHAIR_BACK.png +0 -0
- package/pipeline/office/assets/CUSHIONED_CHAIR_FRONT.png +0 -0
- package/pipeline/office/assets/CUSHIONED_CHAIR_SIDE.png +0 -0
- package/pipeline/office/assets/DESK_FRONT.png +0 -0
- package/pipeline/office/assets/DESK_SIDE.png +0 -0
- package/pipeline/office/assets/DEVICE_STAND.png +0 -0
- package/pipeline/office/assets/DOUBLE_BOOKSHELF.png +0 -0
- package/pipeline/office/assets/FILING_CABINET.png +0 -0
- package/pipeline/office/assets/FRIDGE.png +0 -0
- package/pipeline/office/assets/HANGING_PLANT.png +0 -0
- package/pipeline/office/assets/LARGE_PAINTING.png +0 -0
- package/pipeline/office/assets/LARGE_PLANT.png +0 -0
- package/pipeline/office/assets/MAILBOX.png +0 -0
- package/pipeline/office/assets/MOP_BUCKET.png +0 -0
- package/pipeline/office/assets/PC_BACK.png +0 -0
- package/pipeline/office/assets/PC_FRONT_OFF.png +0 -0
- package/pipeline/office/assets/PC_FRONT_ON_1.png +0 -0
- package/pipeline/office/assets/PC_FRONT_ON_2.png +0 -0
- package/pipeline/office/assets/PC_FRONT_ON_3.png +0 -0
- package/pipeline/office/assets/PC_SIDE.png +0 -0
- package/pipeline/office/assets/PLANT.png +0 -0
- package/pipeline/office/assets/PLANT_2.png +0 -0
- package/pipeline/office/assets/POT.png +0 -0
- package/pipeline/office/assets/PRINTER.png +0 -0
- package/pipeline/office/assets/RECEPTION_DESK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK_2.png +0 -0
- package/pipeline/office/assets/SMALL_PAINTING.png +0 -0
- package/pipeline/office/assets/SMALL_PAINTING_2.png +0 -0
- package/pipeline/office/assets/SMALL_TABLE_FRONT.png +0 -0
- package/pipeline/office/assets/SMALL_TABLE_SIDE.png +0 -0
- package/pipeline/office/assets/SOFA_BACK.png +0 -0
- package/pipeline/office/assets/SOFA_FRONT.png +0 -0
- package/pipeline/office/assets/SOFA_SIDE.png +0 -0
- package/pipeline/office/assets/TABLE_FRONT.png +0 -0
- package/pipeline/office/assets/TOILET_STALL.png +0 -0
- package/pipeline/office/assets/VENDING.png +0 -0
- package/pipeline/office/assets/WASHBASIN.png +0 -0
- package/pipeline/office/assets/WATER_COOLER.png +0 -0
- package/pipeline/office/assets/WHITEBOARD.png +0 -0
- package/pipeline/office/assets/WOODEN_BENCH.png +0 -0
- package/pipeline/office/assets/WOODEN_CHAIR_BACK.png +0 -0
- package/pipeline/office/assets/WOODEN_CHAIR_FRONT.png +0 -0
- package/pipeline/office/assets/WOODEN_CHAIR_SIDE.png +0 -0
- package/pipeline/office/assets/char_0.png +0 -0
- package/pipeline/office/assets/char_1.png +0 -0
- package/pipeline/office/assets/char_2.png +0 -0
- package/pipeline/office/assets/char_3.png +0 -0
- package/pipeline/office/assets/char_4.png +0 -0
- package/pipeline/office/assets/char_5.png +0 -0
- package/pipeline/office/assets/floor_0.png +0 -0
- package/pipeline/office/assets/floor_1.png +0 -0
- package/pipeline/office/assets/floor_2.png +0 -0
- package/pipeline/office/assets/floor_3.png +0 -0
- package/pipeline/office/assets/floor_4.png +0 -0
- package/pipeline/office/assets/floor_5.png +0 -0
- package/pipeline/office/assets/floor_6.png +0 -0
- package/pipeline/office/assets/floor_7.png +0 -0
- package/pipeline/office/assets/floor_8.png +0 -0
- package/pipeline/office/assets/wall_0.png +0 -0
- package/pipeline/office/index.html +8431 -0
- package/pipeline/office/toolkit.json +3 -0
- package/pipeline/schemas/activity-chat.schema.json +78 -0
- package/pipeline/schemas/activity-roles.json +48 -0
- package/pipeline/schemas/activity-roles.schema.json +49 -0
- package/pipeline/schemas/activity.schema.json +613 -0
- package/pipeline/schemas/agent-state.schema.json +493 -115
- package/pipeline/schemas/autopilot-on-request.schema.json +33 -0
- package/pipeline/schemas/autopilot-on.schema.json +111 -0
- package/pipeline/schemas/contract-error.schema.json +45 -4
- package/pipeline/schemas/issue-maturity.schema.json +68 -0
- package/pipeline/schemas/job-change.schema.json +72 -0
- package/pipeline/schemas/job-request.schema.json +38 -0
- package/pipeline/schemas/launch.json +4 -4
- package/pipeline/schemas/ledger.schema.json +202 -0
- package/pipeline/schemas/office-credentials.schema.json +71 -0
- package/pipeline/schemas/office-jobs.json +253 -0
- package/pipeline/schemas/office-jobs.schema.json +62 -0
- package/pipeline/schemas/office-memory.schema.json +105 -0
- package/pipeline/schemas/office-settings-request.schema.json +30 -0
- package/pipeline/schemas/office-settings.schema.json +67 -0
- package/pipeline/schemas/permissions.schema.json +47 -0
- package/pipeline/schemas/phases.json +10 -2
- package/pipeline/schemas/phases.schema.json +86 -19
- package/pipeline/schemas/prefs.schema.json +19 -0
- package/pipeline/schemas/prs.schema.json +78 -0
- package/pipeline/schemas/prune-logs-request.schema.json +26 -0
- package/pipeline/schemas/prune-logs.schema.json +52 -0
- package/pipeline/schemas/repo-change.schema.json +54 -0
- package/pipeline/schemas/repo-request.schema.json +29 -0
- package/pipeline/schemas/repos.schema.json +49 -1
- package/pipeline/schemas/review-verdict-request.schema.json +20 -0
- package/pipeline/schemas/review-verdict.schema.json +62 -0
- package/pipeline/schemas/routine-delete.schema.json +59 -0
- package/pipeline/schemas/routines.schema.json +38 -0
- package/pipeline/schemas/run-evidence.schema.json +29 -0
- package/pipeline/schemas/run-results.schema.json +218 -0
- package/pipeline/schemas/runs-index.schema.json +257 -46
- package/pipeline/schemas/service-read.schema.json +58 -0
- package/pipeline/schemas/stack-change.schema.json +82 -0
- package/pipeline/schemas/stack-request.schema.json +33 -0
- package/pipeline/schemas/stack.schema.json +59 -0
- package/pipeline/schemas/steer-request.schema.json +25 -0
- package/pipeline/schemas/steer.schema.json +59 -0
- package/pipeline/schemas/system-status.schema.json +111 -0
- package/pipeline/schemas/tool-call-request.schema.json +19 -0
- package/pipeline/schemas/tool-call.schema.json +47 -0
- package/pipeline/schemas/tools.schema.json +65 -0
- package/pipeline/schemas/unattended-profile.json +7 -2
- package/pipeline/scripts/_active-run.mjs +2 -1
- package/pipeline/scripts/_cost.mjs +66 -1
- package/pipeline/scripts/_office-smoke-lib.sh +115 -0
- package/pipeline/scripts/_spawn-async.mjs +77 -0
- package/pipeline/scripts/_transcript-tail.mjs +274 -0
- package/pipeline/scripts/activity-snapshot.mjs +1223 -0
- package/pipeline/scripts/autopilot-control.mjs +214 -9
- package/pipeline/scripts/autopilot-runner.mjs +7 -5
- package/pipeline/scripts/contract-server.mjs +1853 -197
- package/pipeline/scripts/cost-analyze.mjs +2 -53
- package/pipeline/scripts/cost-table.json +1 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
- package/pipeline/scripts/job-launch.mjs +477 -0
- package/pipeline/scripts/launch-request.mjs +94 -9
- package/pipeline/scripts/office-build.mjs +226 -0
- package/pipeline/scripts/office-control.mjs +410 -0
- package/pipeline/scripts/office-credentials.mjs +106 -0
- package/pipeline/scripts/office-ledger.mjs +409 -0
- package/pipeline/scripts/office-memory.mjs +200 -0
- package/pipeline/scripts/office-prune.mjs +125 -0
- package/pipeline/scripts/office-settings.mjs +145 -0
- package/pipeline/scripts/phase-tracker.sh +12 -5
- package/pipeline/scripts/prune-logs.sh +53 -2
- package/pipeline/scripts/routine-registry.mjs +42 -6
- package/pipeline/scripts/run-results.mjs +373 -0
- package/pipeline/scripts/runs-index.mjs +62 -2
- package/pipeline/scripts/service-read.mjs +244 -0
- package/pipeline/scripts/stack-toggle.mjs +285 -0
- package/pipeline/scripts/steer-run.mjs +97 -0
- package/pipeline/scripts/system-status.mjs +196 -0
- package/pipeline/scripts/task-mirror.mjs +150 -0
- package/pipeline/scripts/task-mirror.sh +20 -0
- package/pipeline/scripts/toolkit-tools.mjs +625 -0
- package/pipeline/scripts/transcript-index.mjs +4 -1
- package/pipeline/scripts/uninstall.mjs +3 -0
- package/pipeline/scripts/work-items.mjs +473 -0
- package/pipeline/skills/.skill-manifest.json +23 -11
- package/pipeline/skills/.skills-index.json +34 -1
- package/pipeline/skills/shared/README.md +6 -3
- package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +13 -15
- package/pipeline/skills/shared/core/multi-agent-office-off/SKILL.md +16 -0
- package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +21 -0
- package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +17 -0
- package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -0
- package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-serve/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +14 -6
- package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +10 -9
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
- package/pipeline/skills/skills-index.md +4 -1
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
writeFileSync,
|
|
100
100
|
} from "node:fs";
|
|
101
101
|
import { spawnSync } from "node:child_process";
|
|
102
|
+
import { createHash } from "node:crypto";
|
|
102
103
|
import { homedir } from "node:os";
|
|
103
104
|
import { basename, isAbsolute, join } from "node:path";
|
|
104
105
|
import { catalog, commandsDir } from "./commands.mjs";
|
|
@@ -108,6 +109,7 @@ import { autopilotRoot, unattendedLogsRoot } from "../lib/pr-request-location.mj
|
|
|
108
109
|
import { lockTimeoutFrom, updateJsonFileSync } from "../lib/json-file-lock.mjs";
|
|
109
110
|
import { unattendedSettingsPath } from "../lib/unattended-settings-location.mjs";
|
|
110
111
|
import { isUnattended } from "../lib/unattended.mjs";
|
|
112
|
+
import { listRuns, logsRoot } from "./_run-paths.mjs";
|
|
111
113
|
|
|
112
114
|
export const CONTRACT_VERSION = "1.1.0";
|
|
113
115
|
export const MODES = ["interactive", "autopilot", "background"];
|
|
@@ -548,6 +550,84 @@ export function launchRepos(env = process.env) {
|
|
|
548
550
|
return out;
|
|
549
551
|
}
|
|
550
552
|
|
|
553
|
+
/** A stable, path-free handle for a repository path, as a client names one. */
|
|
554
|
+
export const repoId = (p) => createHash("sha256").update(String(p)).digest("hex").slice(0, 16);
|
|
555
|
+
|
|
556
|
+
const isCheckout = (real) => existsSync(join(real, ".git"));
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Checkouts earlier runs worked in that a launch does not accept yet: each
|
|
560
|
+
* run's projectRoot (and every projects[].projectRoot) under both log roots,
|
|
561
|
+
* by real path, kept when it is a git checkout and neither the home directory
|
|
562
|
+
* nor the root. A client offers them for registration; nothing is accepted
|
|
563
|
+
* until registerRepo stores one.
|
|
564
|
+
*
|
|
565
|
+
* @returns {{id: string, path: string, name: string}[]}
|
|
566
|
+
*/
|
|
567
|
+
export function suggestedRepos(env = process.env) {
|
|
568
|
+
const allowed = allowedLaunchRepos(env);
|
|
569
|
+
const home = realDir(homedir());
|
|
570
|
+
const out = new Map();
|
|
571
|
+
for (const root of new Set([logsRoot(), unattendedLogsRoot()])) {
|
|
572
|
+
for (const run of listRuns(root)) {
|
|
573
|
+
const state = readJsonOr(join(run.dir, "agent-state.json"), null);
|
|
574
|
+
if (!state || typeof state !== "object") continue;
|
|
575
|
+
const roots = [
|
|
576
|
+
state.projectRoot,
|
|
577
|
+
...(Array.isArray(state.projects) ? state.projects.map((x) => x?.projectRoot) : []),
|
|
578
|
+
];
|
|
579
|
+
for (const raw of roots) {
|
|
580
|
+
if (typeof raw !== "string" || !isAbsolute(raw)) continue;
|
|
581
|
+
const real = realDir(raw);
|
|
582
|
+
if (!real || real === "/" || real === home || allowed.has(real) || out.has(real)) continue;
|
|
583
|
+
if (!isCheckout(real)) continue;
|
|
584
|
+
out.set(real, { id: repoId(real), path: real, name: basename(real) });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return [...out.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Registered entries that are no longer a directory, as a client lists them
|
|
593
|
+
* for removal: launchRepos leaves them out, so nothing else would show them.
|
|
594
|
+
*
|
|
595
|
+
* @returns {{id: string, path: string, name: string}[]}
|
|
596
|
+
*/
|
|
597
|
+
export function missingRepos(env = process.env) {
|
|
598
|
+
const registered = readJsonOr(launchReposPath(env), {});
|
|
599
|
+
return (Array.isArray(registered?.repos) ? registered.repos : [])
|
|
600
|
+
.filter((p) => typeof p === "string" && p && !realDir(expandHome(p)))
|
|
601
|
+
.map((p) => ({ id: repoId(p), path: p, name: basename(p) }));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Remove one registered entry, named by repoId of its stored path. Configured
|
|
606
|
+
* autopilot repos are not touched: they belong to the autopilot config.
|
|
607
|
+
* Returns the removed path, or null when no entry has that id.
|
|
608
|
+
*/
|
|
609
|
+
export function unregisterRepo(id, env = process.env) {
|
|
610
|
+
let removed = null;
|
|
611
|
+
updateJsonFileSync(
|
|
612
|
+
launchReposPath(env),
|
|
613
|
+
(current) => {
|
|
614
|
+
const repos = Array.isArray(current?.repos) ? current.repos : [];
|
|
615
|
+
const keep = repos.filter((p) => {
|
|
616
|
+
// The list shows entries by their real path, so match that form too.
|
|
617
|
+
const real = typeof p === "string" ? realDir(expandHome(p)) : null;
|
|
618
|
+
if (removed === null && (repoId(p) === id || (real && repoId(real) === id))) {
|
|
619
|
+
removed = p;
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
return true;
|
|
623
|
+
});
|
|
624
|
+
return { ...current, repos: keep };
|
|
625
|
+
},
|
|
626
|
+
{ fallback: {}, mode: 0o600, timeoutMs: lockTimeoutFrom(env) },
|
|
627
|
+
);
|
|
628
|
+
return removed;
|
|
629
|
+
}
|
|
630
|
+
|
|
551
631
|
/** Is `repoPath` a directory a launch may start in? Compared by real path. */
|
|
552
632
|
export function launchRepoAllowed(repoPath, env = process.env) {
|
|
553
633
|
const real = typeof repoPath === "string" && isAbsolute(repoPath) ? realDir(repoPath) : null;
|
|
@@ -560,7 +640,7 @@ export function launchRepoAllowed(repoPath, env = process.env) {
|
|
|
560
640
|
* The read-modify-write holds the file's lock and replaces it atomically, so
|
|
561
641
|
* two registrations at once both land. Returns the stored path.
|
|
562
642
|
*/
|
|
563
|
-
export function registerRepo(repoPath, env = process.env) {
|
|
643
|
+
export function registerRepo(repoPath, env = process.env, { requireCheckout = false } = {}) {
|
|
564
644
|
if (typeof repoPath !== "string" || !isAbsolute(repoPath))
|
|
565
645
|
throw new Error("the path must be absolute");
|
|
566
646
|
const real = realDir(repoPath);
|
|
@@ -568,6 +648,7 @@ export function registerRepo(repoPath, env = process.env) {
|
|
|
568
648
|
if (real === "/" || real === realDir(homedir())) {
|
|
569
649
|
throw new Error("the home directory or the root is not a repository");
|
|
570
650
|
}
|
|
651
|
+
if (requireCheckout && !isCheckout(real)) throw new Error("the path is not a git checkout");
|
|
571
652
|
const file = launchReposPath(env);
|
|
572
653
|
mkdirSync(autopilotRoot(env), { recursive: true, mode: 0o700 });
|
|
573
654
|
updateJsonFileSync(
|
|
@@ -798,12 +879,15 @@ function planMain(rest) {
|
|
|
798
879
|
}
|
|
799
880
|
|
|
800
881
|
/** The `repos` document: launchRepos() with contractVersion; paths dropped under redact. */
|
|
801
|
-
export function reposDocument({ redact = false, env = process.env } = {}) {
|
|
882
|
+
export function reposDocument({ redact = false, env = process.env, extras = false } = {}) {
|
|
802
883
|
const repos = launchRepos(env);
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
884
|
+
const strip = (list) => (redact ? list.map(({ path: _p, ...r }) => r) : list);
|
|
885
|
+
const doc = { contractVersion: CONTRACT_VERSION, repos: strip(repos) };
|
|
886
|
+
if (extras) {
|
|
887
|
+
doc.suggested = strip(suggestedRepos(env));
|
|
888
|
+
doc.missing = strip(missingRepos(env));
|
|
889
|
+
}
|
|
890
|
+
return doc;
|
|
807
891
|
}
|
|
808
892
|
|
|
809
893
|
function resumeMain(rest) {
|
|
@@ -857,11 +941,12 @@ function main(argv) {
|
|
|
857
941
|
const [cmd, ...rest] = argv;
|
|
858
942
|
if (cmd === "repos") {
|
|
859
943
|
const redact = rest.includes("--redact");
|
|
860
|
-
|
|
861
|
-
|
|
944
|
+
const extras = rest.includes("--all");
|
|
945
|
+
if (rest.some((a) => a !== "--redact" && a !== "--all")) {
|
|
946
|
+
process.stderr.write("launch-request: repos takes only --redact and --all\n");
|
|
862
947
|
return 2;
|
|
863
948
|
}
|
|
864
|
-
process.stdout.write(JSON.stringify(reposDocument({ redact }), null, 2) + "\n");
|
|
949
|
+
process.stdout.write(JSON.stringify(reposDocument({ redact, extras }), null, 2) + "\n");
|
|
865
950
|
return 0;
|
|
866
951
|
}
|
|
867
952
|
if (cmd === "questions" || cmd === "spec") {
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* office-build.mjs - assemble the office panel page from pipeline/office-src.
|
|
4
|
+
*
|
|
5
|
+
* The server's CSP allows one nonce'd inline script, one nonce'd inline style
|
|
6
|
+
* and data: images, so the page has to be a single file. The sources stay
|
|
7
|
+
* readable as separate files (shell.html, style.css, world.js, app.js,
|
|
8
|
+
* strings.json) and this script inlines them into
|
|
9
|
+
* pipeline/office/index.html: the list of sprite names in pipeline/office/assets
|
|
10
|
+
* (served by contract-server.mjs at /office/assets/), the strings as a JSON
|
|
11
|
+
* literal, the two code files in order inside one IIFE. It also renders the
|
|
12
|
+
* pixel grids in pipeline/office-art into PNG sprites in pipeline/office/assets
|
|
13
|
+
* and writes pipeline/office/toolkit.json, the toolkit version shipped with
|
|
14
|
+
* the page; --check holds all three to their sources.
|
|
15
|
+
*
|
|
16
|
+
* node pipeline/scripts/office-build.mjs write the page
|
|
17
|
+
* node pipeline/scripts/office-build.mjs --check exit 1 when the page on
|
|
18
|
+
* disk is not what the
|
|
19
|
+
* sources build to
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { deflateSync } from "node:zlib";
|
|
25
|
+
import { basename, join } from "node:path";
|
|
26
|
+
import { pathToFileURL } from "node:url";
|
|
27
|
+
|
|
28
|
+
const ROOT = join(import.meta.dirname, "..");
|
|
29
|
+
const SRC = join(ROOT, "office-src");
|
|
30
|
+
const OUT = join(ROOT, "office", "index.html");
|
|
31
|
+
const ASSETS = join(ROOT, "office", "assets");
|
|
32
|
+
const TOOLKIT = join(ROOT, "office", "toolkit.json");
|
|
33
|
+
const ART = join(ROOT, "office-art");
|
|
34
|
+
const FACTS = join(ROOT, "..", "docs", "facts.json");
|
|
35
|
+
const PNG_SIGNATURE = "89504e470d0a1a0a";
|
|
36
|
+
|
|
37
|
+
// --- the project's own sprites -----------------------------------------------
|
|
38
|
+
// Each office-art/<NAME>.txt is a "size WxH" line and H rows of W letters, one
|
|
39
|
+
// letter per pixel from office-art/palette.json, "." transparent. They are
|
|
40
|
+
// encoded here into office/assets/<NAME>.png byte for byte the same each time
|
|
41
|
+
// (no timestamp, fixed compression), so --check can hold the PNGs to their
|
|
42
|
+
// text and a review reads the drawing as a diff.
|
|
43
|
+
|
|
44
|
+
const CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
45
|
+
let c = n;
|
|
46
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
47
|
+
return c >>> 0;
|
|
48
|
+
});
|
|
49
|
+
function crc32(buf) {
|
|
50
|
+
let c = 0xffffffff;
|
|
51
|
+
for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
52
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
53
|
+
}
|
|
54
|
+
function chunk(type, data) {
|
|
55
|
+
const len = Buffer.alloc(4);
|
|
56
|
+
len.writeUInt32BE(data.length);
|
|
57
|
+
const body = Buffer.concat([Buffer.from(type, "ascii"), data]);
|
|
58
|
+
const crc = Buffer.alloc(4);
|
|
59
|
+
crc.writeUInt32BE(crc32(body));
|
|
60
|
+
return Buffer.concat([len, body, crc]);
|
|
61
|
+
}
|
|
62
|
+
/** An RGBA PNG, unfiltered rows, zlib level 9. */
|
|
63
|
+
export function encodePng(width, height, rgba) {
|
|
64
|
+
const header = Buffer.alloc(13);
|
|
65
|
+
header.writeUInt32BE(width, 0);
|
|
66
|
+
header.writeUInt32BE(height, 4);
|
|
67
|
+
header[8] = 8;
|
|
68
|
+
header[9] = 6;
|
|
69
|
+
const raw = Buffer.alloc((width * 4 + 1) * height);
|
|
70
|
+
for (let y = 0; y < height; y++) {
|
|
71
|
+
rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4);
|
|
72
|
+
}
|
|
73
|
+
return Buffer.concat([
|
|
74
|
+
Buffer.from(PNG_SIGNATURE, "hex"),
|
|
75
|
+
chunk("IHDR", header),
|
|
76
|
+
chunk("IDAT", deflateSync(raw, { level: 9 })),
|
|
77
|
+
chunk("IEND", Buffer.alloc(0)),
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One art file as a PNG; throws on a size line, row length or letter that does not fit. */
|
|
82
|
+
export function renderArt(text, colors, name = "sprite") {
|
|
83
|
+
const lines = text.replace(/\r/g, "").trimEnd().split("\n");
|
|
84
|
+
const m = /^size (\d{1,3})x(\d{1,3})$/.exec(lines[0] ?? "");
|
|
85
|
+
if (!m) throw new Error(`${name}: the first line is "size WxH"`);
|
|
86
|
+
const [w, h] = [Number(m[1]), Number(m[2])];
|
|
87
|
+
const rows = lines.slice(1);
|
|
88
|
+
if (rows.length !== h) throw new Error(`${name}: ${rows.length} rows, expected ${h}`);
|
|
89
|
+
const rgba = Buffer.alloc(w * h * 4);
|
|
90
|
+
rows.forEach((row, y) => {
|
|
91
|
+
if (row.length !== w)
|
|
92
|
+
throw new Error(`${name}: row ${y + 1} is ${row.length} wide, expected ${w}`);
|
|
93
|
+
[...row].forEach((ch, x) => {
|
|
94
|
+
if (ch === ".") return;
|
|
95
|
+
const hex = colors[ch];
|
|
96
|
+
if (!hex) throw new Error(`${name}: "${ch}" is not in the palette`);
|
|
97
|
+
const o = (y * w + x) * 4;
|
|
98
|
+
rgba[o] = parseInt(hex.slice(0, 2), 16);
|
|
99
|
+
rgba[o + 1] = parseInt(hex.slice(2, 4), 16);
|
|
100
|
+
rgba[o + 2] = parseInt(hex.slice(4, 6), 16);
|
|
101
|
+
rgba[o + 3] = 255;
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
return encodePng(w, h, rgba);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Every art sprite, name -> PNG bytes. */
|
|
108
|
+
export function artSprites() {
|
|
109
|
+
const { colors } = JSON.parse(readFileSync(join(ART, "palette.json"), "utf8"));
|
|
110
|
+
const out = new Map();
|
|
111
|
+
for (const f of readdirSync(ART).sort()) {
|
|
112
|
+
if (!f.endsWith(".txt")) continue;
|
|
113
|
+
const name = basename(f, ".txt");
|
|
114
|
+
if (!/^[A-Z0-9_]{1,48}$/.test(name)) throw new Error(`${f}: sprite names are A-Z, 0-9 and _`);
|
|
115
|
+
out.set(name, renderArt(readFileSync(join(ART, f), "utf8"), colors, name));
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// The sprite names the page may load from /office/assets/: every PNG beside
|
|
121
|
+
// the page, checked to be one.
|
|
122
|
+
function atlas() {
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const name of readdirSync(ASSETS).sort()) {
|
|
125
|
+
if (!name.endsWith(".png")) continue;
|
|
126
|
+
const buf = readFileSync(join(ASSETS, name));
|
|
127
|
+
if (buf.subarray(0, 8).toString("hex") !== PNG_SIGNATURE) {
|
|
128
|
+
throw new Error(`${name} is not a PNG file`);
|
|
129
|
+
}
|
|
130
|
+
out.push(basename(name, ".png"));
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The toolkit version the office's Tools window starts, shipped beside the
|
|
137
|
+
* page: the version this release was checked against, from docs/facts.json.
|
|
138
|
+
* The host runs the toolkit as @latest; the panel only runs an exact version,
|
|
139
|
+
* and every pipeline update brings the newer one with it.
|
|
140
|
+
*/
|
|
141
|
+
export function toolkitDoc() {
|
|
142
|
+
const v = JSON.parse(readFileSync(FACTS, "utf8")).toolkitVersion;
|
|
143
|
+
if (typeof v !== "string" || !/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v)) {
|
|
144
|
+
throw new Error("docs/facts.json carries no exact toolkitVersion");
|
|
145
|
+
}
|
|
146
|
+
return JSON.stringify({ toolkitVersion: v }, null, 2) + "\n";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function build() {
|
|
150
|
+
const read = (f) => readFileSync(join(SRC, f), "utf8");
|
|
151
|
+
const strings = JSON.parse(read("strings.json"));
|
|
152
|
+
const sprites = atlas();
|
|
153
|
+
const script = [
|
|
154
|
+
'"use strict";',
|
|
155
|
+
"(() => {",
|
|
156
|
+
`const ATLAS = ${JSON.stringify(sprites)};`,
|
|
157
|
+
`const STRINGS = ${JSON.stringify(strings)};`,
|
|
158
|
+
'const spriteUrl = (name) => "/office/assets/" + name + ".png";',
|
|
159
|
+
read("world.js").trim(),
|
|
160
|
+
read("app.js").trim(),
|
|
161
|
+
"})();",
|
|
162
|
+
].join("\n");
|
|
163
|
+
const style = read("style.css").trim();
|
|
164
|
+
// The build id the server reads back from the installed page: the page
|
|
165
|
+
// compares it with its own to tell that a newer one has been installed.
|
|
166
|
+
const hash = createHash("sha256").update(script).update(style).update(read("shell.html"));
|
|
167
|
+
// Redrawn sprites change the page too, though no source text did.
|
|
168
|
+
for (const [name, png] of artSprites()) hash.update(name).update(png);
|
|
169
|
+
const build = hash.digest("hex").slice(0, 12);
|
|
170
|
+
// Text that would close the inline element early, or open an HTML comment
|
|
171
|
+
// inside it, turns source into markup.
|
|
172
|
+
if (/<\/script|<!--/i.test(script)) throw new Error("the script contains </script or <!--");
|
|
173
|
+
if (/<\/style/i.test(style)) throw new Error("style.css contains </style");
|
|
174
|
+
const shell = read("shell.html");
|
|
175
|
+
for (const marker of ["/*STYLE*/", "/*SCRIPT*/"]) {
|
|
176
|
+
if (shell.split(marker).length !== 2) throw new Error(`shell.html needs exactly one ${marker}`);
|
|
177
|
+
}
|
|
178
|
+
const stamped = script.replace('"use strict";', () => `"use strict";\nconst BUILD = "${build}";`);
|
|
179
|
+
return shell.replace("/*STYLE*/", () => style).replace("/*SCRIPT*/", () => stamped);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
183
|
+
if (isMain) {
|
|
184
|
+
const art = artSprites();
|
|
185
|
+
const check = process.argv.includes("--check");
|
|
186
|
+
if (!check) for (const [name, png] of art) writeFileSync(join(ASSETS, `${name}.png`), png);
|
|
187
|
+
const html = build();
|
|
188
|
+
const toolkit = toolkitDoc();
|
|
189
|
+
if (check) {
|
|
190
|
+
for (const [name, png] of art) {
|
|
191
|
+
let on = null;
|
|
192
|
+
try {
|
|
193
|
+
on = readFileSync(join(ASSETS, `${name}.png`));
|
|
194
|
+
} catch {
|
|
195
|
+
// Missing is stale.
|
|
196
|
+
}
|
|
197
|
+
if (!on || !on.equals(png)) {
|
|
198
|
+
console.error(
|
|
199
|
+
`office/assets/${name}.png is stale: run node pipeline/scripts/office-build.mjs`,
|
|
200
|
+
);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const read = (f) => {
|
|
205
|
+
try {
|
|
206
|
+
return readFileSync(f, "utf8");
|
|
207
|
+
} catch {
|
|
208
|
+
// A missing file is stale by definition.
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
if (read(OUT) !== html) {
|
|
213
|
+
console.error("office/index.html is stale: run node pipeline/scripts/office-build.mjs");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
if (read(TOOLKIT) !== toolkit) {
|
|
217
|
+
console.error("office/toolkit.json is stale: run node pipeline/scripts/office-build.mjs");
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
console.log("office/index.html and office/toolkit.json match their sources");
|
|
221
|
+
} else {
|
|
222
|
+
writeFileSync(OUT, html);
|
|
223
|
+
writeFileSync(TOOLKIT, toolkit);
|
|
224
|
+
console.log(`wrote ${OUT} (${Buffer.byteLength(html)} bytes) and ${TOOLKIT}`);
|
|
225
|
+
}
|
|
226
|
+
}
|