@delorenj/pjangler 1.2.21 → 1.2.25
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/index.js +1852 -368
- package/dist/mcp-server.js +1782 -296
- package/package.json +14 -3
- package/templates/commonproject/copier.yml +3 -0
- package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
- package/templates/commonproject/template/mise.toml.jinja +9 -2
- package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
- package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
- package/templates/hermes-agent/README.md +70 -29
- package/templates/hermes-agent/copier.yml +9 -6
- package/templates/hermes-agent/docs/architecture.md +75 -51
- package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
- package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
- package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
- package/templates/hermes-agent/docs/operations.md +179 -81
- package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
- package/templates/hermes-agent/docs/sentinel/README.md +10 -10
- package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
- package/templates/hermes-agent/docs/sentinel/development.md +12 -15
- package/templates/hermes-agent/docs/sentinel.md +1 -1
- package/templates/hermes-agent/install-local.sh +41 -4
- package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
- package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
- package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
- package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
- package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
- package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
- package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
- package/templates/hermes-agent/template/.gitignore.jinja +5 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
- package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
- package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
- package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
- package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
- package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
- package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +1 -1
- package/templates/hermes-agent/template/SOUL.md.jinja +2 -2
- package/templates/hermes-agent/template/hermes.jinja +6 -3
- package/templates/hermes-agent/template/role.yaml.jinja +24 -13
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
- package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
- package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
- package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
- package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
- package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
- package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
- package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
- package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
- package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
- package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
- package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +0 -181
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5
|
-
import { existsSync as existsSync11, readFileSync as
|
|
6
|
-
import { basename as
|
|
5
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { basename as basename5, join as join16, resolve as resolve4 } from "node:path";
|
|
7
7
|
import { Command as Command3 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/hermes/types.ts
|
|
@@ -19,7 +19,7 @@ function deriveProfileName(repo, role) {
|
|
|
19
19
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
20
20
|
import { homedir, platform } from "node:os";
|
|
21
21
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
22
|
-
import { join as
|
|
22
|
+
import { join as join3, dirname as dirname2 } from "node:path";
|
|
23
23
|
|
|
24
24
|
// src/commands/Command.ts
|
|
25
25
|
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
@@ -57,19 +57,149 @@ var Command = class {
|
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// src/parity/bmadPack.ts
|
|
61
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync } from "node:fs";
|
|
62
|
+
import { createHash } from "node:crypto";
|
|
63
|
+
import { basename, join as join2, relative, resolve, sep } from "node:path";
|
|
64
|
+
var BMAD_PACK_VERSION = "6.10.1-next.31";
|
|
65
|
+
var BMAD_PACK_CHECKSUMS_SHA256 = "a8bc005612ac60e3ec775fff5a11eafe38be6acdae96efa3d770b48322cb3224";
|
|
66
|
+
var BMAD_PACK_SKILL_COUNT = 76;
|
|
67
|
+
var BMAD_PACK_PAYLOAD_FILES = 1072;
|
|
68
|
+
function sha256(content) {
|
|
69
|
+
return createHash("sha256").update(content).digest("hex");
|
|
70
|
+
}
|
|
71
|
+
function readRegularFile(path) {
|
|
72
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
73
|
+
try {
|
|
74
|
+
if (!fstatSync(fd).isFile()) throw new Error(`BMAD pack entry is not a regular file: ${path}`);
|
|
75
|
+
return readFileSync(fd);
|
|
76
|
+
} finally {
|
|
77
|
+
closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function safeRelativePath(value) {
|
|
81
|
+
if (!value || value.includes("\\") || value.startsWith("/") || value.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
82
|
+
throw new Error(`Unsafe BMAD checksum path: ${JSON.stringify(value)}`);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function parsePackToml(content) {
|
|
87
|
+
const required = [
|
|
88
|
+
/^name = "bmad"$/m,
|
|
89
|
+
new RegExp(`^version = "${BMAD_PACK_VERSION.replaceAll(".", "\\.")}"$`, "m"),
|
|
90
|
+
/^upstream = "bmad-method"$/m,
|
|
91
|
+
new RegExp(`^upstream_version = "${BMAD_PACK_VERSION.replaceAll(".", "\\.")}"$`, "m"),
|
|
92
|
+
/^rendered_from = "\.agent\/skills"$/m,
|
|
93
|
+
/^immutable = true$/m,
|
|
94
|
+
/^project_projection = "symlink"$/m
|
|
95
|
+
];
|
|
96
|
+
if (required.some((pattern) => !pattern.test(content))) {
|
|
97
|
+
throw new Error(`BMAD pack.toml does not declare the trusted ${BMAD_PACK_VERSION} contract`);
|
|
98
|
+
}
|
|
99
|
+
const payloadMatch = content.match(/^payload_files = (\d+)$/m);
|
|
100
|
+
const skillsMatch = content.match(/^skills = \[\n([\s\S]*?)^\]$/m);
|
|
101
|
+
if (!payloadMatch || !skillsMatch) throw new Error("BMAD pack.toml is missing payload inventory metadata");
|
|
102
|
+
const skillNames = [...skillsMatch[1].matchAll(/^\s+"([^"]+)",$/gm)].map((match) => match[1]);
|
|
103
|
+
const payloadFiles = Number.parseInt(payloadMatch[1], 10);
|
|
104
|
+
if (skillNames.length !== BMAD_PACK_SKILL_COUNT || new Set(skillNames).size !== skillNames.length) {
|
|
105
|
+
throw new Error(`BMAD pack.toml must declare exactly ${BMAD_PACK_SKILL_COUNT} unique skills`);
|
|
106
|
+
}
|
|
107
|
+
if (payloadFiles !== BMAD_PACK_PAYLOAD_FILES) {
|
|
108
|
+
throw new Error(`BMAD pack.toml must declare exactly ${BMAD_PACK_PAYLOAD_FILES} payload files`);
|
|
109
|
+
}
|
|
110
|
+
for (const name of skillNames) {
|
|
111
|
+
if (!name.startsWith("bmad-") || basename(name) !== name) throw new Error(`Unsafe BMAD skill identity: ${name}`);
|
|
112
|
+
}
|
|
113
|
+
return { skillNames, payloadFiles };
|
|
114
|
+
}
|
|
115
|
+
function walkRegularTree(root) {
|
|
116
|
+
const files = /* @__PURE__ */ new Map();
|
|
117
|
+
const directories = /* @__PURE__ */ new Set();
|
|
118
|
+
const visit = (directory) => {
|
|
119
|
+
for (const name of readdirSync(directory).sort()) {
|
|
120
|
+
const path = join2(directory, name);
|
|
121
|
+
const stat = lstatSync(path);
|
|
122
|
+
if (stat.isSymbolicLink()) throw new Error(`BMAD pack may not contain symlinks: ${path}`);
|
|
123
|
+
if (stat.isDirectory()) {
|
|
124
|
+
directories.add(relative(root, path).split(sep).join("/"));
|
|
125
|
+
visit(path);
|
|
126
|
+
} else if (stat.isFile()) files.set(relative(root, path).split(sep).join("/"), readRegularFile(path));
|
|
127
|
+
else throw new Error(`BMAD pack may contain only regular files/directories: ${path}`);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
visit(root);
|
|
131
|
+
return { files, directories };
|
|
132
|
+
}
|
|
133
|
+
function validateTrustedBmadPack(packRoot) {
|
|
134
|
+
const root = resolve(packRoot);
|
|
135
|
+
const rootStat = lstatSync(root);
|
|
136
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
137
|
+
throw new Error(`BMAD pack root must be a real directory: ${root}`);
|
|
138
|
+
}
|
|
139
|
+
const checksumsContent = readRegularFile(join2(root, "SHA256SUMS"));
|
|
140
|
+
if (sha256(checksumsContent) !== BMAD_PACK_CHECKSUMS_SHA256) {
|
|
141
|
+
throw new Error(`BMAD pack checksum manifest is not the trusted ${BMAD_PACK_VERSION} manifest`);
|
|
142
|
+
}
|
|
143
|
+
const expected = /* @__PURE__ */ new Map();
|
|
144
|
+
for (const line of checksumsContent.toString("utf8").split("\n")) {
|
|
145
|
+
if (!line) continue;
|
|
146
|
+
const match = line.match(/^([0-9a-f]{64}) (.+)$/);
|
|
147
|
+
if (!match) throw new Error(`Invalid BMAD SHA256SUMS entry: ${line}`);
|
|
148
|
+
const path = safeRelativePath(match[2]);
|
|
149
|
+
if (expected.has(path)) throw new Error(`Duplicate BMAD SHA256SUMS entry: ${path}`);
|
|
150
|
+
expected.set(path, match[1]);
|
|
151
|
+
}
|
|
152
|
+
const walked = walkRegularTree(root);
|
|
153
|
+
const actual = walked.files;
|
|
154
|
+
actual.delete("SHA256SUMS");
|
|
155
|
+
const missing = [...expected.keys()].filter((path) => !actual.has(path));
|
|
156
|
+
const extra = [...actual.keys()].filter((path) => !expected.has(path));
|
|
157
|
+
if (missing.length || extra.length) {
|
|
158
|
+
throw new Error(`BMAD checksum coverage mismatch; missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
|
|
159
|
+
}
|
|
160
|
+
for (const [path, digest] of expected) {
|
|
161
|
+
const content = actual.get(path);
|
|
162
|
+
if (!content || sha256(content) !== digest) throw new Error(`BMAD pack digest mismatch: ${path}`);
|
|
163
|
+
}
|
|
164
|
+
for (const directory of walked.directories) {
|
|
165
|
+
if (![...actual.keys()].some((path) => path.startsWith(`${directory}/`))) {
|
|
166
|
+
throw new Error(`BMAD pack contains an unauthenticated empty directory: ${directory}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const packToml = actual.get("pack.toml");
|
|
170
|
+
if (!packToml) throw new Error("BMAD pack.toml is missing");
|
|
171
|
+
const metadata = parsePackToml(packToml.toString("utf8"));
|
|
172
|
+
const skillSet = new Set(metadata.skillNames);
|
|
173
|
+
const topLevelDirectories = readdirSync(root).filter((name) => lstatSync(join2(root, name)).isDirectory()).sort();
|
|
174
|
+
if (topLevelDirectories.length !== skillSet.size || topLevelDirectories.some((name) => !skillSet.has(name))) {
|
|
175
|
+
throw new Error("BMAD pack directory inventory differs from authenticated pack.toml skills");
|
|
176
|
+
}
|
|
177
|
+
for (const name of metadata.skillNames) {
|
|
178
|
+
const skillMd = join2(root, name, "SKILL.md");
|
|
179
|
+
if (!lstatSync(skillMd).isFile() || lstatSync(skillMd).isSymbolicLink()) {
|
|
180
|
+
throw new Error(`BMAD skill is missing a regular SKILL.md: ${name}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const payloadCount = [...actual.keys()].filter((path) => skillSet.has(path.split("/", 1)[0])).length;
|
|
184
|
+
if (payloadCount !== metadata.payloadFiles) {
|
|
185
|
+
throw new Error(`BMAD payload inventory mismatch: ${payloadCount} != ${metadata.payloadFiles}`);
|
|
186
|
+
}
|
|
187
|
+
return { root, skillNames: metadata.skillNames, payloadFiles: metadata.payloadFiles };
|
|
188
|
+
}
|
|
189
|
+
|
|
60
190
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
61
191
|
function resolveTemplateConfigPath() {
|
|
62
192
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
63
193
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
64
194
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
65
|
-
const base = xdg && xdg.length ? xdg :
|
|
66
|
-
return
|
|
195
|
+
const base = xdg && xdg.length ? xdg : join3(homedir(), ".config");
|
|
196
|
+
return join3(base, "hermes-agent-template", "config.toml");
|
|
67
197
|
}
|
|
68
198
|
function detectHermesBin(home) {
|
|
69
199
|
const candidates = [
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
200
|
+
join3(home, "code", "hermes-agent", "venv", "bin", "hermes"),
|
|
201
|
+
join3(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
|
|
202
|
+
join3(home, ".local", "bin", "hermes")
|
|
73
203
|
];
|
|
74
204
|
for (const c of candidates) {
|
|
75
205
|
if (existsSync2(c)) return c;
|
|
@@ -79,11 +209,11 @@ function detectHermesBin(home) {
|
|
|
79
209
|
function renderHostConfig() {
|
|
80
210
|
const home = homedir();
|
|
81
211
|
const hermesBin = detectHermesBin(home);
|
|
82
|
-
const hermesRepo =
|
|
83
|
-
const scaffoldDir =
|
|
84
|
-
const skillsDir =
|
|
85
|
-
const pmExternalSkillGlobalDir =
|
|
86
|
-
const pmExternalSkillBmadDir =
|
|
212
|
+
const hermesRepo = join3(home, "code", "hermes-agent");
|
|
213
|
+
const scaffoldDir = join3(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
214
|
+
const skillsDir = join3(home, ".agents", "skills");
|
|
215
|
+
const pmExternalSkillGlobalDir = join3(home, "code", "skillex", "skill-sets", "global", ".system");
|
|
216
|
+
const pmExternalSkillBmadDir = join3(home, "code", "skillex", "packs", "bmad", BMAD_PACK_VERSION);
|
|
87
217
|
return `# hermes-agent-template \u2014 host configuration
|
|
88
218
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
89
219
|
#
|
|
@@ -398,7 +528,7 @@ if __name__ == "__main__":
|
|
|
398
528
|
|
|
399
529
|
// src/commands/AddMiseCodegraphScript.ts
|
|
400
530
|
import { chmodSync } from "fs";
|
|
401
|
-
import { join as
|
|
531
|
+
import { join as join4 } from "path";
|
|
402
532
|
var AddMiseCodegraphScript = class extends Command {
|
|
403
533
|
async invoke() {
|
|
404
534
|
const filePath = ".mise/scripts/codegraph.sh";
|
|
@@ -452,7 +582,7 @@ fi
|
|
|
452
582
|
`;
|
|
453
583
|
this.writeFile(filePath, content);
|
|
454
584
|
if (!this.context.dryRun) {
|
|
455
|
-
chmodSync(
|
|
585
|
+
chmodSync(join4(this.context.targetDir, filePath), 493);
|
|
456
586
|
}
|
|
457
587
|
return {
|
|
458
588
|
success: true,
|
|
@@ -464,7 +594,7 @@ fi
|
|
|
464
594
|
|
|
465
595
|
// src/commands/AddMiseCodegraphWireScript.ts
|
|
466
596
|
import { chmodSync as chmodSync2 } from "fs";
|
|
467
|
-
import { join as
|
|
597
|
+
import { join as join5 } from "path";
|
|
468
598
|
var AddMiseCodegraphWireScript = class extends Command {
|
|
469
599
|
async invoke() {
|
|
470
600
|
const filePath = ".mise/scripts/codegraph-wire.sh";
|
|
@@ -540,7 +670,7 @@ inject_sse "$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/set
|
|
|
540
670
|
`;
|
|
541
671
|
this.writeFile(filePath, content);
|
|
542
672
|
if (!this.context.dryRun) {
|
|
543
|
-
chmodSync2(
|
|
673
|
+
chmodSync2(join5(this.context.targetDir, filePath), 493);
|
|
544
674
|
}
|
|
545
675
|
return {
|
|
546
676
|
success: true,
|
|
@@ -551,14 +681,14 @@ inject_sse "$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/set
|
|
|
551
681
|
};
|
|
552
682
|
|
|
553
683
|
// src/commands/WireMiseOpInject.ts
|
|
554
|
-
import { join as
|
|
555
|
-
import { existsSync as existsSync3, readFileSync, writeFileSync as writeFileSync3, chmodSync as chmodSync3 } from "node:fs";
|
|
684
|
+
import { join as join6 } from "node:path";
|
|
685
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3, chmodSync as chmodSync3 } from "node:fs";
|
|
556
686
|
var WireMiseOpInject = class _WireMiseOpInject extends Command {
|
|
557
687
|
static MARKER = "# pjangler:op-inject";
|
|
558
688
|
static CR = "{{config_root}}";
|
|
559
689
|
async invoke() {
|
|
560
690
|
const scriptPath = ".mise/scripts/inject-op-secrets.sh";
|
|
561
|
-
const fullScriptPath =
|
|
691
|
+
const fullScriptPath = join6(this.context.targetDir, scriptPath);
|
|
562
692
|
const scriptContent = `#!/usr/bin/env bash
|
|
563
693
|
# Resolve 1Password \`op://\` references in .env.op into .env.secrets, which mise
|
|
564
694
|
# loads via \`_.file\`.
|
|
@@ -602,14 +732,14 @@ fi
|
|
|
602
732
|
if (!this.context.dryRun) {
|
|
603
733
|
chmodSync3(fullScriptPath, 493);
|
|
604
734
|
}
|
|
605
|
-
const misePath =
|
|
735
|
+
const misePath = join6(this.context.targetDir, "mise.toml");
|
|
606
736
|
if (!existsSync3(misePath)) {
|
|
607
737
|
return {
|
|
608
738
|
success: false,
|
|
609
739
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj init mise` first, then re-run."
|
|
610
740
|
};
|
|
611
741
|
}
|
|
612
|
-
let content =
|
|
742
|
+
let content = readFileSync2(misePath, "utf8");
|
|
613
743
|
if (content.includes(_WireMiseOpInject.MARKER)) {
|
|
614
744
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for op-inject") };
|
|
615
745
|
}
|
|
@@ -646,8 +776,8 @@ ${envBlock}`);
|
|
|
646
776
|
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
647
777
|
if (enterRe.test(content)) {
|
|
648
778
|
content = content.replace(enterRe, (_m, head, close) => {
|
|
649
|
-
const
|
|
650
|
-
return `${head}${
|
|
779
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
780
|
+
return `${head}${sep2}
|
|
651
781
|
${enterAdds}${close}`;
|
|
652
782
|
});
|
|
653
783
|
} else {
|
|
@@ -915,12 +1045,12 @@ var NodeRecipe = class extends Recipe {
|
|
|
915
1045
|
};
|
|
916
1046
|
|
|
917
1047
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
918
|
-
import { basename, join as
|
|
919
|
-
import { readFileSync as
|
|
1048
|
+
import { basename as basename2, join as join7 } from "node:path";
|
|
1049
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
920
1050
|
import * as p from "@clack/prompts";
|
|
921
1051
|
function detectTicketProvider(targetDir) {
|
|
922
1052
|
try {
|
|
923
|
-
const t = JSON.parse(
|
|
1053
|
+
const t = JSON.parse(readFileSync3(join7(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
924
1054
|
return t === "plane" || t === "trello" ? t : void 0;
|
|
925
1055
|
} catch {
|
|
926
1056
|
return void 0;
|
|
@@ -929,7 +1059,7 @@ function detectTicketProvider(targetDir) {
|
|
|
929
1059
|
var PromptForAgentConfig = class extends Command {
|
|
930
1060
|
async invoke() {
|
|
931
1061
|
const ctx = this.context;
|
|
932
|
-
const defaultRepo =
|
|
1062
|
+
const defaultRepo = basename2(ctx.targetDir).toLowerCase();
|
|
933
1063
|
ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
|
|
934
1064
|
ctx.role ??= "pm";
|
|
935
1065
|
ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
|
|
@@ -978,7 +1108,7 @@ var PromptForAgentConfig = class extends Command {
|
|
|
978
1108
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
979
1109
|
import { spawnSync } from "node:child_process";
|
|
980
1110
|
import { homedir as homedir2 } from "node:os";
|
|
981
|
-
import { join as
|
|
1111
|
+
import { join as join8, dirname as dirname3 } from "node:path";
|
|
982
1112
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
983
1113
|
import { fileURLToPath } from "node:url";
|
|
984
1114
|
import * as p2 from "@clack/prompts";
|
|
@@ -990,8 +1120,8 @@ function resolveVendoredTemplate(name) {
|
|
|
990
1120
|
return void 0;
|
|
991
1121
|
}
|
|
992
1122
|
for (let i = 0; i < 8; i++) {
|
|
993
|
-
const candidate =
|
|
994
|
-
if (existsSync4(
|
|
1123
|
+
const candidate = join8(dir, "templates", name);
|
|
1124
|
+
if (existsSync4(join8(candidate, "copier.yml"))) return candidate;
|
|
995
1125
|
const parent = dirname3(dir);
|
|
996
1126
|
if (parent === dir) break;
|
|
997
1127
|
dir = parent;
|
|
@@ -1010,7 +1140,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
1010
1140
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
1011
1141
|
};
|
|
1012
1142
|
}
|
|
1013
|
-
const roleDir =
|
|
1143
|
+
const roleDir = join8(ctx.targetDir, "agents", "hermes", role);
|
|
1014
1144
|
ctx.roleDir = roleDir;
|
|
1015
1145
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
1016
1146
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -1020,7 +1150,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
1020
1150
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
1021
1151
|
};
|
|
1022
1152
|
}
|
|
1023
|
-
if (existsSync4(
|
|
1153
|
+
if (existsSync4(join8(roleDir, "role.yaml")) && !ctx.force) {
|
|
1024
1154
|
if (ctx.yes) {
|
|
1025
1155
|
ctx.force = true;
|
|
1026
1156
|
} else {
|
|
@@ -1041,15 +1171,16 @@ var RunCopierTemplate = class extends Command {
|
|
|
1041
1171
|
...process.env,
|
|
1042
1172
|
SKIP_TELEGRAM: "1",
|
|
1043
1173
|
SKIP_EMAIL: "1",
|
|
1044
|
-
//
|
|
1174
|
+
// Bloodbank is a fleet-shared Hermes gateway. Never provision the legacy
|
|
1175
|
+
// per-profile file consumer, even when an older template still exposes it.
|
|
1045
1176
|
SKIP_RUNTIME_REPO: ctx.skipRuntimeRepo ? "1" : "0",
|
|
1046
1177
|
SKIP_PLANE: ctx.skipPlane ? "1" : "0",
|
|
1047
|
-
SKIP_BLOODBANK:
|
|
1178
|
+
SKIP_BLOODBANK: "1",
|
|
1048
1179
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
1049
1180
|
};
|
|
1050
|
-
const LOCAL_TEMPLATE =
|
|
1181
|
+
const LOCAL_TEMPLATE = join8(homedir2(), "code", "hermes-agent-template");
|
|
1051
1182
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
1052
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(
|
|
1183
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join8(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
1053
1184
|
const args = [
|
|
1054
1185
|
"copy",
|
|
1055
1186
|
templateSrc,
|
|
@@ -1080,7 +1211,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
1080
1211
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
1081
1212
|
};
|
|
1082
1213
|
}
|
|
1083
|
-
mkdirSync3(
|
|
1214
|
+
mkdirSync3(join8(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
1084
1215
|
const spinner4 = p2.spinner();
|
|
1085
1216
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
1086
1217
|
const result = spawnSync("copier", args, {
|
|
@@ -1104,20 +1235,26 @@ var RunCopierTemplate = class extends Command {
|
|
|
1104
1235
|
};
|
|
1105
1236
|
|
|
1106
1237
|
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
1107
|
-
import { existsSync as existsSync5, readFileSync as
|
|
1108
|
-
import { join as
|
|
1238
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
1239
|
+
import { join as join9 } from "path";
|
|
1109
1240
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1241
|
+
function sectionHasPath(section, targetPath) {
|
|
1242
|
+
return section.split(/\r?\n/).some((line) => /^\s*path\s*=/.test(line) && line.replace(/^\s*path\s*=\s*/, "").trim() === targetPath);
|
|
1243
|
+
}
|
|
1244
|
+
function removeSubmodulePath(content, targetPath) {
|
|
1245
|
+
return content.replace(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm, (section) => sectionHasPath(section, targetPath) ? "" : section).replace(/\n{3,}/g, "\n\n").trim();
|
|
1246
|
+
}
|
|
1110
1247
|
var UntrackHermesRuntimes = class extends Command {
|
|
1111
1248
|
async invoke() {
|
|
1112
1249
|
const targetDir = this.context.targetDir;
|
|
1113
|
-
const rolesDir =
|
|
1250
|
+
const rolesDir = join9(targetDir, "agents", "hermes");
|
|
1114
1251
|
if (!existsSync5(rolesDir)) {
|
|
1115
1252
|
return {
|
|
1116
1253
|
success: true,
|
|
1117
1254
|
message: "No Hermes agents found (no agents/hermes directory)."
|
|
1118
1255
|
};
|
|
1119
1256
|
}
|
|
1120
|
-
const roles =
|
|
1257
|
+
const roles = readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
1121
1258
|
if (roles.length === 0) {
|
|
1122
1259
|
return {
|
|
1123
1260
|
success: true,
|
|
@@ -1127,47 +1264,79 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
1127
1264
|
let modifiedAny = false;
|
|
1128
1265
|
const details = [];
|
|
1129
1266
|
for (const role of roles) {
|
|
1130
|
-
const roleDir =
|
|
1131
|
-
const runtimePath =
|
|
1132
|
-
const gitignorePath =
|
|
1267
|
+
const roleDir = join9("agents", "hermes", role);
|
|
1268
|
+
const runtimePath = join9(roleDir, "runtime");
|
|
1269
|
+
const gitignorePath = join9(roleDir, ".gitignore");
|
|
1270
|
+
const gitmodulesPath = join9(targetDir, ".gitmodules");
|
|
1133
1271
|
let isTracked = false;
|
|
1134
|
-
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
1272
|
+
const lsResult = spawnSync2("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
1135
1273
|
cwd: targetDir,
|
|
1136
1274
|
encoding: "utf8"
|
|
1137
1275
|
});
|
|
1276
|
+
if (lsResult.status !== 0) {
|
|
1277
|
+
return {
|
|
1278
|
+
success: false,
|
|
1279
|
+
message: `\u2717 Failed to inspect runtime index at ${runtimePath}: ${lsResult.stderr.trim() || `exit ${lsResult.status}`}`
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1138
1282
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
1139
1283
|
isTracked = true;
|
|
1140
1284
|
}
|
|
1285
|
+
let hasStaleMapping = false;
|
|
1286
|
+
let gitmodulesContent = "";
|
|
1287
|
+
if (existsSync5(gitmodulesPath)) {
|
|
1288
|
+
gitmodulesContent = readFileSync4(gitmodulesPath, "utf8");
|
|
1289
|
+
const sections = gitmodulesContent.match(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm) ?? [];
|
|
1290
|
+
hasStaleMapping = sections.some((section) => sectionHasPath(section, runtimePath));
|
|
1291
|
+
}
|
|
1141
1292
|
let isIgnored = false;
|
|
1142
|
-
const fullGitignorePath =
|
|
1293
|
+
const fullGitignorePath = join9(targetDir, gitignorePath);
|
|
1143
1294
|
if (existsSync5(fullGitignorePath)) {
|
|
1144
|
-
const content =
|
|
1295
|
+
const content = readFileSync4(fullGitignorePath, "utf8");
|
|
1145
1296
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
1146
1297
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
1147
1298
|
}
|
|
1148
|
-
if (isTracked || !isIgnored) {
|
|
1299
|
+
if (isTracked || hasStaleMapping || !isIgnored) {
|
|
1149
1300
|
modifiedAny = true;
|
|
1150
1301
|
if (isTracked) {
|
|
1151
1302
|
details.push(`untrack agents/hermes/${role}/runtime`);
|
|
1152
1303
|
if (!this.context.dryRun) {
|
|
1153
|
-
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
|
|
1304
|
+
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
|
|
1154
1305
|
cwd: targetDir,
|
|
1155
1306
|
encoding: "utf8"
|
|
1156
1307
|
});
|
|
1157
1308
|
if (rmResult.status !== 0) {
|
|
1158
1309
|
return {
|
|
1159
1310
|
success: false,
|
|
1160
|
-
message:
|
|
1311
|
+
message: `\u2717 Failed to untrack ${runtimePath}: ${rmResult.stderr.trim() || `exit ${rmResult.status}`}`
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
const verifyResult = spawnSync2("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
1315
|
+
cwd: targetDir,
|
|
1316
|
+
encoding: "utf8"
|
|
1317
|
+
});
|
|
1318
|
+
if (verifyResult.status !== 0 || verifyResult.stdout.trim()) {
|
|
1319
|
+
return {
|
|
1320
|
+
success: false,
|
|
1321
|
+
message: verifyResult.status !== 0 ? `\u2717 Failed to verify untracked runtime ${runtimePath}: ${verifyResult.stderr.trim() || `exit ${verifyResult.status}`}` : `\u2717 Runtime remains tracked after index-only removal: ${runtimePath}`
|
|
1161
1322
|
};
|
|
1162
1323
|
}
|
|
1163
1324
|
}
|
|
1164
1325
|
}
|
|
1326
|
+
if (hasStaleMapping) {
|
|
1327
|
+
details.push(`remove stale .gitmodules mapping for ${runtimePath}`);
|
|
1328
|
+
if (!this.context.dryRun) {
|
|
1329
|
+
const next = removeSubmodulePath(gitmodulesContent, runtimePath);
|
|
1330
|
+
writeFileSync4(gitmodulesPath, next ? `${next}
|
|
1331
|
+
` : "", "utf8");
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1165
1334
|
if (!isIgnored) {
|
|
1166
1335
|
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
1167
1336
|
if (!this.context.dryRun) {
|
|
1168
1337
|
let content = "";
|
|
1169
1338
|
if (existsSync5(fullGitignorePath)) {
|
|
1170
|
-
content =
|
|
1339
|
+
content = readFileSync4(fullGitignorePath, "utf8");
|
|
1171
1340
|
}
|
|
1172
1341
|
if (content && !content.endsWith("\n")) {
|
|
1173
1342
|
content += "\n";
|
|
@@ -1195,7 +1364,7 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
|
|
|
1195
1364
|
|
|
1196
1365
|
// src/commands/hermes/WireTelegram.ts
|
|
1197
1366
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1198
|
-
import { join as
|
|
1367
|
+
import { join as join10 } from "node:path";
|
|
1199
1368
|
import { existsSync as existsSync6, unlinkSync } from "node:fs";
|
|
1200
1369
|
import * as p3 from "@clack/prompts";
|
|
1201
1370
|
var WireTelegram = class extends Command {
|
|
@@ -1284,14 +1453,14 @@ var WireTelegram = class extends Command {
|
|
|
1284
1453
|
if (p3.isCancel(allowedAnswer)) {
|
|
1285
1454
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1286
1455
|
}
|
|
1287
|
-
const script =
|
|
1456
|
+
const script = join10(roleDir, ".scripts", "30-telegram.sh");
|
|
1288
1457
|
if (!existsSync6(script)) {
|
|
1289
1458
|
return {
|
|
1290
1459
|
success: false,
|
|
1291
1460
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1292
1461
|
};
|
|
1293
1462
|
}
|
|
1294
|
-
const marker =
|
|
1463
|
+
const marker = join10(roleDir, ".scripts", ".done-30-telegram");
|
|
1295
1464
|
if (existsSync6(marker)) unlinkSync(marker);
|
|
1296
1465
|
const spinner4 = p3.spinner();
|
|
1297
1466
|
spinner4.start("Verifying token + wiring profile");
|
|
@@ -1319,7 +1488,7 @@ function cap(s) {
|
|
|
1319
1488
|
|
|
1320
1489
|
// src/commands/hermes/WireEmail.ts
|
|
1321
1490
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1322
|
-
import { join as
|
|
1491
|
+
import { join as join11 } from "node:path";
|
|
1323
1492
|
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1324
1493
|
import * as p4 from "@clack/prompts";
|
|
1325
1494
|
var WireEmail = class extends Command {
|
|
@@ -1335,7 +1504,7 @@ var WireEmail = class extends Command {
|
|
|
1335
1504
|
if (!targetRepo || !role || !roleDir) {
|
|
1336
1505
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1337
1506
|
}
|
|
1338
|
-
const script =
|
|
1507
|
+
const script = join11(roleDir, ".scripts", "50-email.sh");
|
|
1339
1508
|
if (!existsSync7(script)) {
|
|
1340
1509
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1341
1510
|
}
|
|
@@ -1398,7 +1567,7 @@ var WireEmail = class extends Command {
|
|
|
1398
1567
|
}
|
|
1399
1568
|
}
|
|
1400
1569
|
}
|
|
1401
|
-
const marker =
|
|
1570
|
+
const marker = join11(roleDir, ".scripts", ".done-50-email");
|
|
1402
1571
|
if (existsSync7(marker)) unlinkSync2(marker);
|
|
1403
1572
|
const spinner4 = p4.spinner();
|
|
1404
1573
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
@@ -1427,7 +1596,6 @@ var PrintHermesSummary = class extends Command {
|
|
|
1427
1596
|
const botHandle = `${targetRepo?.toLowerCase().replace(/-/g, "_")}_${role?.toLowerCase()}_bot`;
|
|
1428
1597
|
const email = `${targetRepo}-${role}@delo.sh`;
|
|
1429
1598
|
const gw = `hermes-${agentId}-gateway.service`;
|
|
1430
|
-
const csm = `hermes-${agentId}-consumer.service`;
|
|
1431
1599
|
const hb = `hermes-${agentId}-heartbeat.timer`;
|
|
1432
1600
|
const lines = [];
|
|
1433
1601
|
lines.push(`agent_id ${agentId}`);
|
|
@@ -1437,13 +1605,13 @@ var PrintHermesSummary = class extends Command {
|
|
|
1437
1605
|
if (!skipEmail) lines.push(`email ${email}`);
|
|
1438
1606
|
lines.push("");
|
|
1439
1607
|
lines.push("Start daemons:");
|
|
1440
|
-
lines.push(` systemctl --user start ${csm}`);
|
|
1441
1608
|
lines.push(` systemctl --user start ${hb}`);
|
|
1442
1609
|
if (!skipTelegram) {
|
|
1443
1610
|
lines.push(` systemctl --user start ${gw}`);
|
|
1444
1611
|
} else {
|
|
1445
1612
|
lines.push(` # gateway needs Telegram wired first (re-run with --skip-telegram=0)`);
|
|
1446
1613
|
}
|
|
1614
|
+
lines.push(" # Bloodbank commands arrive through the fleet-shared Hermes gateway");
|
|
1447
1615
|
lines.push("");
|
|
1448
1616
|
lines.push("Talk locally:");
|
|
1449
1617
|
lines.push(` ${ctx.roleDir}/hermes chat "status"`);
|
|
@@ -1486,15 +1654,15 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1486
1654
|
|
|
1487
1655
|
// src/commands/AgentHooksCommands.ts
|
|
1488
1656
|
import { homedir as homedir4 } from "node:os";
|
|
1489
|
-
import { join as
|
|
1490
|
-
import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as
|
|
1657
|
+
import { join as join13, dirname as dirname5 } from "node:path";
|
|
1658
|
+
import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1491
1659
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1492
1660
|
|
|
1493
1661
|
// src/project/index.ts
|
|
1494
1662
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1495
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as
|
|
1663
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1496
1664
|
import { homedir as homedir3 } from "node:os";
|
|
1497
|
-
import { basename as
|
|
1665
|
+
import { basename as basename3, delimiter, dirname as dirname4, join as join12, resolve as resolve2 } from "node:path";
|
|
1498
1666
|
import YAML2 from "yaml";
|
|
1499
1667
|
|
|
1500
1668
|
// src/project/RegistryStore.ts
|
|
@@ -1729,18 +1897,18 @@ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
|
1729
1897
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1730
1898
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1731
1899
|
"/home/delorenj/code/skillex/all-skills",
|
|
1732
|
-
|
|
1733
|
-
|
|
1900
|
+
join12(homedir3(), ".agents", "skills"),
|
|
1901
|
+
join12(homedir3(), ".codex", "skills")
|
|
1734
1902
|
];
|
|
1735
1903
|
function projectRegistryPath(env2 = process.env) {
|
|
1736
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1904
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join12(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1737
1905
|
}
|
|
1738
1906
|
function emptyProjectRegistry() {
|
|
1739
1907
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1740
1908
|
}
|
|
1741
1909
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1742
1910
|
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
1743
|
-
const raw = YAML2.parse(
|
|
1911
|
+
const raw = YAML2.parse(readFileSync5(path, "utf8"));
|
|
1744
1912
|
if (raw == null) return emptyProjectRegistry();
|
|
1745
1913
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1746
1914
|
const registry = raw;
|
|
@@ -1770,7 +1938,7 @@ function validateProjectRegistry(registry) {
|
|
|
1770
1938
|
validateProjectRecord(project, slug);
|
|
1771
1939
|
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
1772
1940
|
slugs.add(project.slug);
|
|
1773
|
-
const repoKey =
|
|
1941
|
+
const repoKey = resolve2(project.repo_path);
|
|
1774
1942
|
const existingRepoSlug = repoPaths.get(repoKey);
|
|
1775
1943
|
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
1776
1944
|
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
@@ -1837,7 +2005,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1837
2005
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1838
2006
|
if (override === "0" || override === "false") return false;
|
|
1839
2007
|
if (override === "1" || override === "true") return true;
|
|
1840
|
-
return !existsSync8(
|
|
2008
|
+
return !existsSync8(join12(homedir3(), ".agents", "hooks"));
|
|
1841
2009
|
}
|
|
1842
2010
|
function jsonStable(value) {
|
|
1843
2011
|
return JSON.stringify(value);
|
|
@@ -1850,14 +2018,14 @@ function projectRecordEquivalent(a, b) {
|
|
|
1850
2018
|
}
|
|
1851
2019
|
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
1852
2020
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1853
|
-
return
|
|
2021
|
+
return resolve2(dirname4(resolve2(cwd)), compactName);
|
|
1854
2022
|
}
|
|
1855
2023
|
function sourceSkillRoots(env2 = process.env) {
|
|
1856
2024
|
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
|
|
1857
2025
|
const seen = /* @__PURE__ */ new Set();
|
|
1858
2026
|
const roots = [];
|
|
1859
2027
|
for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
|
|
1860
|
-
const normalized =
|
|
2028
|
+
const normalized = resolve2(expandHome(root));
|
|
1861
2029
|
if (seen.has(normalized)) continue;
|
|
1862
2030
|
seen.add(normalized);
|
|
1863
2031
|
roots.push(normalized);
|
|
@@ -1867,12 +2035,12 @@ function sourceSkillRoots(env2 = process.env) {
|
|
|
1867
2035
|
function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
1868
2036
|
if (!sourceSkill) return void 0;
|
|
1869
2037
|
const expanded = expandHome(sourceSkill);
|
|
1870
|
-
const direct =
|
|
2038
|
+
const direct = resolve2(expanded);
|
|
1871
2039
|
if (existsSync8(direct)) return direct;
|
|
1872
|
-
const name =
|
|
2040
|
+
const name = basename3(sourceSkill);
|
|
1873
2041
|
const roots = sourceSkillRoots(env2);
|
|
1874
2042
|
for (const root of roots) {
|
|
1875
|
-
const candidate =
|
|
2043
|
+
const candidate = join12(root, name);
|
|
1876
2044
|
if (existsSync8(candidate)) return candidate;
|
|
1877
2045
|
}
|
|
1878
2046
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
@@ -1881,11 +2049,11 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1881
2049
|
}
|
|
1882
2050
|
function planProjectInit(input) {
|
|
1883
2051
|
if (!input.name.trim()) throw new Error("Project name is required");
|
|
1884
|
-
const registryPath2 =
|
|
2052
|
+
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
1885
2053
|
const registry = loadProjectRegistry(registryPath2);
|
|
1886
2054
|
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1887
2055
|
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
1888
|
-
const targetDir =
|
|
2056
|
+
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
1889
2057
|
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
1890
2058
|
const existing = registry.projects[slug];
|
|
1891
2059
|
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
@@ -1928,7 +2096,7 @@ function planProjectInit(input) {
|
|
|
1928
2096
|
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
1929
2097
|
};
|
|
1930
2098
|
validateNoDuplicateProject(registry, project, overwrite);
|
|
1931
|
-
const pjanglerRoot =
|
|
2099
|
+
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot());
|
|
1932
2100
|
const manifest = projectManifestFromRegistryProject(project);
|
|
1933
2101
|
const apply = input.apply ?? false;
|
|
1934
2102
|
const live = input.live ?? false;
|
|
@@ -1954,7 +2122,7 @@ function planProjectInit(input) {
|
|
|
1954
2122
|
}));
|
|
1955
2123
|
}
|
|
1956
2124
|
actions.push(
|
|
1957
|
-
{ kind: "project.write-manifest", path:
|
|
2125
|
+
{ kind: "project.write-manifest", path: join12(targetDir, ".project.json"), manifest },
|
|
1958
2126
|
{
|
|
1959
2127
|
kind: "ticket-provider.create-or-link",
|
|
1960
2128
|
enabled: live,
|
|
@@ -2015,7 +2183,7 @@ async function executeProjectInitPlan(plan) {
|
|
|
2015
2183
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
2016
2184
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
2017
2185
|
`;
|
|
2018
|
-
const current = existsSync8(action.path) ?
|
|
2186
|
+
const current = existsSync8(action.path) ? readFileSync5(action.path, "utf8") : void 0;
|
|
2019
2187
|
if (current !== next) {
|
|
2020
2188
|
writeFileSync5(action.path, next, "utf8");
|
|
2021
2189
|
changedFiles.push(action.path);
|
|
@@ -2125,7 +2293,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
2125
2293
|
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
2126
2294
|
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
2127
2295
|
} else {
|
|
2128
|
-
const manifestPath =
|
|
2296
|
+
const manifestPath = join12(project.repo_path, ".project.json");
|
|
2129
2297
|
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
2130
2298
|
}
|
|
2131
2299
|
for (const artifact of project.source_artifacts) {
|
|
@@ -2142,7 +2310,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
2142
2310
|
};
|
|
2143
2311
|
}
|
|
2144
2312
|
function buildCommonProjectCopierAction(input) {
|
|
2145
|
-
const templateDir =
|
|
2313
|
+
const templateDir = join12(input.pjanglerRoot, "templates", "commonproject");
|
|
2146
2314
|
const data = {
|
|
2147
2315
|
project_name: input.projectName,
|
|
2148
2316
|
project_description: input.projectDescription ?? "",
|
|
@@ -2171,19 +2339,19 @@ function buildCommonProjectCopierAction(input) {
|
|
|
2171
2339
|
function resolvePjanglerRoot() {
|
|
2172
2340
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
2173
2341
|
while (dir !== dirname4(dir)) {
|
|
2174
|
-
if (existsSync8(
|
|
2342
|
+
if (existsSync8(join12(dir, "package.json")) && existsSync8(join12(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
2175
2343
|
dir = dirname4(dir);
|
|
2176
2344
|
}
|
|
2177
|
-
return
|
|
2345
|
+
return resolve2(process.cwd());
|
|
2178
2346
|
}
|
|
2179
2347
|
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
2180
2348
|
const existingSameSlug = registry.projects[project.slug];
|
|
2181
|
-
if (existingSameSlug && !overwrite &&
|
|
2349
|
+
if (existingSameSlug && !overwrite && resolve2(existingSameSlug.repo_path) !== resolve2(project.repo_path)) {
|
|
2182
2350
|
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
2183
2351
|
}
|
|
2184
2352
|
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
2185
2353
|
if (slug === project.slug) continue;
|
|
2186
|
-
if (
|
|
2354
|
+
if (resolve2(existing.repo_path) === resolve2(project.repo_path)) {
|
|
2187
2355
|
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
2188
2356
|
}
|
|
2189
2357
|
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
@@ -2203,7 +2371,7 @@ function validateProjectRecord(project, key) {
|
|
|
2203
2371
|
}
|
|
2204
2372
|
function expandHome(path) {
|
|
2205
2373
|
if (path === "~") return homedir3();
|
|
2206
|
-
if (path.startsWith("~/")) return
|
|
2374
|
+
if (path.startsWith("~/")) return join12(homedir3(), path.slice(2));
|
|
2207
2375
|
return path;
|
|
2208
2376
|
}
|
|
2209
2377
|
function isRecord(value) {
|
|
@@ -2220,16 +2388,16 @@ function resolveTemplateRoot() {
|
|
|
2220
2388
|
try {
|
|
2221
2389
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
2222
2390
|
for (let i = 0; i < 8; i++) {
|
|
2223
|
-
candidates.push(
|
|
2391
|
+
candidates.push(join13(dir, "templates", "commonproject", "template"));
|
|
2224
2392
|
const parent = dirname5(dir);
|
|
2225
2393
|
if (parent === dir) break;
|
|
2226
2394
|
dir = parent;
|
|
2227
2395
|
}
|
|
2228
2396
|
} catch {
|
|
2229
2397
|
}
|
|
2230
|
-
candidates.push(
|
|
2398
|
+
candidates.push(join13(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
2231
2399
|
for (const c of candidates) {
|
|
2232
|
-
if (existsSync9(
|
|
2400
|
+
if (existsSync9(join13(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
2233
2401
|
}
|
|
2234
2402
|
throw new Error(
|
|
2235
2403
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -2255,8 +2423,8 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
2255
2423
|
const created = [];
|
|
2256
2424
|
const skipped = [];
|
|
2257
2425
|
for (const { rel, dir } of items) {
|
|
2258
|
-
const src =
|
|
2259
|
-
const dest =
|
|
2426
|
+
const src = join13(templateRoot, rel);
|
|
2427
|
+
const dest = join13(this.context.targetDir, rel);
|
|
2260
2428
|
if (!existsSync9(src)) continue;
|
|
2261
2429
|
if (existsSync9(dest) && !this.context.force) {
|
|
2262
2430
|
skipped.push(rel);
|
|
@@ -2284,14 +2452,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2284
2452
|
if (!resolveAgentHooksLayer()) {
|
|
2285
2453
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
2286
2454
|
}
|
|
2287
|
-
const misePath =
|
|
2455
|
+
const misePath = join13(this.context.targetDir, "mise.toml");
|
|
2288
2456
|
if (!existsSync9(misePath)) {
|
|
2289
2457
|
return {
|
|
2290
2458
|
success: false,
|
|
2291
2459
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
2292
2460
|
};
|
|
2293
2461
|
}
|
|
2294
|
-
let content =
|
|
2462
|
+
let content = readFileSync6(misePath, "utf8");
|
|
2295
2463
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
2296
2464
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
2297
2465
|
}
|
|
@@ -2306,15 +2474,15 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2306
2474
|
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
2307
2475
|
if (enterRe.test(content)) {
|
|
2308
2476
|
content = content.replace(enterRe, (_m, head, close) => {
|
|
2309
|
-
const
|
|
2310
|
-
return `${head}${
|
|
2477
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2478
|
+
return `${head}${sep2}
|
|
2311
2479
|
${enterAdds}${close}`;
|
|
2312
2480
|
});
|
|
2313
2481
|
const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
2314
2482
|
if (leaveRe.test(content)) {
|
|
2315
2483
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
2316
|
-
const
|
|
2317
|
-
return `${head}${
|
|
2484
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2485
|
+
return `${head}${sep2}
|
|
2318
2486
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
2319
2487
|
});
|
|
2320
2488
|
} else {
|
|
@@ -2558,24 +2726,41 @@ function createRecipe(name, context) {
|
|
|
2558
2726
|
import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
|
|
2559
2727
|
|
|
2560
2728
|
// src/parity/index.ts
|
|
2561
|
-
import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync6, readFileSync as
|
|
2562
|
-
import { basename as
|
|
2563
|
-
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2729
|
+
import { existsSync as existsSync10, lstatSync as lstatSync2, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, readlinkSync, readdirSync as readdirSync3, realpathSync, renameSync as renameSync2, rmdirSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync4, copyFileSync, rmSync } from "node:fs";
|
|
2730
|
+
import { basename as basename4, dirname as dirname6, join as join14, relative as relative2, resolve as resolve3 } from "node:path";
|
|
2731
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
2564
2732
|
import { homedir as homedir5 } from "node:os";
|
|
2565
2733
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2566
2734
|
import YAML3 from "yaml";
|
|
2567
2735
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2568
2736
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
2569
|
-
var
|
|
2737
|
+
var PROVISION_BMAD_SKILLS_SCRIPT = "python3 '{{config_root}}/.mise/scripts/provision-bmad-skills.py'";
|
|
2738
|
+
var SYNC_SKILLS_SCRIPT = "python3 '{{config_root}}/.mise/scripts/sync-skills.py' --scope project";
|
|
2570
2739
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
2571
2740
|
var SKILLS_REGISTRY_URL = "https://github.com/delorenj/skillex.git";
|
|
2741
|
+
var PROJECT_CLI_SKILL_DIRS = [
|
|
2742
|
+
".gemini/skills",
|
|
2743
|
+
".codex/skills",
|
|
2744
|
+
".kimi/skills",
|
|
2745
|
+
".augment/skills",
|
|
2746
|
+
".config/opencode/skills",
|
|
2747
|
+
".hermes/skills",
|
|
2748
|
+
".claude/skills",
|
|
2749
|
+
".openclaw/skills"
|
|
2750
|
+
];
|
|
2751
|
+
var CANONICAL_CLAUDE_SKILLS_ALIAS = "../.agents/skills";
|
|
2572
2752
|
var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
|
|
2573
2753
|
# agent files to the main AGENTS.md file.
|
|
2574
2754
|
#
|
|
2575
2755
|
# TODO: Ensure this works for all levels of nesting.
|
|
2576
2756
|
# i.e. All linked agent files MUST be siblings at
|
|
2577
2757
|
# any given level of nesting.`;
|
|
2578
|
-
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
2758
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
2759
|
+
LINK_AGENTFILES_SCRIPT,
|
|
2760
|
+
OP_INJECT_SCRIPT,
|
|
2761
|
+
PROVISION_BMAD_SKILLS_SCRIPT,
|
|
2762
|
+
SYNC_SKILLS_SCRIPT
|
|
2763
|
+
];
|
|
2579
2764
|
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
2580
2765
|
patterns = ["AGENTS.md"]
|
|
2581
2766
|
task = "link-agentfiles"
|
|
@@ -2590,7 +2775,12 @@ run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
|
2590
2775
|
|
|
2591
2776
|
[tasks.skills-sync]
|
|
2592
2777
|
description = "Sync skills from manifest to local CLI dirs"
|
|
2593
|
-
|
|
2778
|
+
depends = ["skills-provision-bmad"]
|
|
2779
|
+
run = "python3 '{{config_root}}/.mise/scripts/sync-skills.py' --scope project"
|
|
2780
|
+
|
|
2781
|
+
[tasks.skills-provision-bmad]
|
|
2782
|
+
description = "Provision pinned BMAD skills from the Skillex pack"
|
|
2783
|
+
run = "python3 '{{config_root}}/.mise/scripts/provision-bmad-skills.py'"`;
|
|
2594
2784
|
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
2595
2785
|
[tasks."version"]
|
|
2596
2786
|
description = "Print the current version (vX.Y.Z)"
|
|
@@ -2620,7 +2810,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
|
|
|
2620
2810
|
function resolvePjanglerRoot2() {
|
|
2621
2811
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2622
2812
|
while (dir !== dirname6(dir)) {
|
|
2623
|
-
if (existsSync10(
|
|
2813
|
+
if (existsSync10(join14(dir, "package.json")) && existsSync10(join14(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2624
2814
|
return dir;
|
|
2625
2815
|
}
|
|
2626
2816
|
dir = dirname6(dir);
|
|
@@ -2631,7 +2821,7 @@ function normalizeNewlines(value) {
|
|
|
2631
2821
|
return value.replace(/\r\n/g, "\n");
|
|
2632
2822
|
}
|
|
2633
2823
|
function readText(path) {
|
|
2634
|
-
return normalizeNewlines(
|
|
2824
|
+
return normalizeNewlines(readFileSync7(path, "utf8"));
|
|
2635
2825
|
}
|
|
2636
2826
|
function safeReadText(path) {
|
|
2637
2827
|
return existsSync10(path) ? readText(path) : null;
|
|
@@ -2667,7 +2857,7 @@ function readSymlinkTarget(path) {
|
|
|
2667
2857
|
}
|
|
2668
2858
|
function ensureSymlink(path, target, dryRun) {
|
|
2669
2859
|
if (existsSync10(path)) {
|
|
2670
|
-
const stat =
|
|
2860
|
+
const stat = lstatSync2(path);
|
|
2671
2861
|
if (stat.isSymbolicLink()) {
|
|
2672
2862
|
const current = readSymlinkTarget(path);
|
|
2673
2863
|
if (current === target) return { changed: false };
|
|
@@ -2677,18 +2867,18 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2677
2867
|
}
|
|
2678
2868
|
return { changed: true };
|
|
2679
2869
|
}
|
|
2680
|
-
return { changed: false, blocked: `${
|
|
2870
|
+
return { changed: false, blocked: `${relative2(process.cwd(), path) || path} exists and is not a symlink` };
|
|
2681
2871
|
}
|
|
2682
2872
|
if (!dryRun) symlinkSync(target, path);
|
|
2683
2873
|
return { changed: true };
|
|
2684
2874
|
}
|
|
2685
2875
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2686
|
-
const agentsPath =
|
|
2876
|
+
const agentsPath = join14(repoRoot, "AGENTS.md");
|
|
2687
2877
|
if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
|
|
2688
2878
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2689
|
-
const source =
|
|
2879
|
+
const source = join14(repoRoot, file);
|
|
2690
2880
|
if (!existsSync10(source)) continue;
|
|
2691
|
-
const stat =
|
|
2881
|
+
const stat = lstatSync2(source);
|
|
2692
2882
|
if (stat.isSymbolicLink()) continue;
|
|
2693
2883
|
if (stat.isFile()) {
|
|
2694
2884
|
if (!dryRun) renameSync2(source, agentsPath);
|
|
@@ -2696,9 +2886,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2696
2886
|
}
|
|
2697
2887
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2698
2888
|
}
|
|
2699
|
-
const readmePath =
|
|
2889
|
+
const readmePath = join14(repoRoot, "README.md");
|
|
2700
2890
|
if (existsSync10(readmePath)) {
|
|
2701
|
-
const stat =
|
|
2891
|
+
const stat = lstatSync2(readmePath);
|
|
2702
2892
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2703
2893
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
2704
2894
|
return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
|
|
@@ -2736,11 +2926,11 @@ function yamlGet(text3, keyPath) {
|
|
|
2736
2926
|
return "";
|
|
2737
2927
|
}
|
|
2738
2928
|
function discoverRoles(repoRoot) {
|
|
2739
|
-
const rolesDir =
|
|
2929
|
+
const rolesDir = join14(repoRoot, "agents", "hermes");
|
|
2740
2930
|
if (!existsSync10(rolesDir)) return [];
|
|
2741
|
-
return
|
|
2742
|
-
const roleDir =
|
|
2743
|
-
const roleYamlPath =
|
|
2931
|
+
return readdirSync3(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2932
|
+
const roleDir = join14(rolesDir, entry.name);
|
|
2933
|
+
const roleYamlPath = join14(roleDir, "role.yaml");
|
|
2744
2934
|
if (!existsSync10(roleYamlPath)) return null;
|
|
2745
2935
|
const text3 = readText(roleYamlPath);
|
|
2746
2936
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
@@ -2769,7 +2959,7 @@ function discoverRoles(repoRoot) {
|
|
|
2769
2959
|
}).filter((value) => Boolean(value));
|
|
2770
2960
|
}
|
|
2771
2961
|
function registryPath(homeDir) {
|
|
2772
|
-
return
|
|
2962
|
+
return join14(homeDir, ".hermes", "agents-registry.yaml");
|
|
2773
2963
|
}
|
|
2774
2964
|
function systemctlUser(args) {
|
|
2775
2965
|
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -2780,7 +2970,7 @@ function systemctlUser(args) {
|
|
|
2780
2970
|
};
|
|
2781
2971
|
}
|
|
2782
2972
|
function templateScript(ctx, name) {
|
|
2783
|
-
const source =
|
|
2973
|
+
const source = join14(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2784
2974
|
return existsSync10(source) ? readText(source) : void 0;
|
|
2785
2975
|
}
|
|
2786
2976
|
function templateVersioningScript(ctx) {
|
|
@@ -2793,8 +2983,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2793
2983
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2794
2984
|
if (override === "0" || override === "false") return false;
|
|
2795
2985
|
if (override === "1" || override === "true") return true;
|
|
2796
|
-
if (existsSync10(
|
|
2797
|
-
return !existsSync10(
|
|
2986
|
+
if (existsSync10(join14(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2987
|
+
return !existsSync10(join14(ctx.homeDir, ".agents", "hooks"));
|
|
2798
2988
|
}
|
|
2799
2989
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2800
2990
|
const out = [];
|
|
@@ -2820,13 +3010,13 @@ function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
|
2820
3010
|
}
|
|
2821
3011
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
2822
3012
|
const project = readProjectJson(ctx);
|
|
2823
|
-
const projectName = String(project?.project_name ??
|
|
3013
|
+
const projectName = String(project?.project_name ?? basename4(ctx.repoRoot) ?? "project");
|
|
2824
3014
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2825
3015
|
}
|
|
2826
3016
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2827
|
-
const targetPath =
|
|
3017
|
+
const targetPath = join14(ctx.repoRoot, "mise.toml");
|
|
2828
3018
|
if (existsSync10(targetPath)) return false;
|
|
2829
|
-
const sourcePath =
|
|
3019
|
+
const sourcePath = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2830
3020
|
if (!existsSync10(sourcePath)) return false;
|
|
2831
3021
|
changedFiles.push(targetPath);
|
|
2832
3022
|
if (!ctx.dryRun) {
|
|
@@ -2835,24 +3025,426 @@ function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
|
2835
3025
|
return true;
|
|
2836
3026
|
}
|
|
2837
3027
|
function templateCommonProjectText(ctx, rel) {
|
|
2838
|
-
const path =
|
|
3028
|
+
const path = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
2839
3029
|
return existsSync10(path) ? readText(path) : void 0;
|
|
2840
3030
|
}
|
|
2841
|
-
function
|
|
3031
|
+
function validateSkillName(name) {
|
|
3032
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\") || basename4(name) !== name) {
|
|
3033
|
+
throw new Error(`Unsafe skill name: ${JSON.stringify(name)}`);
|
|
3034
|
+
}
|
|
3035
|
+
return name;
|
|
3036
|
+
}
|
|
3037
|
+
function lstatIfPresent(path) {
|
|
3038
|
+
try {
|
|
3039
|
+
return lstatSync2(path);
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
if (error.code === "ENOENT") return void 0;
|
|
3042
|
+
throw error;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
function isContainedBy(root, target) {
|
|
3046
|
+
const rel = relative2(root, target);
|
|
3047
|
+
return rel === "" || rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\");
|
|
3048
|
+
}
|
|
3049
|
+
function prepareSafeProjectSkillsDirs(ctx) {
|
|
3050
|
+
const projectRoot = realpathSync(ctx.repoRoot);
|
|
3051
|
+
const agentsDir = join14(projectRoot, ".agents");
|
|
3052
|
+
const skillsDir = join14(agentsDir, "skills");
|
|
3053
|
+
for (const path of [agentsDir, skillsDir]) {
|
|
3054
|
+
if (!isContainedBy(projectRoot, path)) throw new Error(`Project skills path escapes repository: ${path}`);
|
|
3055
|
+
const stat = lstatIfPresent(path);
|
|
3056
|
+
if (stat?.isSymbolicLink()) throw new Error(`Refusing symlinked project skills directory: ${path}`);
|
|
3057
|
+
if (stat && !stat.isDirectory()) throw new Error(`Project skills path is not a directory: ${path}`);
|
|
3058
|
+
}
|
|
3059
|
+
if (!ctx.dryRun) {
|
|
3060
|
+
if (!existsSync10(agentsDir)) mkdirSync6(agentsDir, { recursive: false });
|
|
3061
|
+
if (!existsSync10(skillsDir)) mkdirSync6(skillsDir, { recursive: false });
|
|
3062
|
+
for (const path of [agentsDir, skillsDir]) {
|
|
3063
|
+
if (lstatSync2(path).isSymbolicLink() || !lstatSync2(path).isDirectory()) {
|
|
3064
|
+
throw new Error(`Unsafe project skills directory after creation: ${path}`);
|
|
3065
|
+
}
|
|
3066
|
+
if (!isContainedBy(projectRoot, realpathSync(path))) {
|
|
3067
|
+
throw new Error(`Resolved project skills directory escapes repository: ${path}`);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
return { agentsDir, skillsDir };
|
|
3072
|
+
}
|
|
3073
|
+
function projectSkillTopologyIssues(repoRoot) {
|
|
3074
|
+
const issues = [];
|
|
3075
|
+
let projectRoot;
|
|
3076
|
+
try {
|
|
3077
|
+
projectRoot = realpathSync(repoRoot);
|
|
3078
|
+
} catch (error) {
|
|
3079
|
+
return [`Project root is not a readable real directory: ${error instanceof Error ? error.message : String(error)}`];
|
|
3080
|
+
}
|
|
3081
|
+
const managedSkills = join14(projectRoot, ".agents", "skills");
|
|
3082
|
+
for (const rel of PROJECT_CLI_SKILL_DIRS) {
|
|
3083
|
+
const cliDir = join14(projectRoot, rel);
|
|
3084
|
+
const parent = dirname6(cliDir);
|
|
3085
|
+
const parentStat = lstatIfPresent(parent);
|
|
3086
|
+
if (!parentStat) continue;
|
|
3087
|
+
if (parentStat.isSymbolicLink() || !parentStat.isDirectory()) {
|
|
3088
|
+
issues.push(`${rel} has an unsafe symlinked/non-directory parent`);
|
|
3089
|
+
continue;
|
|
3090
|
+
}
|
|
3091
|
+
if (!isContainedBy(projectRoot, realOrSelf(parent))) {
|
|
3092
|
+
issues.push(`${rel} parent resolves outside the project`);
|
|
3093
|
+
continue;
|
|
3094
|
+
}
|
|
3095
|
+
const stat = lstatIfPresent(cliDir);
|
|
3096
|
+
if (!stat) continue;
|
|
3097
|
+
if (stat.isSymbolicLink()) {
|
|
3098
|
+
let rawTarget = "";
|
|
3099
|
+
try {
|
|
3100
|
+
rawTarget = readlinkSync(cliDir);
|
|
3101
|
+
} catch {
|
|
3102
|
+
issues.push(`${rel} is an unreadable skills directory symlink`);
|
|
3103
|
+
continue;
|
|
3104
|
+
}
|
|
3105
|
+
if (rel !== ".claude/skills" || rawTarget !== CANONICAL_CLAUDE_SKILLS_ALIAS) {
|
|
3106
|
+
issues.push(`${rel} is an unsupported skills directory symlink`);
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
3109
|
+
const managedStat = lstatIfPresent(managedSkills);
|
|
3110
|
+
if (!managedStat || managedStat.isSymbolicLink() || !managedStat.isDirectory()) {
|
|
3111
|
+
issues.push(`${rel} canonical alias target .agents/skills is missing or unsafe`);
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
try {
|
|
3115
|
+
if (realpathSync(cliDir) !== realpathSync(managedSkills)) {
|
|
3116
|
+
issues.push(`${rel} canonical alias resolves outside .agents/skills`);
|
|
3117
|
+
}
|
|
3118
|
+
} catch {
|
|
3119
|
+
issues.push(`${rel} canonical alias is broken`);
|
|
3120
|
+
}
|
|
3121
|
+
continue;
|
|
3122
|
+
}
|
|
3123
|
+
if (!stat.isDirectory()) {
|
|
3124
|
+
issues.push(`${rel} is not a directory`);
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
if (!isContainedBy(projectRoot, realOrSelf(cliDir))) {
|
|
3128
|
+
issues.push(`${rel} resolves outside the project`);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
return issues;
|
|
3132
|
+
}
|
|
3133
|
+
function bmadPackRoot(ctx) {
|
|
3134
|
+
return resolve3(
|
|
3135
|
+
process.env.PJ_BMAD_PACK_ROOT?.trim() || join14(ctx.homeDir, "code", "skillex", "packs", "bmad", BMAD_PACK_VERSION)
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
function canonicalBmadSkillEntries(ctx) {
|
|
3139
|
+
const root = bmadPackRoot(ctx);
|
|
3140
|
+
const trusted = validateTrustedBmadPack(root);
|
|
3141
|
+
return trusted.skillNames.map((name) => ({ name: validateSkillName(name), source: pathToFileURL(join14(root, name)).href }));
|
|
3142
|
+
}
|
|
3143
|
+
function skillManifestEntryName(entry) {
|
|
3144
|
+
if (typeof entry === "string") return entry;
|
|
3145
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
3146
|
+
const name = entry.name;
|
|
3147
|
+
return typeof name === "string" ? name : void 0;
|
|
3148
|
+
}
|
|
3149
|
+
function isPackManagedManifestEntry(entry, expectedNames, packRoot) {
|
|
3150
|
+
const name = skillManifestEntryName(entry);
|
|
3151
|
+
if (!name) return false;
|
|
3152
|
+
if (expectedNames.has(name)) return true;
|
|
3153
|
+
if (!entry || typeof entry !== "object") return false;
|
|
3154
|
+
const source = entry.source;
|
|
3155
|
+
if (typeof source !== "string" || !source.startsWith("file:")) return false;
|
|
3156
|
+
try {
|
|
3157
|
+
const sourcePath = resolve3(fileURLToPath3(source));
|
|
3158
|
+
return basename4(sourcePath) === name && isContainedBy(packRoot, sourcePath);
|
|
3159
|
+
} catch {
|
|
3160
|
+
return false;
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
function canonicalSkillsManifest(ctx, current, packSkills = canonicalBmadSkillEntries(ctx)) {
|
|
3164
|
+
const existing = Array.isArray(current?.skills) ? current.skills : [];
|
|
3165
|
+
const expectedNames = new Set(packSkills.map((entry) => entry.name));
|
|
3166
|
+
const packRoot = bmadPackRoot(ctx);
|
|
2842
3167
|
return `${JSON.stringify(
|
|
2843
3168
|
{
|
|
3169
|
+
...current ?? {},
|
|
2844
3170
|
$schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
|
|
2845
3171
|
inherit_global: true,
|
|
2846
3172
|
registry: SKILLS_REGISTRY_URL,
|
|
2847
|
-
skills: [
|
|
3173
|
+
skills: [
|
|
3174
|
+
...existing.filter((entry) => !isPackManagedManifestEntry(entry, expectedNames, packRoot)),
|
|
3175
|
+
...packSkills
|
|
3176
|
+
]
|
|
2848
3177
|
},
|
|
2849
3178
|
null,
|
|
2850
3179
|
2
|
|
2851
3180
|
)}
|
|
2852
3181
|
`;
|
|
2853
3182
|
}
|
|
3183
|
+
function removeProjectEntry(path) {
|
|
3184
|
+
const stat = lstatIfPresent(path);
|
|
3185
|
+
if (!stat) return;
|
|
3186
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
3187
|
+
rmSync(path, { recursive: true, force: true });
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
try {
|
|
3191
|
+
unlinkSync3(path);
|
|
3192
|
+
} catch (error) {
|
|
3193
|
+
if (error.code !== "ENOENT") throw error;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
function normalizeExecutableTemplate(ctx, target, expected, changedFiles) {
|
|
3197
|
+
const stat = lstatIfPresent(target);
|
|
3198
|
+
if (stat && (!stat.isFile() || stat.isSymbolicLink())) {
|
|
3199
|
+
throw new Error(`Refusing non-regular managed executable target: ${target}`);
|
|
3200
|
+
}
|
|
3201
|
+
const contentChanged = !stat || safeReadText(target) !== expected;
|
|
3202
|
+
const modeChanged = !stat || (Number(stat.mode) & 73) === 0;
|
|
3203
|
+
if (!contentChanged && !modeChanged) return;
|
|
3204
|
+
if (!changedFiles.includes(target)) changedFiles.push(target);
|
|
3205
|
+
if (ctx.dryRun) return;
|
|
3206
|
+
if (contentChanged) {
|
|
3207
|
+
writeText(target, expected);
|
|
3208
|
+
}
|
|
3209
|
+
const beforeChmod = lstatIfPresent(target);
|
|
3210
|
+
if (!beforeChmod?.isFile() || beforeChmod.isSymbolicLink()) {
|
|
3211
|
+
throw new Error(`Refusing changed managed executable target: ${target}`);
|
|
3212
|
+
}
|
|
3213
|
+
chmodSync4(target, 493);
|
|
3214
|
+
}
|
|
3215
|
+
function atomicWriteBuffer(path, content, mode, temporary) {
|
|
3216
|
+
writeFileSync7(temporary, content, { flag: "wx" });
|
|
3217
|
+
chmodSync4(temporary, mode);
|
|
3218
|
+
renameSync2(temporary, path);
|
|
3219
|
+
}
|
|
3220
|
+
function provisionBmadSkills(ctx, preservedManifest, hooks = {}) {
|
|
3221
|
+
const packRoot = bmadPackRoot(ctx);
|
|
3222
|
+
let packSkills;
|
|
3223
|
+
try {
|
|
3224
|
+
packSkills = canonicalBmadSkillEntries(ctx);
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
return {
|
|
3227
|
+
ok: false,
|
|
3228
|
+
changedFiles: [],
|
|
3229
|
+
error: `BMAD Skillex pack ${BMAD_PACK_VERSION} is not trusted at ${packRoot}: ${error instanceof Error ? error.message : String(error)}`
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
hooks.afterPreflight?.();
|
|
3233
|
+
const projectRoot = realpathSync(ctx.repoRoot);
|
|
3234
|
+
const agentsPath = join14(projectRoot, ".agents");
|
|
3235
|
+
const skillsPath = join14(agentsPath, "skills");
|
|
3236
|
+
const agentsExisted = Boolean(lstatIfPresent(agentsPath));
|
|
3237
|
+
const skillsExisted = Boolean(lstatIfPresent(skillsPath));
|
|
3238
|
+
let preflightDirs;
|
|
3239
|
+
try {
|
|
3240
|
+
preflightDirs = prepareSafeProjectSkillsDirs({ ...ctx, dryRun: true });
|
|
3241
|
+
} catch (error) {
|
|
3242
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3243
|
+
}
|
|
3244
|
+
const manifestPath = join14(preflightDirs.agentsDir, "skills.json");
|
|
3245
|
+
const manifestStat = lstatIfPresent(manifestPath);
|
|
3246
|
+
if (manifestStat?.isSymbolicLink() || manifestStat && !manifestStat.isFile()) {
|
|
3247
|
+
return { ok: false, changedFiles: [], error: `Refusing unsafe skills manifest: ${manifestPath}` };
|
|
3248
|
+
}
|
|
3249
|
+
const manifestBytes = manifestStat ? readFileSync7(manifestPath) : null;
|
|
3250
|
+
const manifestMode = manifestStat ? Number(manifestStat.mode) & 511 : 420;
|
|
3251
|
+
let currentManifest = {};
|
|
3252
|
+
if (manifestBytes !== null) {
|
|
3253
|
+
try {
|
|
3254
|
+
const parsed = JSON.parse(manifestBytes.toString("utf8"));
|
|
3255
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("must contain a JSON object");
|
|
3256
|
+
currentManifest = parsed;
|
|
3257
|
+
if (currentManifest.skills !== void 0 && !Array.isArray(currentManifest.skills)) throw new Error("skills must be an array");
|
|
3258
|
+
} catch (error) {
|
|
3259
|
+
return { ok: false, changedFiles: [], error: `Invalid existing skills manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}` };
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
let safeDirs;
|
|
3263
|
+
try {
|
|
3264
|
+
safeDirs = prepareSafeProjectSkillsDirs(ctx);
|
|
3265
|
+
} catch (error) {
|
|
3266
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3267
|
+
}
|
|
3268
|
+
const nextManifest = canonicalSkillsManifest(ctx, preservedManifest ?? currentManifest, packSkills);
|
|
3269
|
+
const skillsDir = safeDirs.skillsDir;
|
|
3270
|
+
const resolvedSkillsDir = ctx.dryRun && !existsSync10(skillsDir) ? skillsDir : realpathSync(skillsDir);
|
|
3271
|
+
const expected = new Map(packSkills.map((entry) => [entry.name, fileURLToPath3(entry.source)]));
|
|
3272
|
+
const expectedNames = new Set(expected.keys());
|
|
3273
|
+
const ownershipManifest = preservedManifest ?? currentManifest;
|
|
3274
|
+
const managedManifestNames = new Set(
|
|
3275
|
+
(Array.isArray(ownershipManifest.skills) ? ownershipManifest.skills : []).filter((entry) => isPackManagedManifestEntry(entry, expectedNames, packRoot)).map(skillManifestEntryName).filter((name) => Boolean(name))
|
|
3276
|
+
);
|
|
3277
|
+
const affected = /* @__PURE__ */ new Set();
|
|
3278
|
+
const staleManagedNames = /* @__PURE__ */ new Set();
|
|
3279
|
+
const originalCorrectLinks = /* @__PURE__ */ new Map();
|
|
3280
|
+
if (existsSync10(skillsDir)) {
|
|
3281
|
+
for (const name of readdirSync3(skillsDir)) {
|
|
3282
|
+
validateSkillName(name);
|
|
3283
|
+
if (dirname6(join14(resolvedSkillsDir, name)) !== resolvedSkillsDir) {
|
|
3284
|
+
return { ok: false, changedFiles: [], error: `BMAD skill path escapes project skills directory: ${name}` };
|
|
3285
|
+
}
|
|
3286
|
+
const entryPath = join14(skillsDir, name);
|
|
3287
|
+
let linkTargetsPack = false;
|
|
3288
|
+
try {
|
|
3289
|
+
linkTargetsPack = lstatSync2(entryPath).isSymbolicLink() && isContainedBy(packRoot, resolve3(dirname6(entryPath), readlinkSync(entryPath)));
|
|
3290
|
+
} catch {
|
|
3291
|
+
linkTargetsPack = false;
|
|
3292
|
+
}
|
|
3293
|
+
if (!expected.has(name) && !managedManifestNames.has(name) && !linkTargetsPack) continue;
|
|
3294
|
+
const target = expected.get(name);
|
|
3295
|
+
let correct = false;
|
|
3296
|
+
try {
|
|
3297
|
+
correct = Boolean(target) && lstatSync2(entryPath).isSymbolicLink() && resolve3(dirname6(entryPath), readlinkSync(entryPath)) === target;
|
|
3298
|
+
} catch {
|
|
3299
|
+
correct = false;
|
|
3300
|
+
}
|
|
3301
|
+
if (correct) originalCorrectLinks.set(name, readlinkSync(join14(skillsDir, name)));
|
|
3302
|
+
else {
|
|
3303
|
+
affected.add(name);
|
|
3304
|
+
if (!target) staleManagedNames.add(name);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
for (const [name, target] of expected) {
|
|
3309
|
+
const link = join14(resolvedSkillsDir, validateSkillName(name));
|
|
3310
|
+
if (dirname6(link) !== resolvedSkillsDir) {
|
|
3311
|
+
return { ok: false, changedFiles: [], error: `BMAD skill path escapes project skills directory: ${name}` };
|
|
3312
|
+
}
|
|
3313
|
+
let correct = false;
|
|
3314
|
+
try {
|
|
3315
|
+
correct = lstatSync2(link).isSymbolicLink() && resolve3(dirname6(link), readlinkSync(link)) === target;
|
|
3316
|
+
} catch {
|
|
3317
|
+
correct = false;
|
|
3318
|
+
}
|
|
3319
|
+
if (!correct) affected.add(name);
|
|
3320
|
+
}
|
|
3321
|
+
const manifestChanged = manifestBytes?.toString("utf8") !== nextManifest;
|
|
3322
|
+
const changedFiles = [
|
|
3323
|
+
...manifestChanged ? [manifestPath] : [],
|
|
3324
|
+
...affected.size ? [skillsDir] : []
|
|
3325
|
+
];
|
|
3326
|
+
if (ctx.dryRun || changedFiles.length === 0) {
|
|
3327
|
+
try {
|
|
3328
|
+
const postflight = validateTrustedBmadPack(packRoot);
|
|
3329
|
+
if (JSON.stringify(postflight.skillNames) !== JSON.stringify(packSkills.map((entry) => entry.name))) {
|
|
3330
|
+
throw new Error("BMAD pack inventory changed after preflight");
|
|
3331
|
+
}
|
|
3332
|
+
return { ok: true, changedFiles };
|
|
3333
|
+
} catch (error) {
|
|
3334
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
const transaction = mkdtempSync(join14(safeDirs.agentsDir, ".bmad-transaction-"));
|
|
3338
|
+
const backup = join14(transaction, "entries");
|
|
3339
|
+
mkdirSync6(backup);
|
|
3340
|
+
const moved = [];
|
|
3341
|
+
const rollback = () => {
|
|
3342
|
+
const errors = [];
|
|
3343
|
+
try {
|
|
3344
|
+
for (const name of affected) {
|
|
3345
|
+
removeProjectEntry(join14(skillsDir, validateSkillName(name)));
|
|
3346
|
+
}
|
|
3347
|
+
for (const name of originalCorrectLinks.keys()) {
|
|
3348
|
+
removeProjectEntry(join14(skillsDir, validateSkillName(name)));
|
|
3349
|
+
}
|
|
3350
|
+
} catch (error) {
|
|
3351
|
+
errors.push(`remove applied projection: ${String(error)}`);
|
|
3352
|
+
}
|
|
3353
|
+
for (const name of [...moved].reverse()) {
|
|
3354
|
+
try {
|
|
3355
|
+
renameSync2(join14(backup, name), join14(skillsDir, name));
|
|
3356
|
+
} catch (error) {
|
|
3357
|
+
errors.push(`restore ${name}: ${String(error)}`);
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
for (const [name, rawTarget] of originalCorrectLinks) {
|
|
3361
|
+
try {
|
|
3362
|
+
symlinkSync(rawTarget, join14(skillsDir, name), "dir");
|
|
3363
|
+
} catch (error) {
|
|
3364
|
+
errors.push(`restore ${name}: ${String(error)}`);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
try {
|
|
3368
|
+
if (manifestBytes === null) removeProjectEntry(manifestPath);
|
|
3369
|
+
else atomicWriteBuffer(manifestPath, manifestBytes, manifestMode, join14(transaction, "manifest.restore"));
|
|
3370
|
+
} catch (error) {
|
|
3371
|
+
errors.push(`restore manifest: ${String(error)}`);
|
|
3372
|
+
}
|
|
3373
|
+
rmSync(transaction, { recursive: true, force: true });
|
|
3374
|
+
try {
|
|
3375
|
+
if (!skillsExisted && existsSync10(skillsDir) && readdirSync3(skillsDir).length === 0) rmdirSync(skillsDir);
|
|
3376
|
+
if (!agentsExisted && existsSync10(safeDirs.agentsDir) && readdirSync3(safeDirs.agentsDir).length === 0) rmdirSync(safeDirs.agentsDir);
|
|
3377
|
+
} catch (error) {
|
|
3378
|
+
errors.push(`remove created directories: ${String(error)}`);
|
|
3379
|
+
}
|
|
3380
|
+
if (errors.length) throw new Error(`BMAD rollback was incomplete: ${errors.join("; ")}`);
|
|
3381
|
+
};
|
|
3382
|
+
try {
|
|
3383
|
+
for (const name of affected) {
|
|
3384
|
+
const entry = join14(skillsDir, name);
|
|
3385
|
+
if (lstatIfPresent(entry)) {
|
|
3386
|
+
renameSync2(entry, join14(backup, name));
|
|
3387
|
+
moved.push(name);
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
let index = 0;
|
|
3391
|
+
for (const [name, target] of expected) {
|
|
3392
|
+
index += 1;
|
|
3393
|
+
const link = join14(skillsDir, name);
|
|
3394
|
+
let correct = false;
|
|
3395
|
+
try {
|
|
3396
|
+
correct = lstatSync2(link).isSymbolicLink() && resolve3(skillsDir, readlinkSync(link)) === target;
|
|
3397
|
+
} catch {
|
|
3398
|
+
correct = false;
|
|
3399
|
+
}
|
|
3400
|
+
if (correct) continue;
|
|
3401
|
+
if (hooks.createLink) hooks.createLink(target, link, index);
|
|
3402
|
+
else symlinkSync(target, link, "dir");
|
|
3403
|
+
}
|
|
3404
|
+
if (manifestChanged) {
|
|
3405
|
+
atomicWriteBuffer(manifestPath, Buffer.from(nextManifest), manifestMode, join14(transaction, "manifest.next"));
|
|
3406
|
+
}
|
|
3407
|
+
const postflight = validateTrustedBmadPack(packRoot);
|
|
3408
|
+
if (JSON.stringify(postflight.skillNames) !== JSON.stringify(packSkills.map((entry) => entry.name))) {
|
|
3409
|
+
throw new Error("BMAD pack inventory changed after preflight");
|
|
3410
|
+
}
|
|
3411
|
+
hooks.afterApply?.(manifestPath, skillsDir);
|
|
3412
|
+
for (const name of staleManagedNames) {
|
|
3413
|
+
if (lstatIfPresent(join14(skillsDir, name))) {
|
|
3414
|
+
throw new Error(`Applied BMAD projection retained stale managed entry: ${name}`);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
for (const [name, target] of expected) {
|
|
3418
|
+
const link = join14(skillsDir, name);
|
|
3419
|
+
let correct = false;
|
|
3420
|
+
try {
|
|
3421
|
+
correct = lstatSync2(link).isSymbolicLink() && resolve3(skillsDir, readlinkSync(link)) === target;
|
|
3422
|
+
} catch {
|
|
3423
|
+
correct = false;
|
|
3424
|
+
}
|
|
3425
|
+
if (!correct) throw new Error(`Applied BMAD projection link differs from plan: ${name}`);
|
|
3426
|
+
}
|
|
3427
|
+
const finalManifestStat = lstatIfPresent(manifestPath);
|
|
3428
|
+
if (!finalManifestStat || finalManifestStat.isSymbolicLink() || !finalManifestStat.isFile() || (Number(finalManifestStat.mode) & 511) !== manifestMode || readFileSync7(manifestPath).toString("utf8") !== nextManifest) {
|
|
3429
|
+
throw new Error("Applied BMAD skills manifest differs from planned bytes or mode");
|
|
3430
|
+
}
|
|
3431
|
+
const finalManifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
|
|
3432
|
+
if (finalManifest.$schema !== "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json" || finalManifest.inherit_global !== true || finalManifest.registry !== SKILLS_REGISTRY_URL || !Array.isArray(finalManifest.skills)) {
|
|
3433
|
+
throw new Error("Applied BMAD skills manifest schema differs from plan");
|
|
3434
|
+
}
|
|
3435
|
+
} catch (error) {
|
|
3436
|
+
try {
|
|
3437
|
+
rollback();
|
|
3438
|
+
} catch (rollbackError) {
|
|
3439
|
+
return { ok: false, changedFiles: [], error: `BMAD provisioning failed (${String(error)}); ${String(rollbackError)}` };
|
|
3440
|
+
}
|
|
3441
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3442
|
+
}
|
|
3443
|
+
rmSync(transaction, { recursive: true });
|
|
3444
|
+
return { ok: true, changedFiles };
|
|
3445
|
+
}
|
|
2854
3446
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2855
|
-
const packageJson =
|
|
3447
|
+
const packageJson = join14(repoRoot, "package.json");
|
|
2856
3448
|
return existsSync10(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
2857
3449
|
}
|
|
2858
3450
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
@@ -2877,7 +3469,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2877
3469
|
function requiredMisePathEntries(ctx) {
|
|
2878
3470
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2879
3471
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2880
|
-
if (existsSync10(
|
|
3472
|
+
if (existsSync10(join14(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2881
3473
|
}
|
|
2882
3474
|
return required;
|
|
2883
3475
|
}
|
|
@@ -2979,6 +3571,9 @@ function isManagedHookEntry(value) {
|
|
|
2979
3571
|
const trimmed = value.trim();
|
|
2980
3572
|
if (trimmed === OP_INJECT_SCRIPT) return true;
|
|
2981
3573
|
if (trimmed === SYNC_SKILLS_SCRIPT) return true;
|
|
3574
|
+
if (trimmed === PROVISION_BMAD_SKILLS_SCRIPT) return true;
|
|
3575
|
+
if (/sync-skills(?:\.py)?["']?\s+--scope project/.test(trimmed)) return true;
|
|
3576
|
+
if (/provision-bmad-skills\.py/.test(trimmed)) return true;
|
|
2982
3577
|
if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2983
3578
|
if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2984
3579
|
return /link-agentfiles\.sh'?\s*$/.test(trimmed);
|
|
@@ -3084,6 +3679,7 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
|
3084
3679
|
const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
|
|
3085
3680
|
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
3086
3681
|
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
|
|
3682
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-provision-bmad\]$/, void 0, { includePrecedingComments: false });
|
|
3087
3683
|
cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3088
3684
|
cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3089
3685
|
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
|
|
@@ -3093,7 +3689,7 @@ function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
|
3093
3689
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
3094
3690
|
}
|
|
3095
3691
|
function readProjectJson(ctx) {
|
|
3096
|
-
return tryParseJson(safeReadText(
|
|
3692
|
+
return tryParseJson(safeReadText(join14(ctx.repoRoot, ".project.json")));
|
|
3097
3693
|
}
|
|
3098
3694
|
function boolSetting(value, fallback) {
|
|
3099
3695
|
if (typeof value === "boolean") return value;
|
|
@@ -3131,7 +3727,7 @@ function canonicalProjectJson(ctx) {
|
|
|
3131
3727
|
role.agentId || `${slug}-${role.role}`,
|
|
3132
3728
|
{
|
|
3133
3729
|
role: role.role,
|
|
3134
|
-
role_dir:
|
|
3730
|
+
role_dir: relative2(ctx.repoRoot, role.roleDir)
|
|
3135
3731
|
}
|
|
3136
3732
|
])
|
|
3137
3733
|
);
|
|
@@ -3168,8 +3764,8 @@ function canonicalProjectJson(ctx) {
|
|
|
3168
3764
|
};
|
|
3169
3765
|
}
|
|
3170
3766
|
function projectJsonFinding(ctx) {
|
|
3171
|
-
const projectPath =
|
|
3172
|
-
const planeJsonPath =
|
|
3767
|
+
const projectPath = join14(ctx.repoRoot, ".project.json");
|
|
3768
|
+
const planeJsonPath = join14(ctx.repoRoot, ".plane.json");
|
|
3173
3769
|
const details = [];
|
|
3174
3770
|
const data = readProjectJson(ctx);
|
|
3175
3771
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -3191,8 +3787,8 @@ function projectJsonFinding(ctx) {
|
|
|
3191
3787
|
continue;
|
|
3192
3788
|
}
|
|
3193
3789
|
if (agent.role !== role.role) details.push(`agents.${role.agentId}.role should be ${role.role}`);
|
|
3194
|
-
if (agent.role_dir !==
|
|
3195
|
-
details.push(`agents.${role.agentId}.role_dir should be ${
|
|
3790
|
+
if (agent.role_dir !== relative2(ctx.repoRoot, role.roleDir)) {
|
|
3791
|
+
details.push(`agents.${role.agentId}.role_dir should be ${relative2(ctx.repoRoot, role.roleDir)}`);
|
|
3196
3792
|
}
|
|
3197
3793
|
}
|
|
3198
3794
|
const ticketProvider = data.ticket_provider ?? {};
|
|
@@ -3221,8 +3817,7 @@ function projectJsonFinding(ctx) {
|
|
|
3221
3817
|
function renderSoul(role) {
|
|
3222
3818
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
3223
3819
|
const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` : "Direct and brief.";
|
|
3224
|
-
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat
|
|
3225
|
-
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
3820
|
+
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checks runtime health; when this repo opts into reconciliation (\`automation.reconcile.enabled\` in repo-root \`.project.json\`), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
|
|
3226
3821
|
return `# ${role.displayName || role.agentId}
|
|
3227
3822
|
|
|
3228
3823
|
You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisioned to work inside the
|
|
@@ -3241,7 +3836,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
|
|
|
3241
3836
|
|
|
3242
3837
|
## Scope
|
|
3243
3838
|
|
|
3244
|
-
You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME is the
|
|
3839
|
+
You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME is the ignored local directory at \`./runtime/\`, which \`~/.hermes/profiles/${role.profileName || role.agentId}\` projects into (so \`--profile\` invocations resolve here too). Secrets, SOUL, memories, skills, sessions, gateway state, and runtime files stay local to that runtime and are never project gitlinks.
|
|
3245
3840
|
|
|
3246
3841
|
## Tone
|
|
3247
3842
|
|
|
@@ -3253,12 +3848,12 @@ ${roleSpecific}
|
|
|
3253
3848
|
|
|
3254
3849
|
## Memory hygiene
|
|
3255
3850
|
|
|
3256
|
-
Your memory is
|
|
3851
|
+
Your memory is stored locally at \`./runtime/memories/\`. Use durable memory deliberately and keep \`memories/MEMORY.md\` current.
|
|
3257
3852
|
`;
|
|
3258
3853
|
}
|
|
3259
3854
|
function renderHermesWrapper(role) {
|
|
3260
3855
|
return `#!/usr/bin/env bash
|
|
3261
|
-
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime
|
|
3856
|
+
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the local runtime.
|
|
3262
3857
|
|
|
3263
3858
|
set -euo pipefail
|
|
3264
3859
|
|
|
@@ -3271,30 +3866,44 @@ if [[ -f "$FLEET_ENV" ]]; then
|
|
|
3271
3866
|
source "$FLEET_ENV"
|
|
3272
3867
|
fi
|
|
3273
3868
|
|
|
3274
|
-
HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN
|
|
3275
|
-
HERMES_OAUTH_FILE="{HERMES_OAUTH_FILE:-{HERMES_FLEET_OAUTH_FILE:-$HOME/.hermes/auth.json}}"
|
|
3869
|
+
HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN:-$HOME/.hermes/hermes-agent/.venv/bin/hermes}}"
|
|
3276
3870
|
CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
|
|
3277
3871
|
|
|
3278
3872
|
FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
3279
3873
|
PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
|
|
3280
|
-
|
|
3874
|
+
|
|
3875
|
+
# Singleton-runtime contract: HERMES_HOME MUST be the named profile dir, never
|
|
3876
|
+
# the raw runtime path. Hermes treats any HERMES_HOME that is neither under
|
|
3877
|
+
# ~/.hermes nor a child of a "profiles" dir as its own standalone root, which
|
|
3878
|
+
# makes get_active_profile_name() report "default" and _global_auth_file_path()
|
|
3879
|
+
# return None -- silently disabling shared fleet auth and giving every agent a
|
|
3880
|
+
# divergent config.yaml. The profile dir is a REAL dir whose shared entries
|
|
3881
|
+
# (config.yaml, .env, skills) symlink to the fleet root and whose person-owned
|
|
3882
|
+
# entries (memories, sessions, state.db, workspace) symlink into $RUNTIME_HOME.
|
|
3883
|
+
HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"
|
|
3281
3884
|
|
|
3282
3885
|
if [[ ! -d "$RUNTIME_HOME" ]]; then
|
|
3283
|
-
echo "hermes: runtime
|
|
3284
|
-
echo " fix:
|
|
3886
|
+
echo "hermes: local runtime not provisioned at $RUNTIME_HOME" >&2
|
|
3887
|
+
echo " fix: run the role's .scripts/20-runtime-repo.sh" >&2
|
|
3888
|
+
exit 1
|
|
3889
|
+
fi
|
|
3890
|
+
|
|
3891
|
+
if [[ ! -d "$HERMES_HOME" ]]; then
|
|
3892
|
+
echo "hermes: profile not provisioned at $HERMES_HOME" >&2
|
|
3893
|
+
echo " fix: pj migrate hermes.runtime-singleton" >&2
|
|
3285
3894
|
exit 1
|
|
3286
3895
|
fi
|
|
3287
3896
|
|
|
3288
|
-
exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV"
|
|
3897
|
+
exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" CODEX_HOME="$CODEX_HOME" "$HERMES_BIN" "$@"
|
|
3289
3898
|
`.replace(/\u0010/g, "$");
|
|
3290
3899
|
}
|
|
3291
3900
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
3292
3901
|
if (!existsSync10(sourceDir)) return;
|
|
3293
3902
|
mkdirSync6(targetDir, { recursive: true });
|
|
3294
|
-
for (const entry of
|
|
3295
|
-
const sourcePath =
|
|
3903
|
+
for (const entry of readdirSync3(sourceDir, { withFileTypes: true })) {
|
|
3904
|
+
const sourcePath = join14(sourceDir, entry.name);
|
|
3296
3905
|
if (skip?.(sourcePath)) continue;
|
|
3297
|
-
const targetPath =
|
|
3906
|
+
const targetPath = join14(targetDir, entry.name);
|
|
3298
3907
|
if (entry.isDirectory()) {
|
|
3299
3908
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
3300
3909
|
continue;
|
|
@@ -3307,21 +3916,78 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
3307
3916
|
}
|
|
3308
3917
|
}
|
|
3309
3918
|
}
|
|
3310
|
-
function
|
|
3311
|
-
const
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3919
|
+
function runtimeSubmodulePath(repoRoot, role) {
|
|
3920
|
+
const rolePath = relative2(repoRoot, role.roleDir).replace(/\\/g, "/");
|
|
3921
|
+
if (!/^agents\/hermes\/[^/]+$/.test(rolePath)) return null;
|
|
3922
|
+
return `${rolePath}/runtime`;
|
|
3923
|
+
}
|
|
3924
|
+
function submoduleSectionHasPath(section, targetPath) {
|
|
3925
|
+
return section.split(/\r?\n/).some((line) => /^\s*path\s*=/.test(line) && line.replace(/^\s*path\s*=\s*/, "").trim() === targetPath);
|
|
3926
|
+
}
|
|
3927
|
+
function hasRuntimeSubmoduleMapping(repoRoot, role) {
|
|
3928
|
+
const gitmodulesPath = join14(repoRoot, ".gitmodules");
|
|
3318
3929
|
const current = safeReadText(gitmodulesPath) ?? "";
|
|
3319
|
-
const
|
|
3320
|
-
|
|
3930
|
+
const sections = current.match(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm) ?? [];
|
|
3931
|
+
const targetPath = runtimeSubmodulePath(repoRoot, role);
|
|
3932
|
+
return Boolean(targetPath && sections.some((section) => submoduleSectionHasPath(section, targetPath)));
|
|
3933
|
+
}
|
|
3934
|
+
function removeRuntimeSubmoduleMapping(repoRoot, role, changedFiles, dryRun) {
|
|
3935
|
+
const gitmodulesPath = join14(repoRoot, ".gitmodules");
|
|
3936
|
+
const current = safeReadText(gitmodulesPath) ?? "";
|
|
3937
|
+
if (!hasRuntimeSubmoduleMapping(repoRoot, role)) return [];
|
|
3938
|
+
const targetPath = runtimeSubmodulePath(repoRoot, role);
|
|
3939
|
+
if (!targetPath) return [];
|
|
3940
|
+
const next = current.replace(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm, (section) => submoduleSectionHasPath(section, targetPath) ? "" : section).replace(/\n{3,}/g, "\n\n").trim();
|
|
3321
3941
|
changedFiles.push(gitmodulesPath);
|
|
3322
|
-
if (!dryRun) writeText(gitmodulesPath,
|
|
3942
|
+
if (!dryRun) writeText(gitmodulesPath, next ? `${next}
|
|
3943
|
+
` : "");
|
|
3323
3944
|
return [gitmodulesPath];
|
|
3324
3945
|
}
|
|
3946
|
+
function retireRuntimeSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
3947
|
+
const runtimePath = runtimeSubmodulePath(repoRoot, role);
|
|
3948
|
+
if (!runtimePath) {
|
|
3949
|
+
return { ok: false, details: [], error: `refusing unsafe runtime path for ${role.roleDir}` };
|
|
3950
|
+
}
|
|
3951
|
+
const probe = spawnSync6("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
3952
|
+
cwd: repoRoot,
|
|
3953
|
+
encoding: "utf8"
|
|
3954
|
+
});
|
|
3955
|
+
if (probe.status !== 0) {
|
|
3956
|
+
return { ok: false, details: [], error: `failed to inspect runtime index at ${runtimePath}: ${probe.stderr.trim() || `exit ${probe.status}`}` };
|
|
3957
|
+
}
|
|
3958
|
+
const details = [];
|
|
3959
|
+
if (probe.stdout.trim()) {
|
|
3960
|
+
details.push(`untrack ${runtimePath}`);
|
|
3961
|
+
if (dryRun) {
|
|
3962
|
+
changedFiles.push(runtimePath);
|
|
3963
|
+
} else {
|
|
3964
|
+
const removal = spawnSync6("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
|
|
3965
|
+
cwd: repoRoot,
|
|
3966
|
+
encoding: "utf8"
|
|
3967
|
+
});
|
|
3968
|
+
if (removal.status !== 0) {
|
|
3969
|
+
return { ok: false, details, error: `failed to untrack ${runtimePath}: ${removal.stderr.trim() || `exit ${removal.status}`}` };
|
|
3970
|
+
}
|
|
3971
|
+
const verification = spawnSync6("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
3972
|
+
cwd: repoRoot,
|
|
3973
|
+
encoding: "utf8"
|
|
3974
|
+
});
|
|
3975
|
+
if (verification.status !== 0 || verification.stdout.trim()) {
|
|
3976
|
+
return {
|
|
3977
|
+
ok: false,
|
|
3978
|
+
details,
|
|
3979
|
+
error: verification.status !== 0 ? `failed to verify untracked runtime ${runtimePath}: ${verification.stderr.trim() || `exit ${verification.status}`}` : `runtime remains tracked after index-only removal: ${runtimePath}`
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
changedFiles.push(runtimePath);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
if (hasRuntimeSubmoduleMapping(repoRoot, role)) {
|
|
3986
|
+
details.push(`remove stale .gitmodules mapping for ${runtimePath}`);
|
|
3987
|
+
removeRuntimeSubmoduleMapping(repoRoot, role, changedFiles, dryRun);
|
|
3988
|
+
}
|
|
3989
|
+
return { ok: true, details };
|
|
3990
|
+
}
|
|
3325
3991
|
function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
3326
3992
|
const path = registryPath(homeDir);
|
|
3327
3993
|
const current = safeReadText(path) ?? "# Hermes agent fleet registry.\n# One entry per provisioned agent. Managed by hermes-agent-template/.scripts/80-registry.sh.\nschema_version: 1\nagents: {}\n";
|
|
@@ -3340,9 +4006,11 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
3340
4006
|
project_id: ${ctxEscape(role.ticketProviderBoardId)}
|
|
3341
4007
|
identifier: ${ctxEscape(role.ticketProviderIdentifier)}
|
|
3342
4008
|
runtime_repo: ${ctxEscape(role.runtimeRepo)}
|
|
4009
|
+
bloodbank:
|
|
4010
|
+
gateway_scope: fleet
|
|
4011
|
+
target_agent_id: ${role.agentId}
|
|
3343
4012
|
systemd:
|
|
3344
4013
|
gateway_unit: hermes-${role.agentId}-gateway.service
|
|
3345
|
-
consumer_unit: hermes-${role.agentId}-consumer.service
|
|
3346
4014
|
heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
|
|
3347
4015
|
`;
|
|
3348
4016
|
const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
|
|
@@ -3406,6 +4074,7 @@ function checkUnit(unit) {
|
|
|
3406
4074
|
var BMAD_NPM_PACKAGE = "bmad-method";
|
|
3407
4075
|
var BMAD_TARGET_CHANNEL = "next";
|
|
3408
4076
|
var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
|
|
4077
|
+
var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
|
|
3409
4078
|
var BMAD_INSTALL_TOOLS = [
|
|
3410
4079
|
"claude-code",
|
|
3411
4080
|
"codex",
|
|
@@ -3453,7 +4122,53 @@ var BMAD_INSTALL_TOOLS = [
|
|
|
3453
4122
|
"windsurf",
|
|
3454
4123
|
"zencoder"
|
|
3455
4124
|
];
|
|
3456
|
-
function
|
|
4125
|
+
function manifestBmadModules(repoRoot) {
|
|
4126
|
+
const manifestPath = join14(repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
4127
|
+
const raw = safeReadText(manifestPath);
|
|
4128
|
+
if (raw === null) return { status: "absent" };
|
|
4129
|
+
try {
|
|
4130
|
+
const parsed = YAML3.parse(raw);
|
|
4131
|
+
if (!Array.isArray(parsed?.modules)) {
|
|
4132
|
+
return { status: "invalid", error: `${manifestPath} must define a modules array` };
|
|
4133
|
+
}
|
|
4134
|
+
const declared = [];
|
|
4135
|
+
for (const entry of parsed.modules) {
|
|
4136
|
+
const name = typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.name : void 0;
|
|
4137
|
+
if (typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
|
|
4138
|
+
return { status: "invalid", error: `${manifestPath} contains an invalid module entry` };
|
|
4139
|
+
}
|
|
4140
|
+
if (name !== "core" && name !== "custom") declared.push(name);
|
|
4141
|
+
}
|
|
4142
|
+
return { status: "valid", modules: Array.from(new Set(declared)) };
|
|
4143
|
+
} catch (error) {
|
|
4144
|
+
return {
|
|
4145
|
+
status: "invalid",
|
|
4146
|
+
error: `Could not parse ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
function configuredBmadModules(repoRoot) {
|
|
4151
|
+
const raw = safeReadText(join14(repoRoot, "_bmad", "config.toml"));
|
|
4152
|
+
if (raw === null) return void 0;
|
|
4153
|
+
const modules = [...raw.matchAll(/^\[modules\.([A-Za-z0-9][A-Za-z0-9_-]*)\]\s*$/gm)].map((match) => match[1]);
|
|
4154
|
+
return Array.from(new Set(modules));
|
|
4155
|
+
}
|
|
4156
|
+
function selectedBmadModules(repoRoot) {
|
|
4157
|
+
const manifest = manifestBmadModules(repoRoot);
|
|
4158
|
+
if (manifest.status === "valid") return manifest.modules;
|
|
4159
|
+
if (manifest.status === "invalid") throw new Error(manifest.error);
|
|
4160
|
+
return configuredBmadModules(repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
4161
|
+
}
|
|
4162
|
+
function requiredBmadSentinels(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
4163
|
+
return [
|
|
4164
|
+
join14("core", "config.yaml"),
|
|
4165
|
+
join14("config.toml"),
|
|
4166
|
+
join14("_config", "manifest.yaml"),
|
|
4167
|
+
...modules.map((module) => join14(module, "config.yaml"))
|
|
4168
|
+
];
|
|
4169
|
+
}
|
|
4170
|
+
function bmadInstallArgs(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
4171
|
+
const installerModules = modules.length ? modules.join(",") : "core";
|
|
3457
4172
|
return [
|
|
3458
4173
|
"-y",
|
|
3459
4174
|
`${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
|
|
@@ -3462,20 +4177,20 @@ function bmadInstallArgs(repoRoot) {
|
|
|
3462
4177
|
"--directory",
|
|
3463
4178
|
repoRoot,
|
|
3464
4179
|
"--modules",
|
|
3465
|
-
|
|
4180
|
+
installerModules,
|
|
3466
4181
|
"--tools",
|
|
3467
4182
|
BMAD_INSTALL_TOOLS.join(",")
|
|
3468
4183
|
];
|
|
3469
4184
|
}
|
|
3470
|
-
function runBmadInstall(repoRoot) {
|
|
3471
|
-
const result = spawnSync6("npx", bmadInstallArgs(repoRoot), { encoding: "utf8" });
|
|
4185
|
+
function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
4186
|
+
const result = spawnSync6("npx", bmadInstallArgs(repoRoot, modules), { encoding: "utf8" });
|
|
3472
4187
|
if (result.status !== 0) {
|
|
3473
4188
|
return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
|
|
3474
4189
|
}
|
|
3475
4190
|
return { ok: true };
|
|
3476
4191
|
}
|
|
3477
4192
|
function readInstalledBmadVersion(repoRoot) {
|
|
3478
|
-
const raw = safeReadText(
|
|
4193
|
+
const raw = safeReadText(join14(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
3479
4194
|
if (!raw) return void 0;
|
|
3480
4195
|
try {
|
|
3481
4196
|
const parsed = YAML3.parse(raw);
|
|
@@ -3486,8 +4201,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
3486
4201
|
}
|
|
3487
4202
|
}
|
|
3488
4203
|
function bmadCachePath(homeDir) {
|
|
3489
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
3490
|
-
return
|
|
4204
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join14(homeDir, ".cache");
|
|
4205
|
+
return join14(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
3491
4206
|
}
|
|
3492
4207
|
function readBmadDistTagsCache(homeDir) {
|
|
3493
4208
|
const raw = safeReadText(bmadCachePath(homeDir));
|
|
@@ -3570,65 +4285,279 @@ function compareBmadVersions(a, b) {
|
|
|
3570
4285
|
}
|
|
3571
4286
|
return 0;
|
|
3572
4287
|
}
|
|
3573
|
-
var
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
4288
|
+
var SHARED_PROFILE_ENTRIES = ["config.yaml", ".env", "skills"];
|
|
4289
|
+
var OWNED_PROFILE_ENTRIES = [
|
|
4290
|
+
"memories",
|
|
4291
|
+
"sessions",
|
|
4292
|
+
"workspace",
|
|
4293
|
+
"logs",
|
|
4294
|
+
"cron",
|
|
4295
|
+
"plans",
|
|
4296
|
+
"hooks",
|
|
4297
|
+
"pairing",
|
|
4298
|
+
"audio_cache",
|
|
4299
|
+
"image_cache"
|
|
4300
|
+
];
|
|
4301
|
+
var OWNED_PROFILE_FILES = ["SOUL.md", "state.db", "kanban.db"];
|
|
4302
|
+
function fleetHome(ctx) {
|
|
4303
|
+
return process.env.HERMES_FLEET_HOME || join14(ctx.homeDir, ".hermes");
|
|
4304
|
+
}
|
|
4305
|
+
function fleetBinPath(ctx) {
|
|
4306
|
+
const candidates = [
|
|
4307
|
+
process.env.HERMES_FLEET_BIN,
|
|
4308
|
+
join14(fleetHome(ctx), "hermes-agent", ".venv", "bin", "hermes"),
|
|
4309
|
+
join14(fleetHome(ctx), "hermes-agent", "venv", "bin", "hermes"),
|
|
4310
|
+
join14(ctx.homeDir, ".local", "bin", "hermes")
|
|
4311
|
+
].filter(Boolean);
|
|
4312
|
+
return candidates.find((candidate) => existsSync10(candidate)) ?? "";
|
|
4313
|
+
}
|
|
4314
|
+
function singletonPlan(ctx, role) {
|
|
4315
|
+
const fleetRoot = fleetHome(ctx);
|
|
4316
|
+
const profileName = role.profileName || role.agentId;
|
|
4317
|
+
const profileDir = join14(fleetRoot, "profiles", profileName);
|
|
4318
|
+
const runtimeDir = join14(role.roleDir, "runtime");
|
|
4319
|
+
const links = [];
|
|
4320
|
+
for (const entry of SHARED_PROFILE_ENTRIES) {
|
|
4321
|
+
links.push({ path: join14(profileDir, entry), target: join14(fleetRoot, entry), ensureTargetDir: entry === "skills" });
|
|
4322
|
+
}
|
|
4323
|
+
for (const entry of OWNED_PROFILE_ENTRIES) {
|
|
4324
|
+
links.push({ path: join14(profileDir, entry), target: join14(runtimeDir, entry), ensureTargetDir: true });
|
|
4325
|
+
}
|
|
4326
|
+
for (const entry of OWNED_PROFILE_FILES) {
|
|
4327
|
+
links.push({ path: join14(profileDir, entry), target: join14(runtimeDir, entry), ensureTargetDir: false });
|
|
4328
|
+
}
|
|
4329
|
+
const sharedSeeds = ["config.yaml", "auth.json", ".env"].map((entry) => ({
|
|
4330
|
+
rootPath: join14(fleetRoot, entry),
|
|
4331
|
+
runtimePath: join14(runtimeDir, entry)
|
|
4332
|
+
}));
|
|
4333
|
+
return { fleetRoot, profileDir, runtimeDir, links, sharedSeeds };
|
|
4334
|
+
}
|
|
4335
|
+
function isDanglingLink(path) {
|
|
4336
|
+
try {
|
|
4337
|
+
return lstatSync2(path).isSymbolicLink() && !existsSync10(path);
|
|
4338
|
+
} catch {
|
|
4339
|
+
return false;
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
function linkState(path, target) {
|
|
4343
|
+
let stat;
|
|
4344
|
+
try {
|
|
4345
|
+
stat = lstatSync2(path);
|
|
4346
|
+
} catch {
|
|
4347
|
+
return "missing";
|
|
4348
|
+
}
|
|
4349
|
+
if (!stat.isSymbolicLink()) return "not-a-symlink";
|
|
4350
|
+
try {
|
|
4351
|
+
return readlinkSync(path) === target ? "ok" : "wrong-target";
|
|
4352
|
+
} catch {
|
|
4353
|
+
return "wrong-target";
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
function realOrSelf(path) {
|
|
4357
|
+
try {
|
|
4358
|
+
return realpathSync(path);
|
|
4359
|
+
} catch {
|
|
4360
|
+
return path;
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
function profileUnits(role) {
|
|
4364
|
+
return [
|
|
4365
|
+
`hermes-${role.agentId}-gateway.service`,
|
|
4366
|
+
`hermes-${role.agentId}-heartbeat.service`,
|
|
4367
|
+
`hermes-${role.agentId}-heartbeat.timer`,
|
|
4368
|
+
`hermes-${role.agentId}-checkpoint.service`
|
|
4369
|
+
];
|
|
4370
|
+
}
|
|
4371
|
+
function readRegistry(registryPath2) {
|
|
4372
|
+
const raw = safeReadText(registryPath2);
|
|
4373
|
+
if (raw === null) return null;
|
|
4374
|
+
try {
|
|
4375
|
+
const doc = YAML3.parse(raw);
|
|
4376
|
+
return doc?.agents ?? {};
|
|
4377
|
+
} catch {
|
|
4378
|
+
return null;
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
function declaredAgentIds(repoRoot) {
|
|
4382
|
+
return declaredAgentEntries(repoRoot).map(([agentId]) => agentId);
|
|
4383
|
+
}
|
|
4384
|
+
function declaredAgentEntries(repoRoot) {
|
|
4385
|
+
const raw = safeReadText(join14(repoRoot, ".project.json"));
|
|
4386
|
+
if (raw === null) return [];
|
|
4387
|
+
try {
|
|
4388
|
+
const doc = JSON.parse(raw);
|
|
4389
|
+
return Object.entries(doc.agents ?? {}).map(([agentId, entry]) => [agentId, entry ?? {}]);
|
|
4390
|
+
} catch {
|
|
4391
|
+
return [];
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
function ownedRegistryEntries(registry, repoRoot) {
|
|
4395
|
+
const want = realOrSelf(repoRoot);
|
|
4396
|
+
const owned = [];
|
|
4397
|
+
for (const [agentId, raw] of Object.entries(registry)) {
|
|
4398
|
+
const entry = raw ?? {};
|
|
4399
|
+
const roleDir = String(entry.role_dir ?? "");
|
|
4400
|
+
if (!roleDir) continue;
|
|
4401
|
+
if (realOrSelf(dirname6(dirname6(dirname6(roleDir)))) !== want) continue;
|
|
4402
|
+
owned.push([agentId, entry]);
|
|
4403
|
+
}
|
|
4404
|
+
return owned;
|
|
4405
|
+
}
|
|
4406
|
+
function unprovisionedRoleAgents(registry, repoRoot, canonical) {
|
|
4407
|
+
const blockers = /* @__PURE__ */ new Map();
|
|
4408
|
+
const record = (agentId, roleDir, source) => {
|
|
4409
|
+
const current = blockers.get(agentId) ?? { roleDir, sources: /* @__PURE__ */ new Set() };
|
|
4410
|
+
if (!current.roleDir && roleDir) current.roleDir = roleDir;
|
|
4411
|
+
current.sources.add(source);
|
|
4412
|
+
blockers.set(agentId, current);
|
|
4413
|
+
};
|
|
4414
|
+
for (const [agentId, entry] of ownedRegistryEntries(registry, repoRoot)) {
|
|
4415
|
+
if (canonical.has(agentId)) continue;
|
|
4416
|
+
const roleDir = String(entry.role_dir ?? "");
|
|
4417
|
+
if (!roleDir || !existsSync10(join14(roleDir, "role.yaml"))) record(agentId, roleDir, "registry");
|
|
4418
|
+
}
|
|
4419
|
+
for (const [agentId, entry] of declaredAgentEntries(repoRoot)) {
|
|
4420
|
+
if (canonical.has(agentId)) continue;
|
|
4421
|
+
const configured = String(entry.role_dir ?? "");
|
|
4422
|
+
const roleDir = configured ? resolve3(repoRoot, configured) : "";
|
|
4423
|
+
if (!roleDir || !existsSync10(join14(roleDir, "role.yaml"))) record(agentId, roleDir, ".project.json");
|
|
4424
|
+
}
|
|
4425
|
+
return [...blockers.entries()].map(([agentId, value]) => ({
|
|
4426
|
+
agentId,
|
|
4427
|
+
roleDir: value.roleDir,
|
|
4428
|
+
sources: [...value.sources]
|
|
4429
|
+
}));
|
|
4430
|
+
}
|
|
4431
|
+
function dropDeclaredAgent(ctx, agentId, changedFiles, details) {
|
|
4432
|
+
const path = join14(ctx.repoRoot, ".project.json");
|
|
4433
|
+
const raw = safeReadText(path);
|
|
4434
|
+
if (raw === null) return;
|
|
4435
|
+
let doc;
|
|
4436
|
+
try {
|
|
4437
|
+
doc = JSON.parse(raw);
|
|
4438
|
+
} catch {
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
if (!doc.agents || !(agentId in doc.agents)) return;
|
|
4442
|
+
delete doc.agents[agentId];
|
|
4443
|
+
details.push(`drop agent "${agentId}" from .project.json`);
|
|
4444
|
+
changedFiles.push(path);
|
|
4445
|
+
if (!ctx.dryRun) writeText(path, `${JSON.stringify(doc, null, 2)}
|
|
4446
|
+
`);
|
|
4447
|
+
}
|
|
4448
|
+
function rewriteLauncher(text3) {
|
|
4449
|
+
let next = text3.replace(/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m, 'HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"');
|
|
4450
|
+
next = next.replace(/^HERMES_OAUTH_FILE=.*\n/m, "");
|
|
4451
|
+
next = next.replace(/\s*HERMES_OAUTH_FILE="\$HERMES_OAUTH_FILE"/g, "");
|
|
4452
|
+
next = next.replace(
|
|
4453
|
+
/^.*\/home\/delorenj\/code\/hermes-agent\/\.venv\/bin\/hermes.*$/m,
|
|
4454
|
+
(line) => line.replace("/home/delorenj/code/hermes-agent/.venv/bin/hermes", "$HOME/.hermes/hermes-agent/.venv/bin/hermes")
|
|
4455
|
+
);
|
|
4456
|
+
return next;
|
|
4457
|
+
}
|
|
4458
|
+
function isValidOpReference(value) {
|
|
4459
|
+
if (!value.startsWith("op://") || /[\[\]{}<>]/.test(value)) return false;
|
|
4460
|
+
const withoutScheme = value.slice("op://".length);
|
|
4461
|
+
const fragmentIndex = withoutScheme.indexOf("#");
|
|
4462
|
+
if (fragmentIndex >= 0) return false;
|
|
4463
|
+
const queryIndex = withoutScheme.indexOf("?");
|
|
4464
|
+
const pathPart = queryIndex >= 0 ? withoutScheme.slice(0, queryIndex) : withoutScheme;
|
|
4465
|
+
const queryPart = queryIndex >= 0 ? withoutScheme.slice(queryIndex + 1) : "";
|
|
4466
|
+
const parts = pathPart.split("/");
|
|
4467
|
+
if (parts.length < 3 || parts.length > 4 || parts.some((part) => !part)) return false;
|
|
4468
|
+
try {
|
|
4469
|
+
for (const part of parts) decodeURIComponent(part);
|
|
4470
|
+
} catch {
|
|
4471
|
+
return false;
|
|
4472
|
+
}
|
|
4473
|
+
if (queryIndex >= 0 && !/^attribute=[A-Za-z0-9._~-]+$/.test(queryPart)) return false;
|
|
4474
|
+
return true;
|
|
4475
|
+
}
|
|
4476
|
+
function malformedOpReferences(text3) {
|
|
4477
|
+
const occurrences = [];
|
|
4478
|
+
const lines = text3.split("\n");
|
|
4479
|
+
for (let index = 0; index < lines.length; index++) {
|
|
4480
|
+
const line = lines[index];
|
|
4481
|
+
for (const match of line.matchAll(/op:\/\/[^\s"'`]+/g)) {
|
|
4482
|
+
const value = match[0];
|
|
4483
|
+
if (!isValidOpReference(value)) {
|
|
4484
|
+
occurrences.push({ line: index + 1, value, commentOnly: line.trimStart().startsWith("#") });
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
return occurrences;
|
|
4489
|
+
}
|
|
4490
|
+
function removeMalformedCommentOpReferences(text3) {
|
|
4491
|
+
let changed = false;
|
|
4492
|
+
const lines = text3.split("\n").map((line) => {
|
|
4493
|
+
if (!line.trimStart().startsWith("#")) return line;
|
|
4494
|
+
return line.replace(/op:\/\/[^\s"'`]+/g, (value) => {
|
|
4495
|
+
if (isValidOpReference(value)) return value;
|
|
4496
|
+
changed = true;
|
|
4497
|
+
return "<invalid 1Password reference removed by pjangler>";
|
|
4498
|
+
});
|
|
4499
|
+
});
|
|
4500
|
+
return { text: lines.join("\n"), changed };
|
|
4501
|
+
}
|
|
4502
|
+
var RULES = [
|
|
4503
|
+
{
|
|
4504
|
+
id: "mise.config-root",
|
|
4505
|
+
title: "mise config_root + AGENTS link hooks",
|
|
4506
|
+
audit: (ctx) => {
|
|
4507
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
4508
|
+
if (!existsSync10(misePath)) {
|
|
4509
|
+
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
4510
|
+
}
|
|
4511
|
+
const text3 = readText(misePath);
|
|
4512
|
+
const details = [];
|
|
4513
|
+
const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
4514
|
+
if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
4515
|
+
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
4516
|
+
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
4517
|
+
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
4518
|
+
if (!text3.includes("'{{config_root}}/.mise/scripts/link-agentfiles.sh'")) details.push("link-agentfiles hook must use single-quoted {{config_root}} guard");
|
|
4519
|
+
if (!text3.includes("op inject -i .env.op > .env")) details.push("hooks.enter must materialize .env from .env.op");
|
|
4520
|
+
if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
|
|
4521
|
+
if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
|
|
4522
|
+
return {
|
|
4523
|
+
id: "mise.config-root",
|
|
4524
|
+
title: "mise config_root + AGENTS link hooks",
|
|
4525
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
4526
|
+
summary: details.length === 0 ? "mise AGENTS-linking parity verified" : `${details.length} issue(s) detected in mise AGENTS-linking contract`,
|
|
4527
|
+
details,
|
|
4528
|
+
fixable: true
|
|
4529
|
+
};
|
|
4530
|
+
},
|
|
4531
|
+
migrate: (ctx, finding) => {
|
|
4532
|
+
const path = join14(ctx.repoRoot, "mise.toml");
|
|
4533
|
+
const changedFiles = [];
|
|
4534
|
+
const details = [];
|
|
4535
|
+
if (!existsSync10(path)) {
|
|
4536
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
4537
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
4538
|
+
}
|
|
4539
|
+
details.push("Initialized mise.toml from generated-project template");
|
|
4540
|
+
if (ctx.dryRun) {
|
|
4541
|
+
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
4542
|
+
}
|
|
4543
|
+
}
|
|
4544
|
+
let text3 = readText(path);
|
|
4545
|
+
const next = upsertLinkAgentfilesBlock(text3, ctx);
|
|
4546
|
+
if (next !== text3) {
|
|
4547
|
+
if (!changedFiles.includes(path)) changedFiles.push(path);
|
|
4548
|
+
if (!ctx.dryRun) writeText(path, next);
|
|
4549
|
+
text3 = next;
|
|
4550
|
+
}
|
|
4551
|
+
const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
4552
|
+
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
4553
|
+
if (expectedScript === void 0) {
|
|
4554
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/link-agentfiles.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
4555
|
+
}
|
|
4556
|
+
if (safeReadText(linkAgentfilesPath) !== expectedScript) {
|
|
4557
|
+
changedFiles.push(linkAgentfilesPath);
|
|
4558
|
+
if (!ctx.dryRun) {
|
|
4559
|
+
writeText(linkAgentfilesPath, expectedScript);
|
|
4560
|
+
chmodSync4(linkAgentfilesPath, 493);
|
|
3632
4561
|
}
|
|
3633
4562
|
}
|
|
3634
4563
|
return {
|
|
@@ -3646,9 +4575,9 @@ var RULES = [
|
|
|
3646
4575
|
title: "managed mise versioning block",
|
|
3647
4576
|
audit: (ctx) => {
|
|
3648
4577
|
const details = [];
|
|
3649
|
-
const misePath =
|
|
3650
|
-
const versioningPath =
|
|
3651
|
-
const manifestPath =
|
|
4578
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
4579
|
+
const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
4580
|
+
const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3652
4581
|
const text3 = safeReadText(misePath);
|
|
3653
4582
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
3654
4583
|
if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
@@ -3665,7 +4594,7 @@ var RULES = [
|
|
|
3665
4594
|
migrate: (ctx, finding) => {
|
|
3666
4595
|
const changedFiles = [];
|
|
3667
4596
|
const details = [];
|
|
3668
|
-
const misePath =
|
|
4597
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3669
4598
|
if (!existsSync10(misePath)) {
|
|
3670
4599
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3671
4600
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
@@ -3690,7 +4619,7 @@ var RULES = [
|
|
|
3690
4619
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3691
4620
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3692
4621
|
}
|
|
3693
|
-
const versioningPath =
|
|
4622
|
+
const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3694
4623
|
const expectedScript = templateVersioningScript(ctx);
|
|
3695
4624
|
if (expectedScript === void 0) {
|
|
3696
4625
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/versioning.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -3702,7 +4631,7 @@ var RULES = [
|
|
|
3702
4631
|
chmodSync4(versioningPath, 493);
|
|
3703
4632
|
}
|
|
3704
4633
|
}
|
|
3705
|
-
const manifestPath =
|
|
4634
|
+
const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3706
4635
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
3707
4636
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3708
4637
|
changedFiles.push(manifestPath);
|
|
@@ -3723,39 +4652,112 @@ var RULES = [
|
|
|
3723
4652
|
title: "Skillex project skills manifest",
|
|
3724
4653
|
audit: (ctx) => {
|
|
3725
4654
|
const details = [];
|
|
3726
|
-
const manifestPath =
|
|
3727
|
-
const legacyDir =
|
|
3728
|
-
const localExamplePath =
|
|
3729
|
-
const misePath =
|
|
4655
|
+
const manifestPath = join14(ctx.repoRoot, ".agents", "skills.json");
|
|
4656
|
+
const legacyDir = join14(ctx.repoRoot, ".agents", "skills");
|
|
4657
|
+
const localExamplePath = join14(ctx.repoRoot, ".agents", "local.example.json");
|
|
4658
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3730
4659
|
let fixable = true;
|
|
4660
|
+
let expectedBmad = [];
|
|
4661
|
+
try {
|
|
4662
|
+
expectedBmad = canonicalBmadSkillEntries(ctx);
|
|
4663
|
+
} catch (error) {
|
|
4664
|
+
details.push(
|
|
4665
|
+
`BMAD Skillex pack ${BMAD_PACK_VERSION} is not trusted at ${bmadPackRoot(ctx)}: ${error instanceof Error ? error.message : String(error)}`
|
|
4666
|
+
);
|
|
4667
|
+
fixable = false;
|
|
4668
|
+
}
|
|
4669
|
+
const expectedByName = new Map(expectedBmad.map((entry) => [entry.name, fileURLToPath3(entry.source)]));
|
|
4670
|
+
const expectedNames = new Set(expectedByName.keys());
|
|
4671
|
+
const packRoot = bmadPackRoot(ctx);
|
|
3731
4672
|
const manifest = tryParseJson(safeReadText(manifestPath));
|
|
3732
4673
|
if (!manifest) {
|
|
3733
4674
|
details.push(".agents/skills.json missing or invalid JSON");
|
|
3734
4675
|
} else {
|
|
3735
4676
|
if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
|
|
3736
4677
|
if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
|
|
3737
|
-
if (!Array.isArray(manifest.skills))
|
|
4678
|
+
if (!Array.isArray(manifest.skills)) {
|
|
4679
|
+
details.push(".agents/skills.json should define a skills array");
|
|
4680
|
+
} else {
|
|
4681
|
+
const actualBmad = new Map(
|
|
4682
|
+
manifest.skills.filter((entry) => Boolean(entry) && typeof entry === "object" && isPackManagedManifestEntry(entry, expectedNames, packRoot)).map((entry) => [String(entry.name), String(entry.source ?? "")])
|
|
4683
|
+
);
|
|
4684
|
+
const stale = expectedBmad.filter((entry) => actualBmad.get(entry.name) !== entry.source);
|
|
4685
|
+
if (stale.length > 0 || actualBmad.size !== expectedBmad.length) {
|
|
4686
|
+
details.push(`.agents/skills.json should record all ${expectedBmad.length} BMAD ${BMAD_PACK_VERSION} pack entries as file:// sources`);
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
3738
4689
|
}
|
|
4690
|
+
const invalidBmadLinkNames = /* @__PURE__ */ new Set();
|
|
3739
4691
|
if (existsSync10(legacyDir)) {
|
|
3740
|
-
const
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
4692
|
+
for (const name of readdirSync3(legacyDir)) {
|
|
4693
|
+
const expected = expectedByName.get(name);
|
|
4694
|
+
const path = join14(legacyDir, name);
|
|
4695
|
+
let linkTargetsPack = false;
|
|
4696
|
+
try {
|
|
4697
|
+
linkTargetsPack = lstatSync2(path).isSymbolicLink() && isContainedBy(packRoot, resolve3(dirname6(path), readlinkSync(path)));
|
|
4698
|
+
} catch {
|
|
4699
|
+
linkTargetsPack = false;
|
|
4700
|
+
}
|
|
4701
|
+
if (!expected && !linkTargetsPack) continue;
|
|
4702
|
+
try {
|
|
4703
|
+
if (!expected || !lstatSync2(path).isSymbolicLink() || resolve3(dirname6(path), readlinkSync(path)) !== expected) invalidBmadLinkNames.add(name);
|
|
4704
|
+
} catch {
|
|
4705
|
+
invalidBmadLinkNames.add(name);
|
|
4706
|
+
}
|
|
3746
4707
|
}
|
|
4708
|
+
for (const [name, expected] of expectedByName) {
|
|
4709
|
+
const path = join14(legacyDir, name);
|
|
4710
|
+
try {
|
|
4711
|
+
if (!lstatSync2(path).isSymbolicLink() || resolve3(dirname6(path), readlinkSync(path)) !== expected) invalidBmadLinkNames.add(name);
|
|
4712
|
+
} catch {
|
|
4713
|
+
invalidBmadLinkNames.add(name);
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
} else {
|
|
4717
|
+
for (const name of expectedByName.keys()) invalidBmadLinkNames.add(name);
|
|
4718
|
+
}
|
|
4719
|
+
if (invalidBmadLinkNames.size > 0) {
|
|
4720
|
+
details.push(`${invalidBmadLinkNames.size} managed BMAD skill path(s) should be symlinks into the ${BMAD_PACK_VERSION} pack`);
|
|
3747
4721
|
}
|
|
3748
4722
|
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3749
|
-
if (existsSync10(
|
|
4723
|
+
if (existsSync10(join14(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
|
|
3750
4724
|
}
|
|
3751
4725
|
const localExample = tryParseJson(safeReadText(localExamplePath));
|
|
3752
4726
|
if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
|
|
3753
4727
|
details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
|
|
3754
4728
|
}
|
|
3755
4729
|
const mise = safeReadText(misePath);
|
|
3756
|
-
if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run sync-skills.py
|
|
4730
|
+
if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run the shipped project-local sync-skills.py engine via config_root");
|
|
4731
|
+
if (!mise?.includes(PROVISION_BMAD_SKILLS_SCRIPT)) details.push("mise.toml should provision pinned BMAD pack links before syncing skills");
|
|
4732
|
+
if (mise?.includes(SYNC_SKILLS_SCRIPT) && mise.includes(PROVISION_BMAD_SKILLS_SCRIPT) && mise.indexOf(PROVISION_BMAD_SKILLS_SCRIPT) > mise.indexOf(SYNC_SKILLS_SCRIPT)) {
|
|
4733
|
+
details.push("mise.toml should run the BMAD provisioner before project skill sync");
|
|
4734
|
+
}
|
|
4735
|
+
if (mise?.includes('script = "sync-skills.py --scope project"') || mise?.includes('run = "sync-skills.py --scope project"')) {
|
|
4736
|
+
details.push("mise.toml still invokes the missing bare sync-skills.py executable");
|
|
4737
|
+
}
|
|
3757
4738
|
if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
|
|
3758
4739
|
if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
|
|
4740
|
+
if (!mise?.includes('depends = ["skills-provision-bmad"]')) details.push("skills-sync task should depend on skills-provision-bmad");
|
|
4741
|
+
for (const [rel, label] of [
|
|
4742
|
+
[".mise/scripts/provision-bmad-skills.py", "BMAD Skillex provisioning script"],
|
|
4743
|
+
[".mise/scripts/sync-skills.py", "Project-local skills sync engine"]
|
|
4744
|
+
]) {
|
|
4745
|
+
const target = join14(ctx.repoRoot, rel);
|
|
4746
|
+
const expected = templateCommonProjectText(ctx, rel);
|
|
4747
|
+
const stat = lstatIfPresent(target);
|
|
4748
|
+
if (!stat || !stat.isFile() || stat.isSymbolicLink()) {
|
|
4749
|
+
details.push(`${label} is missing or unsafe`);
|
|
4750
|
+
if (stat) fixable = false;
|
|
4751
|
+
} else {
|
|
4752
|
+
if (expected === void 0 || safeReadText(target) !== expected) details.push(`${label} differs from the shipped template`);
|
|
4753
|
+
if ((Number(stat.mode) & 73) === 0) details.push(`${label} is not executable`);
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
const topologyIssues = projectSkillTopologyIssues(ctx.repoRoot);
|
|
4757
|
+
if (topologyIssues.length) {
|
|
4758
|
+
details.push(...topologyIssues.map((issue) => `CLI skill topology: ${issue}`));
|
|
4759
|
+
fixable = false;
|
|
4760
|
+
}
|
|
3759
4761
|
if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
|
|
3760
4762
|
details.push("mise.toml still contains legacy skill-link wiring");
|
|
3761
4763
|
}
|
|
@@ -3771,33 +4773,66 @@ var RULES = [
|
|
|
3771
4773
|
migrate: (ctx, finding) => {
|
|
3772
4774
|
const changedFiles = [];
|
|
3773
4775
|
const details = [];
|
|
3774
|
-
const manifestPath =
|
|
3775
|
-
const
|
|
3776
|
-
const
|
|
3777
|
-
const
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
if (!ctx.dryRun) rmSync(legacyDir, { recursive: true, force: true });
|
|
4776
|
+
const manifestPath = join14(ctx.repoRoot, ".agents", "skills.json");
|
|
4777
|
+
const localExamplePath = join14(ctx.repoRoot, ".agents", "local.example.json");
|
|
4778
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
4779
|
+
const provisionScriptPath = join14(ctx.repoRoot, ".mise", "scripts", "provision-bmad-skills.py");
|
|
4780
|
+
const syncScriptPath = join14(ctx.repoRoot, ".mise", "scripts", "sync-skills.py");
|
|
4781
|
+
const expectedProvisionScript = templateCommonProjectText(ctx, ".mise/scripts/provision-bmad-skills.py");
|
|
4782
|
+
const expectedSyncScript = templateCommonProjectText(ctx, ".mise/scripts/sync-skills.py");
|
|
4783
|
+
const topologyIssues = projectSkillTopologyIssues(ctx.repoRoot);
|
|
4784
|
+
if (topologyIssues.length) {
|
|
4785
|
+
return {
|
|
4786
|
+
id: finding.id,
|
|
4787
|
+
title: finding.title,
|
|
4788
|
+
status: "blocked",
|
|
4789
|
+
summary: "Unsafe project CLI skill topology must be repaired manually",
|
|
4790
|
+
changedFiles,
|
|
4791
|
+
details: topologyIssues
|
|
4792
|
+
};
|
|
3792
4793
|
}
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
4794
|
+
if (!expectedProvisionScript || !expectedSyncScript) {
|
|
4795
|
+
return {
|
|
4796
|
+
id: finding.id,
|
|
4797
|
+
title: finding.title,
|
|
4798
|
+
status: "blocked",
|
|
4799
|
+
summary: "pjangler install is missing a shipped skills executable",
|
|
4800
|
+
changedFiles,
|
|
4801
|
+
details: [
|
|
4802
|
+
...!expectedProvisionScript ? ["Missing BMAD Skillex provisioning script template"] : [],
|
|
4803
|
+
...!expectedSyncScript ? ["Missing project-local skills sync engine template"] : []
|
|
4804
|
+
]
|
|
4805
|
+
};
|
|
4806
|
+
}
|
|
4807
|
+
const unsafeScriptTargets = [provisionScriptPath, syncScriptPath].filter((path) => {
|
|
4808
|
+
const stat = lstatIfPresent(path);
|
|
4809
|
+
return Boolean(stat && (!stat.isFile() || stat.isSymbolicLink()));
|
|
4810
|
+
});
|
|
4811
|
+
if (unsafeScriptTargets.length) {
|
|
4812
|
+
return {
|
|
4813
|
+
id: finding.id,
|
|
4814
|
+
title: finding.title,
|
|
4815
|
+
status: "blocked",
|
|
4816
|
+
summary: "Refusing non-regular managed skills executable target",
|
|
4817
|
+
changedFiles,
|
|
4818
|
+
details: unsafeScriptTargets.map((path) => `${path} must be removed or repaired manually`)
|
|
4819
|
+
};
|
|
4820
|
+
}
|
|
4821
|
+
const provisioned = provisionBmadSkills(ctx);
|
|
4822
|
+
if (!provisioned.ok) {
|
|
4823
|
+
return {
|
|
4824
|
+
id: finding.id,
|
|
4825
|
+
title: finding.title,
|
|
4826
|
+
status: "blocked",
|
|
4827
|
+
summary: `BMAD Skillex pack ${BMAD_PACK_VERSION} is unavailable`,
|
|
4828
|
+
changedFiles,
|
|
4829
|
+
details: [provisioned.error ?? "Unknown BMAD pack error"]
|
|
4830
|
+
};
|
|
3798
4831
|
}
|
|
4832
|
+
changedFiles.push(...provisioned.changedFiles);
|
|
4833
|
+
if (provisioned.changedFiles.includes(manifestPath)) details.push(`Recorded BMAD pack ${BMAD_PACK_VERSION} in .agents/skills.json`);
|
|
3799
4834
|
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3800
|
-
const path =
|
|
4835
|
+
const path = join14(ctx.repoRoot, rel);
|
|
3801
4836
|
if (existsSync10(path)) {
|
|
3802
4837
|
changedFiles.push(path);
|
|
3803
4838
|
if (!ctx.dryRun) unlinkSync3(path);
|
|
@@ -3809,6 +4844,8 @@ var RULES = [
|
|
|
3809
4844
|
changedFiles.push(localExamplePath);
|
|
3810
4845
|
if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
|
|
3811
4846
|
}
|
|
4847
|
+
normalizeExecutableTemplate(ctx, provisionScriptPath, expectedProvisionScript, changedFiles);
|
|
4848
|
+
normalizeExecutableTemplate(ctx, syncScriptPath, expectedSyncScript, changedFiles);
|
|
3812
4849
|
if (!existsSync10(misePath)) {
|
|
3813
4850
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3814
4851
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details };
|
|
@@ -3834,9 +4871,9 @@ var RULES = [
|
|
|
3834
4871
|
id: "sot.agent-symlinks",
|
|
3835
4872
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
3836
4873
|
audit: (ctx) => {
|
|
3837
|
-
const agentsPath =
|
|
4874
|
+
const agentsPath = join14(ctx.repoRoot, "AGENTS.md");
|
|
3838
4875
|
if (!existsSync10(agentsPath)) {
|
|
3839
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(
|
|
4876
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join14(ctx.repoRoot, file)));
|
|
3840
4877
|
if (fallbackSources.length === 0) {
|
|
3841
4878
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
3842
4879
|
}
|
|
@@ -3851,7 +4888,7 @@ var RULES = [
|
|
|
3851
4888
|
}
|
|
3852
4889
|
const details = [];
|
|
3853
4890
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3854
|
-
const full =
|
|
4891
|
+
const full = join14(ctx.repoRoot, file);
|
|
3855
4892
|
const target = readSymlinkTarget(full);
|
|
3856
4893
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
3857
4894
|
}
|
|
@@ -3875,7 +4912,7 @@ var RULES = [
|
|
|
3875
4912
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
3876
4913
|
}
|
|
3877
4914
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3878
|
-
const full =
|
|
4915
|
+
const full = join14(ctx.repoRoot, file);
|
|
3879
4916
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
3880
4917
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
3881
4918
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3897,7 +4934,7 @@ var RULES = [
|
|
|
3897
4934
|
migrate: (ctx, finding) => {
|
|
3898
4935
|
const changedFiles = [];
|
|
3899
4936
|
const details = [];
|
|
3900
|
-
const path =
|
|
4937
|
+
const path = join14(ctx.repoRoot, ".project.json");
|
|
3901
4938
|
const existing = readProjectJson(ctx) ?? {};
|
|
3902
4939
|
const canonical = canonicalProjectJson(ctx);
|
|
3903
4940
|
const merged = { ...existing, ...canonical };
|
|
@@ -3907,11 +4944,11 @@ var RULES = [
|
|
|
3907
4944
|
changedFiles.push(path);
|
|
3908
4945
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3909
4946
|
}
|
|
3910
|
-
const planeJson =
|
|
4947
|
+
const planeJson = join14(ctx.repoRoot, ".plane.json");
|
|
3911
4948
|
if (existsSync10(planeJson)) {
|
|
3912
4949
|
const backup = `${planeJson}.migrated-backup`;
|
|
3913
4950
|
if (existsSync10(backup)) {
|
|
3914
|
-
details.push(`cannot back up .plane.json because ${
|
|
4951
|
+
details.push(`cannot back up .plane.json because ${relative2(ctx.repoRoot, backup)} already exists`);
|
|
3915
4952
|
} else {
|
|
3916
4953
|
changedFiles.push(backup);
|
|
3917
4954
|
if (!ctx.dryRun) renameSync2(planeJson, backup);
|
|
@@ -3932,17 +4969,21 @@ var RULES = [
|
|
|
3932
4969
|
title: ".env.op + gitignore secrets contract",
|
|
3933
4970
|
audit: (ctx) => {
|
|
3934
4971
|
const details = [];
|
|
3935
|
-
const envOp = safeReadText(
|
|
3936
|
-
const gitignore = safeReadText(
|
|
4972
|
+
const envOp = safeReadText(join14(ctx.repoRoot, ".env.op"));
|
|
4973
|
+
const gitignore = safeReadText(join14(ctx.repoRoot, ".gitignore"));
|
|
3937
4974
|
if (!envOp) {
|
|
3938
4975
|
details.push(".env.op missing");
|
|
3939
4976
|
} else {
|
|
3940
|
-
const
|
|
4977
|
+
const malformed = malformedOpReferences(envOp);
|
|
4978
|
+
if (malformed.length) {
|
|
4979
|
+
details.push(`.env.op has malformed op:// reference(s) on line(s): ${Array.from(new Set(malformed.map((entry) => entry.line))).join(", ")}`);
|
|
4980
|
+
}
|
|
4981
|
+
const invalidLines = envOp.split("\n").map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(({ line }) => line && !line.startsWith("#") && line.includes("=")).filter(({ line }) => {
|
|
3941
4982
|
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
3942
4983
|
const quotedLiteral = /^"[^"\r\n]*"$/.test(value) || /^'[^'\r\n]*'$/.test(value);
|
|
3943
4984
|
return !value.startsWith("op://") && !/^https?:\/\//.test(value) && !/^[A-Za-z0-9_.:-]+$/.test(value) && !quotedLiteral;
|
|
3944
4985
|
});
|
|
3945
|
-
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals: ${invalidLines.join(", ")}`);
|
|
4986
|
+
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals on line(s): ${invalidLines.map((entry) => entry.number).join(", ")}`);
|
|
3946
4987
|
}
|
|
3947
4988
|
if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
|
|
3948
4989
|
if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
|
|
@@ -3959,12 +5000,23 @@ var RULES = [
|
|
|
3959
5000
|
migrate: (ctx, finding) => {
|
|
3960
5001
|
const changedFiles = [];
|
|
3961
5002
|
const details = [];
|
|
3962
|
-
const envOpPath =
|
|
5003
|
+
const envOpPath = join14(ctx.repoRoot, ".env.op");
|
|
3963
5004
|
if (!existsSync10(envOpPath)) {
|
|
3964
5005
|
changedFiles.push(envOpPath);
|
|
3965
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
5006
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join14(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
5007
|
+
} else {
|
|
5008
|
+
const current = readText(envOpPath);
|
|
5009
|
+
const repaired = removeMalformedCommentOpReferences(current);
|
|
5010
|
+
if (repaired.changed) {
|
|
5011
|
+
changedFiles.push(envOpPath);
|
|
5012
|
+
if (!ctx.dryRun) writeText(envOpPath, repaired.text);
|
|
5013
|
+
}
|
|
5014
|
+
const remaining = malformedOpReferences(repaired.text).filter((entry) => !entry.commentOnly);
|
|
5015
|
+
if (remaining.length) {
|
|
5016
|
+
details.push(`Malformed active op:// reference(s) remain on line(s) ${Array.from(new Set(remaining.map((entry) => entry.line))).join(", ")}; repair them manually without replacing valid user references`);
|
|
5017
|
+
}
|
|
3966
5018
|
}
|
|
3967
|
-
const gitignorePath =
|
|
5019
|
+
const gitignorePath = join14(ctx.repoRoot, ".gitignore");
|
|
3968
5020
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3969
5021
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3970
5022
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3991,7 +5043,7 @@ var RULES = [
|
|
|
3991
5043
|
title: ".copier-answers.yml provenance + drift report",
|
|
3992
5044
|
audit: (ctx) => {
|
|
3993
5045
|
const details = [];
|
|
3994
|
-
const path =
|
|
5046
|
+
const path = join14(ctx.repoRoot, ".copier-answers.yml");
|
|
3995
5047
|
const text3 = safeReadText(path);
|
|
3996
5048
|
const project = readProjectJson(ctx);
|
|
3997
5049
|
if (!text3) {
|
|
@@ -4022,12 +5074,12 @@ var RULES = [
|
|
|
4022
5074
|
const changedFiles = [];
|
|
4023
5075
|
const project = canonicalProjectJson(ctx);
|
|
4024
5076
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
4025
|
-
_src_path: ${
|
|
5077
|
+
_src_path: ${join14(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
4026
5078
|
project_description: ${String(project.project_description)}
|
|
4027
5079
|
project_name: ${String(project.project_name)}
|
|
4028
5080
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
4029
5081
|
`;
|
|
4030
|
-
const path =
|
|
5082
|
+
const path = join14(ctx.repoRoot, ".copier-answers.yml");
|
|
4031
5083
|
if (safeReadText(path) !== text3) {
|
|
4032
5084
|
changedFiles.push(path);
|
|
4033
5085
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -4046,14 +5098,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4046
5098
|
id: "bmad.scaffold",
|
|
4047
5099
|
title: "BMAD modules/docs scaffold",
|
|
4048
5100
|
audit: (ctx) => {
|
|
4049
|
-
const
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
5101
|
+
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
5102
|
+
if (manifestSelection.status === "invalid") {
|
|
5103
|
+
return {
|
|
5104
|
+
id: "bmad.scaffold",
|
|
5105
|
+
title: "BMAD modules/docs scaffold",
|
|
5106
|
+
status: "fail",
|
|
5107
|
+
summary: "BMAD module manifest is invalid; refusing fallback module selection",
|
|
5108
|
+
details: [manifestSelection.error],
|
|
5109
|
+
fixable: false
|
|
5110
|
+
};
|
|
5111
|
+
}
|
|
5112
|
+
const targetRoot = join14(ctx.repoRoot, "_bmad");
|
|
5113
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
5114
|
+
const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
|
|
5115
|
+
const missing = sentinels.filter((file) => !existsSync10(join14(targetRoot, file)));
|
|
4057
5116
|
return {
|
|
4058
5117
|
id: "bmad.scaffold",
|
|
4059
5118
|
title: "BMAD modules/docs scaffold",
|
|
@@ -4065,9 +5124,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4065
5124
|
},
|
|
4066
5125
|
migrate: (ctx, finding) => {
|
|
4067
5126
|
const changedFiles = [];
|
|
5127
|
+
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
5128
|
+
if (manifestSelection.status === "invalid") {
|
|
5129
|
+
return {
|
|
5130
|
+
id: finding.id,
|
|
5131
|
+
title: finding.title,
|
|
5132
|
+
status: "blocked",
|
|
5133
|
+
summary: "BMAD module manifest is invalid; refusing fallback module selection",
|
|
5134
|
+
changedFiles,
|
|
5135
|
+
details: [manifestSelection.error]
|
|
5136
|
+
};
|
|
5137
|
+
}
|
|
5138
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
4068
5139
|
if (ctx.dryRun) {
|
|
4069
5140
|
for (const detail of finding.details) {
|
|
4070
|
-
changedFiles.push(
|
|
5141
|
+
changedFiles.push(join14(ctx.repoRoot, detail));
|
|
4071
5142
|
}
|
|
4072
5143
|
return {
|
|
4073
5144
|
id: finding.id,
|
|
@@ -4076,11 +5147,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4076
5147
|
summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
|
|
4077
5148
|
changedFiles,
|
|
4078
5149
|
details: [
|
|
4079
|
-
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
5150
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot, selectedModules).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
4080
5151
|
]
|
|
4081
5152
|
};
|
|
4082
5153
|
}
|
|
4083
|
-
const
|
|
5154
|
+
const preservedSkillsManifest = tryParseJson(
|
|
5155
|
+
safeReadText(join14(ctx.repoRoot, ".agents", "skills.json"))
|
|
5156
|
+
);
|
|
5157
|
+
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
4084
5158
|
if (!install.ok) {
|
|
4085
5159
|
return {
|
|
4086
5160
|
id: finding.id,
|
|
@@ -4091,16 +5165,28 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4091
5165
|
details: [install.error ?? "Unknown error"]
|
|
4092
5166
|
};
|
|
4093
5167
|
}
|
|
5168
|
+
const provisioned = provisionBmadSkills(ctx, preservedSkillsManifest);
|
|
5169
|
+
if (!provisioned.ok) {
|
|
5170
|
+
return {
|
|
5171
|
+
id: finding.id,
|
|
5172
|
+
title: finding.title,
|
|
5173
|
+
status: "blocked",
|
|
5174
|
+
summary: `BMAD installed but Skillex pack ${BMAD_PACK_VERSION} provisioning failed`,
|
|
5175
|
+
changedFiles: [],
|
|
5176
|
+
details: [provisioned.error ?? "Unknown BMAD pack error"]
|
|
5177
|
+
};
|
|
5178
|
+
}
|
|
4094
5179
|
for (const detail of finding.details) {
|
|
4095
|
-
if (existsSync10(
|
|
4096
|
-
changedFiles.push(
|
|
5180
|
+
if (existsSync10(join14(ctx.repoRoot, detail))) {
|
|
5181
|
+
changedFiles.push(join14(ctx.repoRoot, detail));
|
|
4097
5182
|
}
|
|
4098
5183
|
}
|
|
5184
|
+
changedFiles.push(...provisioned.changedFiles);
|
|
4099
5185
|
return {
|
|
4100
5186
|
id: finding.id,
|
|
4101
5187
|
title: finding.title,
|
|
4102
5188
|
status: changedFiles.length ? "applied" : "noop",
|
|
4103
|
-
summary: changedFiles.length ?
|
|
5189
|
+
summary: changedFiles.length ? `Installed BMAD scaffold with Skillex pack ${BMAD_PACK_VERSION} skills` : "No changes required",
|
|
4104
5190
|
changedFiles,
|
|
4105
5191
|
details: []
|
|
4106
5192
|
};
|
|
@@ -4116,7 +5202,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4116
5202
|
id: "bmad.version",
|
|
4117
5203
|
title: "BMAD version currency",
|
|
4118
5204
|
status: "skip",
|
|
4119
|
-
summary: existsSync10(
|
|
5205
|
+
summary: existsSync10(join14(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
4120
5206
|
details: [],
|
|
4121
5207
|
fixable: false
|
|
4122
5208
|
};
|
|
@@ -4171,7 +5257,19 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4171
5257
|
}
|
|
4172
5258
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
4173
5259
|
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
4174
|
-
const manifestPath =
|
|
5260
|
+
const manifestPath = join14(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
5261
|
+
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
5262
|
+
if (manifestSelection.status === "invalid") {
|
|
5263
|
+
return {
|
|
5264
|
+
id: finding.id,
|
|
5265
|
+
title: finding.title,
|
|
5266
|
+
status: "blocked",
|
|
5267
|
+
summary: "BMAD module manifest is invalid; refusing fallback module selection",
|
|
5268
|
+
changedFiles: [],
|
|
5269
|
+
details: [manifestSelection.error]
|
|
5270
|
+
};
|
|
5271
|
+
}
|
|
5272
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
4175
5273
|
if (ctx.dryRun) {
|
|
4176
5274
|
return {
|
|
4177
5275
|
id: finding.id,
|
|
@@ -4180,11 +5278,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4180
5278
|
summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
|
|
4181
5279
|
changedFiles: [manifestPath],
|
|
4182
5280
|
details: [
|
|
4183
|
-
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
5281
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot, selectedModules).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
4184
5282
|
]
|
|
4185
5283
|
};
|
|
4186
5284
|
}
|
|
4187
|
-
const
|
|
5285
|
+
const preservedSkillsManifest = tryParseJson(
|
|
5286
|
+
safeReadText(join14(ctx.repoRoot, ".agents", "skills.json"))
|
|
5287
|
+
);
|
|
5288
|
+
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
4188
5289
|
if (!install.ok) {
|
|
4189
5290
|
return {
|
|
4190
5291
|
id: finding.id,
|
|
@@ -4195,6 +5296,17 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4195
5296
|
details: [install.error ?? "Unknown error"]
|
|
4196
5297
|
};
|
|
4197
5298
|
}
|
|
5299
|
+
const provisioned = provisionBmadSkills(ctx, preservedSkillsManifest);
|
|
5300
|
+
if (!provisioned.ok) {
|
|
5301
|
+
return {
|
|
5302
|
+
id: finding.id,
|
|
5303
|
+
title: finding.title,
|
|
5304
|
+
status: "blocked",
|
|
5305
|
+
summary: `BMAD upgraded but Skillex pack ${BMAD_PACK_VERSION} provisioning failed`,
|
|
5306
|
+
changedFiles: [],
|
|
5307
|
+
details: [provisioned.error ?? "Unknown BMAD pack error"]
|
|
5308
|
+
};
|
|
5309
|
+
}
|
|
4198
5310
|
const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
|
|
4199
5311
|
const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
|
|
4200
5312
|
return {
|
|
@@ -4202,7 +5314,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4202
5314
|
title: finding.title,
|
|
4203
5315
|
status: upgraded ? "applied" : "noop",
|
|
4204
5316
|
summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
|
|
4205
|
-
changedFiles:
|
|
5317
|
+
changedFiles: Array.from(/* @__PURE__ */ new Set([
|
|
5318
|
+
...upgraded ? [manifestPath] : [],
|
|
5319
|
+
...provisioned.changedFiles
|
|
5320
|
+
])),
|
|
4206
5321
|
details: []
|
|
4207
5322
|
};
|
|
4208
5323
|
}
|
|
@@ -4217,12 +5332,13 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4217
5332
|
return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
|
|
4218
5333
|
}
|
|
4219
5334
|
const details = [];
|
|
4220
|
-
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md"
|
|
4221
|
-
if (!existsSync10(
|
|
5335
|
+
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md"]) {
|
|
5336
|
+
if (!existsSync10(join14(role.roleDir, rel))) details.push(`missing ${relative2(ctx.repoRoot, join14(role.roleDir, rel))}`);
|
|
5337
|
+
}
|
|
5338
|
+
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
|
|
5339
|
+
details.push(".gitmodules contains retired pm runtime submodule mapping");
|
|
4222
5340
|
}
|
|
4223
|
-
|
|
4224
|
-
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
4225
|
-
if (!profileMetaInheritsDefault(join13(role.roleDir, "runtime", "profile.yaml"))) {
|
|
5341
|
+
if (!profileMetaInheritsDefault(join14(role.roleDir, "runtime", "profile.yaml"))) {
|
|
4226
5342
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
4227
5343
|
}
|
|
4228
5344
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -4243,21 +5359,32 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4243
5359
|
if (!role) {
|
|
4244
5360
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
4245
5361
|
}
|
|
4246
|
-
const
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
5362
|
+
const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
5363
|
+
details.push(...retirement.details);
|
|
5364
|
+
if (!retirement.ok) {
|
|
5365
|
+
return {
|
|
5366
|
+
id: finding.id,
|
|
5367
|
+
title: finding.title,
|
|
5368
|
+
status: "blocked",
|
|
5369
|
+
summary: "Failed to retire PM runtime submodule metadata safely",
|
|
5370
|
+
changedFiles,
|
|
5371
|
+
details: [retirement.error ?? "unknown runtime retirement failure"]
|
|
5372
|
+
};
|
|
5373
|
+
}
|
|
5374
|
+
const templateRoleDir = join14(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
5375
|
+
writeIfDifferent(join14(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
5376
|
+
writeIfDifferent(join14(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
5377
|
+
writeIfDifferent(join14(role.roleDir, ".gitignore"), readText(join14(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
5378
|
+
copyMissingRecursive(join14(templateRoleDir, ".runtime-scaffold"), join14(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
5379
|
+
copyMissingRecursive(join14(templateRoleDir, ".runtime-scaffold"), join14(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
5380
|
+
copyMissingRecursive(join14(templateRoleDir, ".scripts"), join14(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
5381
|
+
const promptSource = join14(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
5382
|
+
const promptTarget = join14(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
4255
5383
|
if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
|
|
4256
5384
|
const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo).replace(/\{\{ display_name \}\}/g, role.displayName || role.agentId);
|
|
4257
5385
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
4258
5386
|
}
|
|
4259
|
-
|
|
4260
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(join13(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
5387
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join14(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
4261
5388
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
4262
5389
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
4263
5390
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -4288,8 +5415,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4288
5415
|
}
|
|
4289
5416
|
const details = [];
|
|
4290
5417
|
for (const role of roles) {
|
|
4291
|
-
const roleRelDir =
|
|
4292
|
-
const runtimeRelPath =
|
|
5418
|
+
const roleRelDir = relative2(ctx.repoRoot, role.roleDir);
|
|
5419
|
+
const runtimeRelPath = join14(roleRelDir, "runtime");
|
|
4293
5420
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
4294
5421
|
cwd: ctx.repoRoot,
|
|
4295
5422
|
encoding: "utf8"
|
|
@@ -4297,15 +5424,18 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4297
5424
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
4298
5425
|
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
4299
5426
|
}
|
|
4300
|
-
|
|
5427
|
+
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
|
|
5428
|
+
details.push(`stale .gitmodules mapping exists for ${runtimeRelPath}`);
|
|
5429
|
+
}
|
|
5430
|
+
const gitignorePath = join14(role.roleDir, ".gitignore");
|
|
4301
5431
|
if (existsSync10(gitignorePath)) {
|
|
4302
5432
|
const content = safeReadText(gitignorePath) ?? "";
|
|
4303
5433
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
4304
5434
|
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
4305
|
-
details.push(`.gitignore missing runtime/ ignore entry in ${
|
|
5435
|
+
details.push(`.gitignore missing runtime/ ignore entry in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
4306
5436
|
}
|
|
4307
5437
|
} else {
|
|
4308
|
-
details.push(`.gitignore is missing in ${
|
|
5438
|
+
details.push(`.gitignore is missing in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
4309
5439
|
}
|
|
4310
5440
|
}
|
|
4311
5441
|
return {
|
|
@@ -4322,23 +5452,19 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4322
5452
|
const changedFiles = [];
|
|
4323
5453
|
const details = [];
|
|
4324
5454
|
for (const role of roles) {
|
|
4325
|
-
const
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
cwd: ctx.repoRoot,
|
|
4337
|
-
encoding: "utf8"
|
|
4338
|
-
});
|
|
4339
|
-
}
|
|
5455
|
+
const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
5456
|
+
details.push(...retirement.details);
|
|
5457
|
+
if (!retirement.ok) {
|
|
5458
|
+
return {
|
|
5459
|
+
id: finding.id,
|
|
5460
|
+
title: finding.title,
|
|
5461
|
+
status: "blocked",
|
|
5462
|
+
summary: "Failed to retire Hermes runtime submodule metadata safely",
|
|
5463
|
+
changedFiles,
|
|
5464
|
+
details: [retirement.error ?? "unknown runtime retirement failure"]
|
|
5465
|
+
};
|
|
4340
5466
|
}
|
|
4341
|
-
const gitignorePath =
|
|
5467
|
+
const gitignorePath = join14(role.roleDir, ".gitignore");
|
|
4342
5468
|
let content = "";
|
|
4343
5469
|
let isIgnored = false;
|
|
4344
5470
|
if (existsSync10(gitignorePath)) {
|
|
@@ -4347,7 +5473,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4347
5473
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
4348
5474
|
}
|
|
4349
5475
|
if (!isIgnored) {
|
|
4350
|
-
details.push(`ignore runtime/ in ${
|
|
5476
|
+
details.push(`ignore runtime/ in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
4351
5477
|
changedFiles.push(gitignorePath);
|
|
4352
5478
|
if (!ctx.dryRun) {
|
|
4353
5479
|
if (content && !content.endsWith("\n")) {
|
|
@@ -4382,7 +5508,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4382
5508
|
}
|
|
4383
5509
|
const details = [];
|
|
4384
5510
|
for (const role of roles) {
|
|
4385
|
-
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-
|
|
5511
|
+
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
|
|
4386
5512
|
const state = checkUnit(unit);
|
|
4387
5513
|
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
4388
5514
|
}
|
|
@@ -4408,9 +5534,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4408
5534
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
4409
5535
|
}
|
|
4410
5536
|
for (const role of roles) {
|
|
4411
|
-
const sysDir =
|
|
4412
|
-
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-
|
|
4413
|
-
const allUnitsPresent = units.every((unit) => existsSync10(
|
|
5537
|
+
const sysDir = join14(ctx.homeDir, ".config", "systemd", "user");
|
|
5538
|
+
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
5539
|
+
const allUnitsPresent = units.every((unit) => existsSync10(join14(sysDir, unit)));
|
|
4414
5540
|
if (allUnitsPresent) {
|
|
4415
5541
|
if (ctx.dryRun) {
|
|
4416
5542
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -4422,7 +5548,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4422
5548
|
}
|
|
4423
5549
|
continue;
|
|
4424
5550
|
}
|
|
4425
|
-
for (const script of [
|
|
5551
|
+
for (const script of [join14(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
4426
5552
|
if (!script || !existsSync10(script)) continue;
|
|
4427
5553
|
if (ctx.dryRun) {
|
|
4428
5554
|
details.push(`would run: bash ${script}`);
|
|
@@ -4441,6 +5567,349 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4441
5567
|
details
|
|
4442
5568
|
};
|
|
4443
5569
|
}
|
|
5570
|
+
},
|
|
5571
|
+
{
|
|
5572
|
+
id: "hermes.runtime-singleton",
|
|
5573
|
+
title: "Hermes singleton runtime (shared config/auth, per-agent memory)",
|
|
5574
|
+
audit: (ctx) => {
|
|
5575
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5576
|
+
if (!roles.length) {
|
|
5577
|
+
return { id: "hermes.runtime-singleton", title: "Hermes singleton runtime (shared config/auth, per-agent memory)", status: "skip", summary: "No Hermes roles present", details: [], fixable: false };
|
|
5578
|
+
}
|
|
5579
|
+
const details = [];
|
|
5580
|
+
for (const role of roles) {
|
|
5581
|
+
const plan = singletonPlan(ctx, role);
|
|
5582
|
+
if (!existsSync10(plan.fleetRoot)) {
|
|
5583
|
+
details.push(`fleet root missing at ${plan.fleetRoot}`);
|
|
5584
|
+
continue;
|
|
5585
|
+
}
|
|
5586
|
+
if (!existsSync10(plan.profileDir)) {
|
|
5587
|
+
details.push(`profile dir missing: ${plan.profileDir}`);
|
|
5588
|
+
} else if (lstatSync2(plan.profileDir).isSymbolicLink()) {
|
|
5589
|
+
details.push(`profile dir is a symlink (must be a real dir): ${plan.profileDir}`);
|
|
5590
|
+
}
|
|
5591
|
+
for (const link of plan.links) {
|
|
5592
|
+
const state = linkState(link.path, link.target);
|
|
5593
|
+
if (state !== "ok") details.push(`${state}: ${link.path} -> ${link.target}`);
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
return {
|
|
5597
|
+
id: "hermes.runtime-singleton",
|
|
5598
|
+
title: "Hermes singleton runtime (shared config/auth, per-agent memory)",
|
|
5599
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
5600
|
+
summary: details.length === 0 ? "Singleton runtime contract satisfied" : `${details.length} singleton-runtime issue(s) detected`,
|
|
5601
|
+
details,
|
|
5602
|
+
fixable: true
|
|
5603
|
+
};
|
|
5604
|
+
},
|
|
5605
|
+
migrate: (ctx, finding) => {
|
|
5606
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5607
|
+
const changedFiles = [];
|
|
5608
|
+
const details = [];
|
|
5609
|
+
for (const role of roles) {
|
|
5610
|
+
const plan = singletonPlan(ctx, role);
|
|
5611
|
+
if (!existsSync10(plan.fleetRoot)) {
|
|
5612
|
+
details.push(`blocked: fleet root missing at ${plan.fleetRoot}`);
|
|
5613
|
+
continue;
|
|
5614
|
+
}
|
|
5615
|
+
for (const shared of plan.sharedSeeds) {
|
|
5616
|
+
if (existsSync10(shared.rootPath)) continue;
|
|
5617
|
+
const donor = existsSync10(shared.runtimePath) ? shared.runtimePath : null;
|
|
5618
|
+
if (!donor) continue;
|
|
5619
|
+
details.push(`seed fleet ${basename4(shared.rootPath)} from ${donor}`);
|
|
5620
|
+
changedFiles.push(shared.rootPath);
|
|
5621
|
+
if (!ctx.dryRun) copyFileSync(donor, shared.rootPath);
|
|
5622
|
+
}
|
|
5623
|
+
if (existsSync10(plan.profileDir) && lstatSync2(plan.profileDir).isSymbolicLink()) {
|
|
5624
|
+
details.push(`convert profile symlink to real dir: ${plan.profileDir}`);
|
|
5625
|
+
changedFiles.push(plan.profileDir);
|
|
5626
|
+
if (!ctx.dryRun) unlinkSync3(plan.profileDir);
|
|
5627
|
+
}
|
|
5628
|
+
if (!existsSync10(plan.profileDir)) {
|
|
5629
|
+
details.push(`create profile dir: ${plan.profileDir}`);
|
|
5630
|
+
changedFiles.push(plan.profileDir);
|
|
5631
|
+
if (!ctx.dryRun) mkdirSync6(plan.profileDir, { recursive: true });
|
|
5632
|
+
}
|
|
5633
|
+
for (const link of plan.links) {
|
|
5634
|
+
const state = linkState(link.path, link.target);
|
|
5635
|
+
if (state === "ok") continue;
|
|
5636
|
+
if (link.ensureTargetDir && !existsSync10(link.target) && !ctx.dryRun) {
|
|
5637
|
+
mkdirSync6(link.target, { recursive: true });
|
|
5638
|
+
}
|
|
5639
|
+
details.push(`link ${link.path} -> ${link.target}`);
|
|
5640
|
+
changedFiles.push(link.path);
|
|
5641
|
+
if (ctx.dryRun) continue;
|
|
5642
|
+
if (existsSync10(link.path) || isDanglingLink(link.path)) {
|
|
5643
|
+
const lst = lstatSync2(link.path);
|
|
5644
|
+
if (lst.isSymbolicLink()) {
|
|
5645
|
+
unlinkSync3(link.path);
|
|
5646
|
+
} else {
|
|
5647
|
+
const parked = `${link.path}.pre-singleton`;
|
|
5648
|
+
renameSync2(link.path, parked);
|
|
5649
|
+
details.push(`parked pre-existing ${link.path} at ${parked}`);
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
ensureParent(link.path);
|
|
5653
|
+
symlinkSync(link.target, link.path);
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
return {
|
|
5657
|
+
id: finding.id,
|
|
5658
|
+
title: finding.title,
|
|
5659
|
+
status: details.some((d) => d.startsWith("blocked:")) ? "blocked" : changedFiles.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
5660
|
+
summary: changedFiles.length ? ctx.dryRun ? "Planned singleton-runtime wiring" : "Singleton runtime wired" : "No changes required",
|
|
5661
|
+
changedFiles,
|
|
5662
|
+
details
|
|
5663
|
+
};
|
|
5664
|
+
}
|
|
5665
|
+
},
|
|
5666
|
+
{
|
|
5667
|
+
id: "hermes.profile-wiring",
|
|
5668
|
+
title: "Launcher + systemd HERMES_HOME points at the named profile",
|
|
5669
|
+
audit: (ctx) => {
|
|
5670
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5671
|
+
if (!roles.length) {
|
|
5672
|
+
return { id: "hermes.profile-wiring", title: "Launcher + systemd HERMES_HOME points at the named profile", status: "skip", summary: "No Hermes roles present", details: [], fixable: false };
|
|
5673
|
+
}
|
|
5674
|
+
const details = [];
|
|
5675
|
+
for (const role of roles) {
|
|
5676
|
+
const plan = singletonPlan(ctx, role);
|
|
5677
|
+
const launcher = join14(role.roleDir, "hermes");
|
|
5678
|
+
const text3 = safeReadText(launcher);
|
|
5679
|
+
if (text3 === null) {
|
|
5680
|
+
details.push(`launcher missing: ${relative2(ctx.repoRoot, launcher)}`);
|
|
5681
|
+
} else {
|
|
5682
|
+
if (/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m.test(text3)) {
|
|
5683
|
+
details.push(`launcher sets HERMES_HOME to the raw runtime path (disables shared auth + profile identity): ${relative2(ctx.repoRoot, launcher)}`);
|
|
5684
|
+
}
|
|
5685
|
+
if (/HERMES_OAUTH_FILE/.test(text3)) {
|
|
5686
|
+
details.push(`launcher exports HERMES_OAUTH_FILE, which Hermes does not implement (dead config): ${relative2(ctx.repoRoot, launcher)}`);
|
|
5687
|
+
}
|
|
5688
|
+
}
|
|
5689
|
+
for (const unit of profileUnits(role)) {
|
|
5690
|
+
const unitPath = join14(ctx.homeDir, ".config", "systemd", "user", unit);
|
|
5691
|
+
const unitText = safeReadText(unitPath);
|
|
5692
|
+
if (unitText === null) continue;
|
|
5693
|
+
const current = /^Environment=HERMES_HOME=(.*)$/m.exec(unitText)?.[1]?.trim();
|
|
5694
|
+
if (current && current !== plan.profileDir) {
|
|
5695
|
+
details.push(`${unit} HERMES_HOME=${current} (expected ${plan.profileDir})`);
|
|
5696
|
+
}
|
|
5697
|
+
if (/^Environment=HERMES_OAUTH_FILE=/m.test(unitText)) {
|
|
5698
|
+
details.push(`${unit} sets HERMES_OAUTH_FILE (dead config)`);
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
return {
|
|
5703
|
+
id: "hermes.profile-wiring",
|
|
5704
|
+
title: "Launcher + systemd HERMES_HOME points at the named profile",
|
|
5705
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
5706
|
+
summary: details.length === 0 ? "HERMES_HOME wiring is in parity" : `${details.length} HERMES_HOME wiring issue(s) detected`,
|
|
5707
|
+
details,
|
|
5708
|
+
fixable: true
|
|
5709
|
+
};
|
|
5710
|
+
},
|
|
5711
|
+
migrate: (ctx, finding) => {
|
|
5712
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5713
|
+
const changedFiles = [];
|
|
5714
|
+
const details = [];
|
|
5715
|
+
let unitsTouched = false;
|
|
5716
|
+
for (const role of roles) {
|
|
5717
|
+
const plan = singletonPlan(ctx, role);
|
|
5718
|
+
const launcher = join14(role.roleDir, "hermes");
|
|
5719
|
+
const text3 = safeReadText(launcher);
|
|
5720
|
+
if (text3 !== null) {
|
|
5721
|
+
const rewritten = rewriteLauncher(text3);
|
|
5722
|
+
if (rewritten !== text3) {
|
|
5723
|
+
details.push(`rewrite launcher HERMES_HOME -> profile path: ${relative2(ctx.repoRoot, launcher)}`);
|
|
5724
|
+
writeIfDifferent(launcher, rewritten, ctx.dryRun, changedFiles, 493);
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
for (const unit of profileUnits(role)) {
|
|
5728
|
+
const unitPath = join14(ctx.homeDir, ".config", "systemd", "user", unit);
|
|
5729
|
+
const unitText = safeReadText(unitPath);
|
|
5730
|
+
if (unitText === null) continue;
|
|
5731
|
+
let next = unitText.replace(/^Environment=HERMES_HOME=.*$/m, `Environment=HERMES_HOME=${plan.profileDir}`);
|
|
5732
|
+
next = next.replace(/^Environment=HERMES_OAUTH_FILE=.*\n/m, "");
|
|
5733
|
+
if (next !== unitText) {
|
|
5734
|
+
details.push(`repoint ${unit} HERMES_HOME -> ${plan.profileDir}`);
|
|
5735
|
+
writeIfDifferent(unitPath, next, ctx.dryRun, changedFiles);
|
|
5736
|
+
unitsTouched = true;
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
if (unitsTouched && !ctx.dryRun) {
|
|
5741
|
+
systemctlUser(["daemon-reload"]);
|
|
5742
|
+
details.push("systemctl --user daemon-reload (restart units to pick up the new HERMES_HOME)");
|
|
5743
|
+
}
|
|
5744
|
+
return {
|
|
5745
|
+
id: finding.id,
|
|
5746
|
+
title: finding.title,
|
|
5747
|
+
status: changedFiles.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
5748
|
+
summary: changedFiles.length ? ctx.dryRun ? "Planned HERMES_HOME rewiring" : "HERMES_HOME rewired to named profiles" : "No changes required",
|
|
5749
|
+
changedFiles,
|
|
5750
|
+
details
|
|
5751
|
+
};
|
|
5752
|
+
}
|
|
5753
|
+
},
|
|
5754
|
+
{
|
|
5755
|
+
id: "hermes.registry-parity",
|
|
5756
|
+
title: "Fleet registry matches .project.json (no duplicate or stale agents)",
|
|
5757
|
+
audit: (ctx) => {
|
|
5758
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5759
|
+
const details = [];
|
|
5760
|
+
const registryPath2 = join14(ctx.homeDir, ".hermes", "agents-registry.yaml");
|
|
5761
|
+
const registry = readRegistry(registryPath2);
|
|
5762
|
+
if (!registry) {
|
|
5763
|
+
if (!roles.length && declaredAgentIds(ctx.repoRoot).length === 0) {
|
|
5764
|
+
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "skip", summary: "No Hermes roles or declared agents present", details: [], fixable: false };
|
|
5765
|
+
}
|
|
5766
|
+
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "warn", summary: `registry unreadable at ${registryPath2}`, details: [], fixable: false };
|
|
5767
|
+
}
|
|
5768
|
+
const canonical = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
5769
|
+
const owned = ownedRegistryEntries(registry, ctx.repoRoot);
|
|
5770
|
+
const unprovisioned = unprovisionedRoleAgents(registry, ctx.repoRoot, canonical);
|
|
5771
|
+
if (unprovisioned.length) {
|
|
5772
|
+
return {
|
|
5773
|
+
id: "hermes.registry-parity",
|
|
5774
|
+
title: "Fleet registry matches .project.json (no duplicate or stale agents)",
|
|
5775
|
+
status: "fail",
|
|
5776
|
+
summary: `${unprovisioned.length} unprovisioned Hermes role blocker(s) detected`,
|
|
5777
|
+
details: unprovisioned.map(
|
|
5778
|
+
({ agentId, roleDir, sources }) => `agent "${agentId}" (${sources.join(" + ")}) has no role.yaml${roleDir ? ` at ${roleDir}` : ""}; provision or restore the role, do not delete its registry/declaration`
|
|
5779
|
+
),
|
|
5780
|
+
fixable: false
|
|
5781
|
+
};
|
|
5782
|
+
}
|
|
5783
|
+
if (canonical.size === 0) {
|
|
5784
|
+
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "skip", summary: "No Hermes roles, declarations, or registry entries present", details: [], fixable: false };
|
|
5785
|
+
}
|
|
5786
|
+
for (const [agentId, entry] of owned) {
|
|
5787
|
+
const roleDir = String(entry?.role_dir ?? "");
|
|
5788
|
+
if (!canonical.has(agentId)) {
|
|
5789
|
+
details.push(`stale/duplicate registry agent "${agentId}" for ${roleDir} (role.yaml declares ${[...canonical].join(", ")})`);
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical.has(id))) {
|
|
5793
|
+
details.push(`.project.json declares agent "${extra}" that no role.yaml claims`);
|
|
5794
|
+
}
|
|
5795
|
+
for (const role of roles) {
|
|
5796
|
+
const entry = registry[role.agentId];
|
|
5797
|
+
if (!entry) {
|
|
5798
|
+
details.push(`registry is missing an entry for ${role.agentId}`);
|
|
5799
|
+
continue;
|
|
5800
|
+
}
|
|
5801
|
+
const entryRoleDir = String(entry.role_dir ?? "");
|
|
5802
|
+
if (entryRoleDir && realOrSelf(entryRoleDir) !== realOrSelf(role.roleDir)) {
|
|
5803
|
+
details.push(`registry role_dir for ${role.agentId} is ${entryRoleDir} (expected ${role.roleDir})`);
|
|
5804
|
+
}
|
|
5805
|
+
const bin = String(entry.hermes?.bin ?? "");
|
|
5806
|
+
if (bin && !existsSync10(bin)) {
|
|
5807
|
+
details.push(`registry hermes.bin for ${role.agentId} does not exist: ${bin}`);
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
return {
|
|
5811
|
+
id: "hermes.registry-parity",
|
|
5812
|
+
title: "Fleet registry matches .project.json (no duplicate or stale agents)",
|
|
5813
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
5814
|
+
summary: details.length === 0 ? "Fleet registry is in parity" : `${details.length} registry parity issue(s) detected`,
|
|
5815
|
+
details,
|
|
5816
|
+
fixable: true
|
|
5817
|
+
};
|
|
5818
|
+
},
|
|
5819
|
+
migrate: (ctx, finding) => {
|
|
5820
|
+
const changedFiles = [];
|
|
5821
|
+
const details = [];
|
|
5822
|
+
const registryPath2 = join14(ctx.homeDir, ".hermes", "agents-registry.yaml");
|
|
5823
|
+
const raw = safeReadText(registryPath2);
|
|
5824
|
+
if (raw === null) {
|
|
5825
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: `registry unreadable at ${registryPath2}`, changedFiles, details };
|
|
5826
|
+
}
|
|
5827
|
+
let doc;
|
|
5828
|
+
try {
|
|
5829
|
+
doc = YAML3.parse(raw);
|
|
5830
|
+
} catch {
|
|
5831
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "registry is not valid YAML", changedFiles, details };
|
|
5832
|
+
}
|
|
5833
|
+
const agents = doc?.agents ?? {};
|
|
5834
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5835
|
+
const canonical = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
5836
|
+
const unprovisioned = unprovisionedRoleAgents(agents, ctx.repoRoot, canonical);
|
|
5837
|
+
if (unprovisioned.length) {
|
|
5838
|
+
return {
|
|
5839
|
+
id: finding.id,
|
|
5840
|
+
title: finding.title,
|
|
5841
|
+
status: "blocked",
|
|
5842
|
+
summary: "Registry parity is blocked by an unprovisioned Hermes role",
|
|
5843
|
+
changedFiles,
|
|
5844
|
+
details: unprovisioned.map(
|
|
5845
|
+
({ agentId, roleDir, sources }) => `blocked: "${agentId}" (${sources.join(" + ")}) has no role.yaml${roleDir ? ` at ${roleDir}` : ""}; provision or restore the role without pruning registry/declaration state`
|
|
5846
|
+
)
|
|
5847
|
+
};
|
|
5848
|
+
}
|
|
5849
|
+
const fleetBin = fleetBinPath(ctx);
|
|
5850
|
+
let dirty = false;
|
|
5851
|
+
if (canonical.size === 0) {
|
|
5852
|
+
for (const [agentId] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
5853
|
+
details.push(`blocked: "${agentId}" has no role.yaml; provision the role instead of pruning the registry`);
|
|
5854
|
+
}
|
|
5855
|
+
for (const agentId of declaredAgentIds(ctx.repoRoot)) {
|
|
5856
|
+
if (!details.some((detail) => detail.includes(`"${agentId}"`))) {
|
|
5857
|
+
details.push(`blocked: "${agentId}" is declared but has no role.yaml; provision or restore the role`);
|
|
5858
|
+
}
|
|
5859
|
+
}
|
|
5860
|
+
if (details.length) {
|
|
5861
|
+
return {
|
|
5862
|
+
id: finding.id,
|
|
5863
|
+
title: finding.title,
|
|
5864
|
+
status: "blocked",
|
|
5865
|
+
summary: "Registry parity is blocked by an unprovisioned Hermes role",
|
|
5866
|
+
changedFiles,
|
|
5867
|
+
details
|
|
5868
|
+
};
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
for (const [agentId, entry] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
5872
|
+
if (canonical.size > 0 && !canonical.has(agentId)) {
|
|
5873
|
+
details.push(`drop stale/duplicate registry agent "${agentId}"`);
|
|
5874
|
+
delete agents[agentId];
|
|
5875
|
+
dropDeclaredAgent(ctx, agentId, changedFiles, details);
|
|
5876
|
+
dirty = true;
|
|
5877
|
+
continue;
|
|
5878
|
+
}
|
|
5879
|
+
const hermes = entry.hermes ?? {};
|
|
5880
|
+
if (fleetBin && String(hermes.bin ?? "") !== fleetBin && !existsSync10(String(hermes.bin ?? ""))) {
|
|
5881
|
+
details.push(`repoint ${agentId} hermes.bin -> ${fleetBin}`);
|
|
5882
|
+
hermes.bin = fleetBin;
|
|
5883
|
+
entry.hermes = hermes;
|
|
5884
|
+
dirty = true;
|
|
5885
|
+
}
|
|
5886
|
+
if (hermes.oauth_file) {
|
|
5887
|
+
details.push(`drop dead hermes.oauth_file from ${agentId}`);
|
|
5888
|
+
delete hermes.oauth_file;
|
|
5889
|
+
dirty = true;
|
|
5890
|
+
}
|
|
5891
|
+
}
|
|
5892
|
+
if (canonical.size > 0) {
|
|
5893
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical.has(id))) {
|
|
5894
|
+
dropDeclaredAgent(ctx, extra, changedFiles, details);
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
if (dirty) {
|
|
5898
|
+
changedFiles.push(registryPath2);
|
|
5899
|
+
if (!ctx.dryRun) {
|
|
5900
|
+
doc.agents = agents;
|
|
5901
|
+
writeText(registryPath2, YAML3.stringify(doc));
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
return {
|
|
5905
|
+
id: finding.id,
|
|
5906
|
+
title: finding.title,
|
|
5907
|
+
status: changedFiles.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
5908
|
+
summary: changedFiles.length ? ctx.dryRun ? "Planned registry repair" : "Fleet registry repaired" : "No changes required",
|
|
5909
|
+
changedFiles,
|
|
5910
|
+
details
|
|
5911
|
+
};
|
|
5912
|
+
}
|
|
4444
5913
|
}
|
|
4445
5914
|
];
|
|
4446
5915
|
function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
@@ -4459,7 +5928,7 @@ function getParityRuleIds() {
|
|
|
4459
5928
|
function runAudit(repoArg) {
|
|
4460
5929
|
const pjanglerRoot = resolvePjanglerRoot2();
|
|
4461
5930
|
const ctx = {
|
|
4462
|
-
repoRoot:
|
|
5931
|
+
repoRoot: resolve3(repoArg ?? process.cwd()),
|
|
4463
5932
|
dryRun: true,
|
|
4464
5933
|
pjanglerRoot,
|
|
4465
5934
|
homeDir: homedir5()
|
|
@@ -4475,7 +5944,7 @@ function runAudit(repoArg) {
|
|
|
4475
5944
|
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
4476
5945
|
const pjanglerRoot = resolvePjanglerRoot2();
|
|
4477
5946
|
const ctx = {
|
|
4478
|
-
repoRoot:
|
|
5947
|
+
repoRoot: resolve3(repoArg ?? process.cwd()),
|
|
4479
5948
|
dryRun,
|
|
4480
5949
|
pjanglerRoot,
|
|
4481
5950
|
homeDir: homedir5()
|
|
@@ -4509,7 +5978,22 @@ function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
|
4509
5978
|
};
|
|
4510
5979
|
}
|
|
4511
5980
|
function runMigration(selector, repoArg, dryRun, all) {
|
|
4512
|
-
|
|
5981
|
+
if (all) {
|
|
5982
|
+
const audit = runAudit(repoArg);
|
|
5983
|
+
const ruleIds2 = audit.rules.filter((finding) => finding.fixable && (finding.status === "fail" || finding.status === "warn")).map((finding) => finding.id);
|
|
5984
|
+
if (ruleIds2.length === 0) {
|
|
5985
|
+
return {
|
|
5986
|
+
repo: audit.repo,
|
|
5987
|
+
dryRun,
|
|
5988
|
+
ok: true,
|
|
5989
|
+
selectedRules: [],
|
|
5990
|
+
results: [],
|
|
5991
|
+
changedFiles: []
|
|
5992
|
+
};
|
|
5993
|
+
}
|
|
5994
|
+
return runMigrationForRules(ruleIds2, repoArg, dryRun);
|
|
5995
|
+
}
|
|
5996
|
+
const ruleIds = selector ? [selector] : [];
|
|
4513
5997
|
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
4514
5998
|
}
|
|
4515
5999
|
function prettyTimestamp(iso) {
|
|
@@ -4562,15 +6046,15 @@ function formatMigrationReport(report) {
|
|
|
4562
6046
|
}
|
|
4563
6047
|
|
|
4564
6048
|
// src/utils/version.ts
|
|
4565
|
-
import { readFileSync as
|
|
4566
|
-
import { dirname as dirname7, join as
|
|
6049
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
6050
|
+
import { dirname as dirname7, join as join15 } from "node:path";
|
|
4567
6051
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4568
6052
|
var PJANGLER_VERSION = (() => {
|
|
4569
6053
|
try {
|
|
4570
6054
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
4571
6055
|
for (let i = 0; i < 4; i++) {
|
|
4572
6056
|
try {
|
|
4573
|
-
const raw =
|
|
6057
|
+
const raw = readFileSync8(join15(dir, "package.json"), "utf8");
|
|
4574
6058
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
4575
6059
|
} catch {
|
|
4576
6060
|
const parent = dirname7(dir);
|
|
@@ -4615,7 +6099,7 @@ async function promptForRuleIds(rules) {
|
|
|
4615
6099
|
function readJson(path) {
|
|
4616
6100
|
if (!existsSync11(path)) return void 0;
|
|
4617
6101
|
try {
|
|
4618
|
-
const parsed = JSON.parse(
|
|
6102
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
4619
6103
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
4620
6104
|
} catch {
|
|
4621
6105
|
return void 0;
|
|
@@ -4624,7 +6108,7 @@ function readJson(path) {
|
|
|
4624
6108
|
function findGitRoot(cwd) {
|
|
4625
6109
|
const result = spawnSync7("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
4626
6110
|
if (result.status !== 0) return void 0;
|
|
4627
|
-
return
|
|
6111
|
+
return resolve4(result.stdout.trim());
|
|
4628
6112
|
}
|
|
4629
6113
|
function packageNameToProjectName(value) {
|
|
4630
6114
|
if (!value) return void 0;
|
|
@@ -4632,9 +6116,9 @@ function packageNameToProjectName(value) {
|
|
|
4632
6116
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
4633
6117
|
}
|
|
4634
6118
|
function deriveProjectDefaults(targetDir) {
|
|
4635
|
-
const manifest = readJson(
|
|
4636
|
-
const pkg = readJson(
|
|
4637
|
-
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(
|
|
6119
|
+
const manifest = readJson(join16(targetDir, ".project.json"));
|
|
6120
|
+
const pkg = readJson(join16(targetDir, "package.json"));
|
|
6121
|
+
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename5(targetDir)) || "Project";
|
|
4638
6122
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
4639
6123
|
return {
|
|
4640
6124
|
name,
|
|
@@ -4689,7 +6173,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
4689
6173
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
4690
6174
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
4691
6175
|
`;
|
|
4692
|
-
return !existsSync11(action.path) ||
|
|
6176
|
+
return !existsSync11(action.path) || readFileSync9(action.path, "utf8") !== next;
|
|
4693
6177
|
}
|
|
4694
6178
|
if (kind === "copier.copy.commonproject") return true;
|
|
4695
6179
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -4737,26 +6221,26 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
4737
6221
|
const interactive = isInteractiveProjectInit(options);
|
|
4738
6222
|
const cwd = process.cwd();
|
|
4739
6223
|
const cwdGitRoot = findGitRoot(cwd);
|
|
4740
|
-
let targetDir = options.targetDir ?
|
|
6224
|
+
let targetDir = options.targetDir ? resolve4(options.targetDir) : void 0;
|
|
4741
6225
|
if (!targetDir && cwdGitRoot) {
|
|
4742
6226
|
targetDir = cwdGitRoot;
|
|
4743
6227
|
}
|
|
4744
6228
|
if (!targetDir && interactive) {
|
|
4745
|
-
const defaultName = name ??
|
|
6229
|
+
const defaultName = name ?? basename5(cwd);
|
|
4746
6230
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
4747
|
-
const defaultDir =
|
|
6231
|
+
const defaultDir = join16(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
4748
6232
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
4749
6233
|
name = promptedName;
|
|
4750
6234
|
}
|
|
4751
6235
|
if (!targetDir) {
|
|
4752
6236
|
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
4753
|
-
targetDir =
|
|
6237
|
+
targetDir = resolve4(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
4754
6238
|
}
|
|
4755
6239
|
const targetExists = existsSync11(targetDir);
|
|
4756
6240
|
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
4757
6241
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
4758
|
-
const syncMode = Boolean(targetGitRoot &&
|
|
4759
|
-
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(
|
|
6242
|
+
const syncMode = Boolean(targetGitRoot && resolve4(targetGitRoot) === resolve4(targetDir));
|
|
6243
|
+
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename5(targetDir)) ?? "Project", description: "" };
|
|
4760
6244
|
if (!name && interactive && !syncMode) {
|
|
4761
6245
|
name = await promptTextValue("Project name", defaults.name);
|
|
4762
6246
|
}
|
|
@@ -5140,7 +6624,7 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
|
|
|
5140
6624
|
process.exit(1);
|
|
5141
6625
|
}
|
|
5142
6626
|
});
|
|
5143
|
-
program.command("hermes-agent").alias("hermes").description("Provision the PM agent for the current repo (defaults everything; only asks about Telegram)").option("-y, --yes", "Non-interactive: accept all defaults (also skips the Telegram prompt)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role override (default: pm \u2014 the only role in the fleet)").option("--purpose <text>", 'One-line agent purpose (default: "pm agent for <repo>")').option(`--tone <tone>`, `Personality tone (default: direct; ${SOUL_TONES.join(" | ")})`).option("--model-provider <name>", 'Inference provider override ("" = inherit shared default profile)').option("--model-name <name>", 'Model name override ("" = inherit shared default profile)').option("--skip-telegram", "Skip the Telegram wire-up (no BotFather prompt)").option("--email", "Also provision the delo.sh email address (off by default; never prompted)").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating or linking the ticket board").option("--skip-bloodbank", "
|
|
6627
|
+
program.command("hermes-agent").alias("hermes").description("Provision the PM agent for the current repo (defaults everything; only asks about Telegram)").option("-y, --yes", "Non-interactive: accept all defaults (also skips the Telegram prompt)").option("--target-repo <name>", "Target repo name (default: basename of cwd)").option("--role <role>", "Agent role override (default: pm \u2014 the only role in the fleet)").option("--purpose <text>", 'One-line agent purpose (default: "pm agent for <repo>")').option(`--tone <tone>`, `Personality tone (default: direct; ${SOUL_TONES.join(" | ")})`).option("--model-provider <name>", 'Inference provider override ("" = inherit shared default profile)').option("--model-name <name>", 'Model name override ("" = inherit shared default profile)').option("--skip-telegram", "Skip the Telegram wire-up (no BotFather prompt)").option("--email", "Also provision the delo.sh email address (off by default; never prompted)").option("--skip-runtime-repo", "Skip creating the per-agent runtime GH repo").option("--skip-plane", "Skip creating or linking the ticket board").option("--skip-bloodbank", "Deprecated compatibility flag; Bloodbank now uses one fleet-shared Hermes gateway").option("--skip-systemd", "Skip installing systemd --user units").option("--local", "Local-only: skip runtime repo, ticket-board creation, Bloodbank, and systemd (safe for laptops/macOS/non-technical operators)").option("--force-config", "Regenerate ~/.config/hermes-agent-template/config.toml even if it exists").option("--dry-run", "Preview what would run; don't execute copier").option("-f, --force", "Re-render even if agents/hermes/<role>/role.yaml already exists").action(async (options) => {
|
|
5144
6628
|
const isDarwin = process.platform === "darwin";
|
|
5145
6629
|
const local = options.local ?? false;
|
|
5146
6630
|
const context = {
|