@delorenj/pjangler 1.2.19 → 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 +2353 -518
- package/dist/mcp-server.js +2331 -494
- package/package.json +14 -3
- package/templates/commonproject/copier.yml +6 -4
- package/templates/commonproject/template/.agents/hooks/README.md +13 -26
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
- package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
- package/templates/commonproject/template/.agents/local.example.json +2 -8
- package/templates/commonproject/template/.agents/skills.json +6 -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 +16 -16
- 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 +12 -1
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +21 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +36 -15
- 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/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
- 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
|
|
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";
|
|
@@ -410,97 +540,284 @@ var AddMiseCodegraphScript = class extends Command {
|
|
|
410
540
|
};
|
|
411
541
|
}
|
|
412
542
|
const content = `#!/usr/bin/env bash
|
|
413
|
-
#
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
543
|
+
# Auto-generated by pjangler
|
|
544
|
+
|
|
545
|
+
REPO_ROOT="$(pwd)"
|
|
546
|
+
PROJECT_NAME="$(basename "$REPO_ROOT")"
|
|
547
|
+
CONTAINER_NAME="codegraph-mcp-$PROJECT_NAME"
|
|
548
|
+
CACHE_DIR="$REPO_ROOT/.codegraph"
|
|
549
|
+
|
|
550
|
+
# Deterministically generate a port based on the repository path
|
|
551
|
+
PORT=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}' | tr -d 'a-f' | cut -c1-4)
|
|
552
|
+
# Ensure port is > 1024
|
|
553
|
+
PORT=$(( (PORT % 60000) + 1025 ))
|
|
554
|
+
|
|
555
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
|
|
556
|
+
echo "\u{1F680} Starting CodeGraph MCP Server on port $PORT..."
|
|
557
|
+
|
|
558
|
+
# Ensure the container isn't lingering in a stopped state
|
|
559
|
+
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
560
|
+
|
|
561
|
+
# Run the true Colby McHenry CodeGraph Docker image
|
|
562
|
+
# which we built locally as colbymchenry-codegraph-mcp:latest
|
|
563
|
+
docker run -d \\
|
|
564
|
+
--name "$CONTAINER_NAME" \\
|
|
565
|
+
--restart unless-stopped \\
|
|
566
|
+
-p "$PORT:8045" \\
|
|
567
|
+
-v "$REPO_ROOT:/repo" \\
|
|
568
|
+
colbymchenry-codegraph-mcp:latest >/dev/null
|
|
569
|
+
|
|
570
|
+
echo "\u2705 CodeGraph MCP running in background. SSE URL: http://localhost:$PORT/sse"
|
|
571
|
+
fi
|
|
572
|
+
|
|
573
|
+
# Run init inside the container to ensure the index is bootstrapped.
|
|
574
|
+
# We run this using the standard codegraph CLI inside the container
|
|
575
|
+
docker exec "$CONTAINER_NAME" codegraph init -i /repo >/dev/null 2>&1 || true
|
|
576
|
+
|
|
577
|
+
# Wire up the MCP server to local agents
|
|
578
|
+
WIRE_SCRIPT="$(dirname "$0")/codegraph-wire.sh"
|
|
579
|
+
if [ -x "$WIRE_SCRIPT" ]; then
|
|
580
|
+
"$WIRE_SCRIPT"
|
|
581
|
+
fi
|
|
582
|
+
`;
|
|
583
|
+
this.writeFile(filePath, content);
|
|
584
|
+
if (!this.context.dryRun) {
|
|
585
|
+
chmodSync(join4(this.context.targetDir, filePath), 493);
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
success: true,
|
|
589
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
590
|
+
filePath
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// src/commands/AddMiseCodegraphWireScript.ts
|
|
596
|
+
import { chmodSync as chmodSync2 } from "fs";
|
|
597
|
+
import { join as join5 } from "path";
|
|
598
|
+
var AddMiseCodegraphWireScript = class extends Command {
|
|
599
|
+
async invoke() {
|
|
600
|
+
const filePath = ".mise/scripts/codegraph-wire.sh";
|
|
601
|
+
if (this.fileExists(filePath) && !this.context.force) {
|
|
602
|
+
return {
|
|
603
|
+
success: false,
|
|
604
|
+
message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph-wire.sh already exists"),
|
|
605
|
+
filePath
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const content = `#!/usr/bin/env bash
|
|
609
|
+
# Mise enter hook: Auto-wire CodeGraph MCP Server to local agents.
|
|
610
|
+
# Detects Claude Code, Codex, Gemini, Kimi, and OpenCode and configures
|
|
611
|
+
# them to use the local SSE endpoint for the current project.
|
|
419
612
|
|
|
420
613
|
set -euo pipefail
|
|
421
614
|
|
|
422
615
|
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
616
|
+
|
|
617
|
+
# Compute the same port as the container script
|
|
618
|
+
PORT_HASH=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}')
|
|
619
|
+
PORT_DEC=$(printf "%d" "0x\${PORT_HASH:0:4}")
|
|
620
|
+
PORT=$(( 8045 + (PORT_DEC % 955) ))
|
|
621
|
+
|
|
622
|
+
SSE_URL="http://localhost:$PORT/sse"
|
|
623
|
+
|
|
624
|
+
inject_sse() {
|
|
625
|
+
local target="$1"
|
|
626
|
+
local agent="$2"
|
|
627
|
+
|
|
628
|
+
if [ ! -f "$target" ]; then
|
|
629
|
+
mkdir -p "$(dirname "$target")"
|
|
630
|
+
echo '{"mcpServers": {}}' > "$target"
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
# Ensure the file is valid JSON (fail gracefully if it's garbled)
|
|
634
|
+
if ! jq . "$target" >/dev/null 2>&1; then
|
|
635
|
+
echo "[mise] WARNING: $target is not valid JSON, skipping $agent wiring" >&2
|
|
636
|
+
return
|
|
437
637
|
fi
|
|
638
|
+
|
|
639
|
+
# Inject or update the codegraph server
|
|
640
|
+
jq --arg url "$SSE_URL" '.mcpServers.codegraph = {"type": "sse", "url": $url}' "$target" > "$target.tmp" && mv "$target.tmp" "$target"
|
|
641
|
+
echo "[mise] Wired CodeGraph SSE for $agent -> $target"
|
|
438
642
|
}
|
|
439
643
|
|
|
440
|
-
#
|
|
441
|
-
|
|
442
|
-
if command -v codegraph >/dev/null 2>&1; then
|
|
443
|
-
return 0
|
|
444
|
-
fi
|
|
644
|
+
# 1. Claude Code (Project-scoped)
|
|
645
|
+
inject_sse "$REPO_ROOT/.claude.json" "Claude Code"
|
|
445
646
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
449
|
-
return 0
|
|
450
|
-
fi
|
|
647
|
+
# 2. Codex (Global)
|
|
648
|
+
inject_sse "$HOME/.codex/mcp.json" "Codex"
|
|
451
649
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if [ -x "$d/codegraph" ]; then
|
|
455
|
-
export PATH="$d:$PATH"
|
|
456
|
-
return 0
|
|
457
|
-
fi
|
|
458
|
-
done
|
|
650
|
+
# 3. Gemini (Global)
|
|
651
|
+
inject_sse "$HOME/.gemini/config/mcp.json" "Gemini"
|
|
459
652
|
|
|
460
|
-
|
|
461
|
-
|
|
653
|
+
# 4. Kimi (Global)
|
|
654
|
+
inject_sse "$HOME/.kimi-code/mcp.json" "Kimi"
|
|
462
655
|
|
|
463
|
-
#
|
|
464
|
-
|
|
465
|
-
init_project() {
|
|
466
|
-
local err_file
|
|
467
|
-
err_file="$(mktemp)"
|
|
468
|
-
trap 'rm -f "$err_file"' RETURN
|
|
656
|
+
# 5. OpenCode (Global)
|
|
657
|
+
inject_sse "$HOME/.opencode/mcp.json" "OpenCode"
|
|
469
658
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
fi
|
|
659
|
+
# 6. Cursor (Global)
|
|
660
|
+
inject_sse "$HOME/.cursor/mcp.json" "Cursor"
|
|
473
661
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
ensure_codegraph
|
|
477
|
-
codegraph init -i "$REPO_ROOT"
|
|
478
|
-
return 0
|
|
479
|
-
fi
|
|
662
|
+
# 7. VSCode native MCP (Global)
|
|
663
|
+
inject_sse "$HOME/.vscode/mcp.json" "VSCode"
|
|
480
664
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
665
|
+
# 8. Cline (VSCode extension)
|
|
666
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" "Cline"
|
|
484
667
|
|
|
485
|
-
|
|
668
|
+
# 9. Roo (VSCode extension)
|
|
669
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json" "Roo Code"
|
|
486
670
|
`;
|
|
487
671
|
this.writeFile(filePath, content);
|
|
488
672
|
if (!this.context.dryRun) {
|
|
489
|
-
|
|
673
|
+
chmodSync2(join5(this.context.targetDir, filePath), 493);
|
|
490
674
|
}
|
|
491
675
|
return {
|
|
492
676
|
success: true,
|
|
493
|
-
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
677
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph-wire.sh" : "\u2705 Created .mise/scripts/codegraph-wire.sh"),
|
|
494
678
|
filePath
|
|
495
679
|
};
|
|
496
680
|
}
|
|
497
681
|
};
|
|
498
682
|
|
|
683
|
+
// src/commands/WireMiseOpInject.ts
|
|
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";
|
|
686
|
+
var WireMiseOpInject = class _WireMiseOpInject extends Command {
|
|
687
|
+
static MARKER = "# pjangler:op-inject";
|
|
688
|
+
static CR = "{{config_root}}";
|
|
689
|
+
async invoke() {
|
|
690
|
+
const scriptPath = ".mise/scripts/inject-op-secrets.sh";
|
|
691
|
+
const fullScriptPath = join6(this.context.targetDir, scriptPath);
|
|
692
|
+
const scriptContent = `#!/usr/bin/env bash
|
|
693
|
+
# Resolve 1Password \`op://\` references in .env.op into .env.secrets, which mise
|
|
694
|
+
# loads via \`_.file\`.
|
|
695
|
+
#
|
|
696
|
+
# Runs from the mise enter hook: fail-open and non-interactive.
|
|
697
|
+
# No .env.op means no 1Password, so this exits before spending anything.
|
|
698
|
+
|
|
699
|
+
set -euo pipefail
|
|
700
|
+
|
|
701
|
+
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
702
|
+
SRC="$REPO_ROOT/.env.op"
|
|
703
|
+
OUT="$REPO_ROOT/.env.secrets"
|
|
704
|
+
|
|
705
|
+
TTL_HOURS="\${OP_INJECT_TTL_HOURS:-12}"
|
|
706
|
+
|
|
707
|
+
[[ -f "$SRC" ]] || exit 0
|
|
708
|
+
command -v op >/dev/null 2>&1 || exit 0
|
|
709
|
+
|
|
710
|
+
if [[ -f "$OUT" && "$SRC" -ot "$OUT" ]]; then
|
|
711
|
+
if [[ "$TTL_HOURS" != "0" ]] && [[ -z "$(find "$OUT" -mmin "+$((TTL_HOURS * 60))" 2>/dev/null)" ]]; then
|
|
712
|
+
exit 0
|
|
713
|
+
fi
|
|
714
|
+
fi
|
|
715
|
+
|
|
716
|
+
op account list >/dev/null 2>&1 || exit 0
|
|
717
|
+
|
|
718
|
+
tmp="$(mktemp "\${OUT}.XXXXXX")"
|
|
719
|
+
trap 'rm -f "$tmp"' EXIT
|
|
720
|
+
chmod 600 "$tmp"
|
|
721
|
+
|
|
722
|
+
if op inject -i "$SRC" -o "$tmp" --force >/dev/null 2>&1; then
|
|
723
|
+
mv "$tmp" "$OUT"
|
|
724
|
+
trap - EXIT
|
|
725
|
+
else
|
|
726
|
+
echo "mise: op inject from .env.op failed \u2014 $(basename "$OUT") unchanged." >&2
|
|
727
|
+
echo " Check 'op signin', or that .env.op's op:// items are readable." >&2
|
|
728
|
+
echo " To disable: rm .env.op" >&2
|
|
729
|
+
fi
|
|
730
|
+
`;
|
|
731
|
+
this.writeFile(scriptPath, scriptContent);
|
|
732
|
+
if (!this.context.dryRun) {
|
|
733
|
+
chmodSync3(fullScriptPath, 493);
|
|
734
|
+
}
|
|
735
|
+
const misePath = join6(this.context.targetDir, "mise.toml");
|
|
736
|
+
if (!existsSync3(misePath)) {
|
|
737
|
+
return {
|
|
738
|
+
success: false,
|
|
739
|
+
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj init mise` first, then re-run."
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
let content = readFileSync2(misePath, "utf8");
|
|
743
|
+
if (content.includes(_WireMiseOpInject.MARKER)) {
|
|
744
|
+
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for op-inject") };
|
|
745
|
+
}
|
|
746
|
+
const cr = _WireMiseOpInject.CR;
|
|
747
|
+
const envRe = /\[env\]\s*\n([\s\S]*?(?=\n\[|$))/;
|
|
748
|
+
if (envRe.test(content)) {
|
|
749
|
+
content = content.replace(envRe, (m) => {
|
|
750
|
+
let block = m;
|
|
751
|
+
if (!block.includes("_.file")) {
|
|
752
|
+
block += `
|
|
753
|
+
_.file = ['.env', '.env.secrets']
|
|
754
|
+
`;
|
|
755
|
+
} else if (!block.includes(".env.secrets")) {
|
|
756
|
+
block = block.replace(/(_\.file\s*=\s*\[)([^\]]*?)(\])/, (m2, prefix, files, suffix) => {
|
|
757
|
+
const added = files.trim() ? `${files}, '.env.secrets'` : `'.env.secrets'`;
|
|
758
|
+
return `${prefix}${added}${suffix}`;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return block;
|
|
762
|
+
});
|
|
763
|
+
} else {
|
|
764
|
+
const envBlock = `[env]
|
|
765
|
+
_.file = ['.env', '.env.secrets']
|
|
766
|
+
|
|
767
|
+
`;
|
|
768
|
+
if (content.includes("[tools]")) {
|
|
769
|
+
content = content.replace(/(\[tools\][\s\S]*?(?=\n\[|$))/, `$1
|
|
770
|
+
${envBlock}`);
|
|
771
|
+
} else {
|
|
772
|
+
content = envBlock + content;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const enterAdds = ` '${cr}/.mise/scripts/inject-op-secrets.sh',`;
|
|
776
|
+
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
777
|
+
if (enterRe.test(content)) {
|
|
778
|
+
content = content.replace(enterRe, (_m, head, close) => {
|
|
779
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
780
|
+
return `${head}${sep2}
|
|
781
|
+
${enterAdds}${close}`;
|
|
782
|
+
});
|
|
783
|
+
} else {
|
|
784
|
+
if (content.includes("[hooks]")) {
|
|
785
|
+
content = content.replace(/\[hooks\]/, `[hooks]
|
|
786
|
+
enter = [
|
|
787
|
+
${enterAdds}
|
|
788
|
+
]`);
|
|
789
|
+
} else {
|
|
790
|
+
content += `
|
|
791
|
+
[hooks]
|
|
792
|
+
enter = [
|
|
793
|
+
${enterAdds}
|
|
794
|
+
]
|
|
795
|
+
`;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const appended = [
|
|
799
|
+
"",
|
|
800
|
+
_WireMiseOpInject.MARKER,
|
|
801
|
+
"[tasks.secrets-inject]",
|
|
802
|
+
'description = "Re-resolve .env.op secrets from 1Password into .env.secrets"',
|
|
803
|
+
`run = "OP_INJECT_TTL_HOURS=0 .mise/scripts/inject-op-secrets.sh"`,
|
|
804
|
+
_WireMiseOpInject.MARKER + ":end",
|
|
805
|
+
""
|
|
806
|
+
].join("\n");
|
|
807
|
+
content = content.replace(/\n*$/, "\n") + appended;
|
|
808
|
+
if (!this.context.dryRun) writeFileSync3(misePath, content);
|
|
809
|
+
return {
|
|
810
|
+
success: true,
|
|
811
|
+
message: this.formatMessage("\u2705 Wired mise.toml for op-inject (_.file, [hooks] enter, tasks.secrets-inject)")
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
|
|
499
816
|
// src/recipes/MiseRecipe.ts
|
|
500
817
|
var MiseRecipe = class extends Recipe {
|
|
501
818
|
constructor(context) {
|
|
502
819
|
super(context);
|
|
503
|
-
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
|
|
820
|
+
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript).addIngredient(AddMiseCodegraphWireScript).addIngredient(WireMiseOpInject);
|
|
504
821
|
}
|
|
505
822
|
printNextSteps() {
|
|
506
823
|
console.log("\u{1F389} Mise subsystem initialized successfully!");
|
|
@@ -728,12 +1045,12 @@ var NodeRecipe = class extends Recipe {
|
|
|
728
1045
|
};
|
|
729
1046
|
|
|
730
1047
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
731
|
-
import { basename, join as
|
|
732
|
-
import { readFileSync } from "node:fs";
|
|
1048
|
+
import { basename as basename2, join as join7 } from "node:path";
|
|
1049
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
733
1050
|
import * as p from "@clack/prompts";
|
|
734
1051
|
function detectTicketProvider(targetDir) {
|
|
735
1052
|
try {
|
|
736
|
-
const t = JSON.parse(
|
|
1053
|
+
const t = JSON.parse(readFileSync3(join7(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
737
1054
|
return t === "plane" || t === "trello" ? t : void 0;
|
|
738
1055
|
} catch {
|
|
739
1056
|
return void 0;
|
|
@@ -742,7 +1059,7 @@ function detectTicketProvider(targetDir) {
|
|
|
742
1059
|
var PromptForAgentConfig = class extends Command {
|
|
743
1060
|
async invoke() {
|
|
744
1061
|
const ctx = this.context;
|
|
745
|
-
const defaultRepo =
|
|
1062
|
+
const defaultRepo = basename2(ctx.targetDir).toLowerCase();
|
|
746
1063
|
ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
|
|
747
1064
|
ctx.role ??= "pm";
|
|
748
1065
|
ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
|
|
@@ -791,8 +1108,8 @@ var PromptForAgentConfig = class extends Command {
|
|
|
791
1108
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
792
1109
|
import { spawnSync } from "node:child_process";
|
|
793
1110
|
import { homedir as homedir2 } from "node:os";
|
|
794
|
-
import { join as
|
|
795
|
-
import { existsSync as
|
|
1111
|
+
import { join as join8, dirname as dirname3 } from "node:path";
|
|
1112
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
796
1113
|
import { fileURLToPath } from "node:url";
|
|
797
1114
|
import * as p2 from "@clack/prompts";
|
|
798
1115
|
function resolveVendoredTemplate(name) {
|
|
@@ -803,8 +1120,8 @@ function resolveVendoredTemplate(name) {
|
|
|
803
1120
|
return void 0;
|
|
804
1121
|
}
|
|
805
1122
|
for (let i = 0; i < 8; i++) {
|
|
806
|
-
const candidate =
|
|
807
|
-
if (
|
|
1123
|
+
const candidate = join8(dir, "templates", name);
|
|
1124
|
+
if (existsSync4(join8(candidate, "copier.yml"))) return candidate;
|
|
808
1125
|
const parent = dirname3(dir);
|
|
809
1126
|
if (parent === dir) break;
|
|
810
1127
|
dir = parent;
|
|
@@ -823,7 +1140,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
823
1140
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
824
1141
|
};
|
|
825
1142
|
}
|
|
826
|
-
const roleDir =
|
|
1143
|
+
const roleDir = join8(ctx.targetDir, "agents", "hermes", role);
|
|
827
1144
|
ctx.roleDir = roleDir;
|
|
828
1145
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
829
1146
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -833,7 +1150,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
833
1150
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
834
1151
|
};
|
|
835
1152
|
}
|
|
836
|
-
if (
|
|
1153
|
+
if (existsSync4(join8(roleDir, "role.yaml")) && !ctx.force) {
|
|
837
1154
|
if (ctx.yes) {
|
|
838
1155
|
ctx.force = true;
|
|
839
1156
|
} else {
|
|
@@ -854,15 +1171,16 @@ var RunCopierTemplate = class extends Command {
|
|
|
854
1171
|
...process.env,
|
|
855
1172
|
SKIP_TELEGRAM: "1",
|
|
856
1173
|
SKIP_EMAIL: "1",
|
|
857
|
-
//
|
|
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.
|
|
858
1176
|
SKIP_RUNTIME_REPO: ctx.skipRuntimeRepo ? "1" : "0",
|
|
859
1177
|
SKIP_PLANE: ctx.skipPlane ? "1" : "0",
|
|
860
|
-
SKIP_BLOODBANK:
|
|
1178
|
+
SKIP_BLOODBANK: "1",
|
|
861
1179
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
862
1180
|
};
|
|
863
|
-
const LOCAL_TEMPLATE =
|
|
1181
|
+
const LOCAL_TEMPLATE = join8(homedir2(), "code", "hermes-agent-template");
|
|
864
1182
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
865
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (
|
|
1183
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join8(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
866
1184
|
const args = [
|
|
867
1185
|
"copy",
|
|
868
1186
|
templateSrc,
|
|
@@ -893,7 +1211,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
893
1211
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
894
1212
|
};
|
|
895
1213
|
}
|
|
896
|
-
mkdirSync3(
|
|
1214
|
+
mkdirSync3(join8(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
897
1215
|
const spinner4 = p2.spinner();
|
|
898
1216
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
899
1217
|
const result = spawnSync("copier", args, {
|
|
@@ -917,20 +1235,26 @@ var RunCopierTemplate = class extends Command {
|
|
|
917
1235
|
};
|
|
918
1236
|
|
|
919
1237
|
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
920
|
-
import { existsSync as
|
|
921
|
-
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";
|
|
922
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
|
+
}
|
|
923
1247
|
var UntrackHermesRuntimes = class extends Command {
|
|
924
1248
|
async invoke() {
|
|
925
1249
|
const targetDir = this.context.targetDir;
|
|
926
|
-
const rolesDir =
|
|
927
|
-
if (!
|
|
1250
|
+
const rolesDir = join9(targetDir, "agents", "hermes");
|
|
1251
|
+
if (!existsSync5(rolesDir)) {
|
|
928
1252
|
return {
|
|
929
1253
|
success: true,
|
|
930
1254
|
message: "No Hermes agents found (no agents/hermes directory)."
|
|
931
1255
|
};
|
|
932
1256
|
}
|
|
933
|
-
const roles =
|
|
1257
|
+
const roles = readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
934
1258
|
if (roles.length === 0) {
|
|
935
1259
|
return {
|
|
936
1260
|
success: true,
|
|
@@ -940,53 +1264,85 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
940
1264
|
let modifiedAny = false;
|
|
941
1265
|
const details = [];
|
|
942
1266
|
for (const role of roles) {
|
|
943
|
-
const roleDir =
|
|
944
|
-
const runtimePath =
|
|
945
|
-
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");
|
|
946
1271
|
let isTracked = false;
|
|
947
|
-
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
1272
|
+
const lsResult = spawnSync2("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
948
1273
|
cwd: targetDir,
|
|
949
1274
|
encoding: "utf8"
|
|
950
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
|
+
}
|
|
951
1282
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
952
1283
|
isTracked = true;
|
|
953
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
|
+
}
|
|
954
1292
|
let isIgnored = false;
|
|
955
|
-
const fullGitignorePath =
|
|
956
|
-
if (
|
|
957
|
-
const content =
|
|
1293
|
+
const fullGitignorePath = join9(targetDir, gitignorePath);
|
|
1294
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1295
|
+
const content = readFileSync4(fullGitignorePath, "utf8");
|
|
958
1296
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
959
1297
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
960
1298
|
}
|
|
961
|
-
if (isTracked || !isIgnored) {
|
|
1299
|
+
if (isTracked || hasStaleMapping || !isIgnored) {
|
|
962
1300
|
modifiedAny = true;
|
|
963
1301
|
if (isTracked) {
|
|
964
1302
|
details.push(`untrack agents/hermes/${role}/runtime`);
|
|
965
1303
|
if (!this.context.dryRun) {
|
|
966
|
-
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
|
|
1304
|
+
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
|
|
967
1305
|
cwd: targetDir,
|
|
968
1306
|
encoding: "utf8"
|
|
969
1307
|
});
|
|
970
1308
|
if (rmResult.status !== 0) {
|
|
971
1309
|
return {
|
|
972
1310
|
success: false,
|
|
973
|
-
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}`
|
|
974
1322
|
};
|
|
975
1323
|
}
|
|
976
1324
|
}
|
|
977
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
|
+
}
|
|
978
1334
|
if (!isIgnored) {
|
|
979
1335
|
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
980
1336
|
if (!this.context.dryRun) {
|
|
981
1337
|
let content = "";
|
|
982
|
-
if (
|
|
983
|
-
content =
|
|
1338
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1339
|
+
content = readFileSync4(fullGitignorePath, "utf8");
|
|
984
1340
|
}
|
|
985
1341
|
if (content && !content.endsWith("\n")) {
|
|
986
1342
|
content += "\n";
|
|
987
1343
|
}
|
|
988
1344
|
content += "runtime/\n";
|
|
989
|
-
|
|
1345
|
+
writeFileSync4(fullGitignorePath, content, "utf8");
|
|
990
1346
|
}
|
|
991
1347
|
}
|
|
992
1348
|
}
|
|
@@ -1008,8 +1364,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
|
|
|
1008
1364
|
|
|
1009
1365
|
// src/commands/hermes/WireTelegram.ts
|
|
1010
1366
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1011
|
-
import { join as
|
|
1012
|
-
import { existsSync as
|
|
1367
|
+
import { join as join10 } from "node:path";
|
|
1368
|
+
import { existsSync as existsSync6, unlinkSync } from "node:fs";
|
|
1013
1369
|
import * as p3 from "@clack/prompts";
|
|
1014
1370
|
var WireTelegram = class extends Command {
|
|
1015
1371
|
async invoke() {
|
|
@@ -1097,15 +1453,15 @@ var WireTelegram = class extends Command {
|
|
|
1097
1453
|
if (p3.isCancel(allowedAnswer)) {
|
|
1098
1454
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1099
1455
|
}
|
|
1100
|
-
const script =
|
|
1101
|
-
if (!
|
|
1456
|
+
const script = join10(roleDir, ".scripts", "30-telegram.sh");
|
|
1457
|
+
if (!existsSync6(script)) {
|
|
1102
1458
|
return {
|
|
1103
1459
|
success: false,
|
|
1104
1460
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1105
1461
|
};
|
|
1106
1462
|
}
|
|
1107
|
-
const marker =
|
|
1108
|
-
if (
|
|
1463
|
+
const marker = join10(roleDir, ".scripts", ".done-30-telegram");
|
|
1464
|
+
if (existsSync6(marker)) unlinkSync(marker);
|
|
1109
1465
|
const spinner4 = p3.spinner();
|
|
1110
1466
|
spinner4.start("Verifying token + wiring profile");
|
|
1111
1467
|
const result = spawnSync3("bash", [script], {
|
|
@@ -1132,8 +1488,8 @@ function cap(s) {
|
|
|
1132
1488
|
|
|
1133
1489
|
// src/commands/hermes/WireEmail.ts
|
|
1134
1490
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1135
|
-
import { join as
|
|
1136
|
-
import { existsSync as
|
|
1491
|
+
import { join as join11 } from "node:path";
|
|
1492
|
+
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1137
1493
|
import * as p4 from "@clack/prompts";
|
|
1138
1494
|
var WireEmail = class extends Command {
|
|
1139
1495
|
async invoke() {
|
|
@@ -1148,8 +1504,8 @@ var WireEmail = class extends Command {
|
|
|
1148
1504
|
if (!targetRepo || !role || !roleDir) {
|
|
1149
1505
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1150
1506
|
}
|
|
1151
|
-
const script =
|
|
1152
|
-
if (!
|
|
1507
|
+
const script = join11(roleDir, ".scripts", "50-email.sh");
|
|
1508
|
+
if (!existsSync7(script)) {
|
|
1153
1509
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1154
1510
|
}
|
|
1155
1511
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
@@ -1211,8 +1567,8 @@ var WireEmail = class extends Command {
|
|
|
1211
1567
|
}
|
|
1212
1568
|
}
|
|
1213
1569
|
}
|
|
1214
|
-
const marker =
|
|
1215
|
-
if (
|
|
1570
|
+
const marker = join11(roleDir, ".scripts", ".done-50-email");
|
|
1571
|
+
if (existsSync7(marker)) unlinkSync2(marker);
|
|
1216
1572
|
const spinner4 = p4.spinner();
|
|
1217
1573
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1218
1574
|
const result = spawnSync4("bash", [script], {
|
|
@@ -1240,7 +1596,6 @@ var PrintHermesSummary = class extends Command {
|
|
|
1240
1596
|
const botHandle = `${targetRepo?.toLowerCase().replace(/-/g, "_")}_${role?.toLowerCase()}_bot`;
|
|
1241
1597
|
const email = `${targetRepo}-${role}@delo.sh`;
|
|
1242
1598
|
const gw = `hermes-${agentId}-gateway.service`;
|
|
1243
|
-
const csm = `hermes-${agentId}-consumer.service`;
|
|
1244
1599
|
const hb = `hermes-${agentId}-heartbeat.timer`;
|
|
1245
1600
|
const lines = [];
|
|
1246
1601
|
lines.push(`agent_id ${agentId}`);
|
|
@@ -1250,13 +1605,13 @@ var PrintHermesSummary = class extends Command {
|
|
|
1250
1605
|
if (!skipEmail) lines.push(`email ${email}`);
|
|
1251
1606
|
lines.push("");
|
|
1252
1607
|
lines.push("Start daemons:");
|
|
1253
|
-
lines.push(` systemctl --user start ${csm}`);
|
|
1254
1608
|
lines.push(` systemctl --user start ${hb}`);
|
|
1255
1609
|
if (!skipTelegram) {
|
|
1256
1610
|
lines.push(` systemctl --user start ${gw}`);
|
|
1257
1611
|
} else {
|
|
1258
1612
|
lines.push(` # gateway needs Telegram wired first (re-run with --skip-telegram=0)`);
|
|
1259
1613
|
}
|
|
1614
|
+
lines.push(" # Bloodbank commands arrive through the fleet-shared Hermes gateway");
|
|
1260
1615
|
lines.push("");
|
|
1261
1616
|
lines.push("Talk locally:");
|
|
1262
1617
|
lines.push(` ${ctx.roleDir}/hermes chat "status"`);
|
|
@@ -1299,15 +1654,15 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1299
1654
|
|
|
1300
1655
|
// src/commands/AgentHooksCommands.ts
|
|
1301
1656
|
import { homedir as homedir4 } from "node:os";
|
|
1302
|
-
import { join as
|
|
1303
|
-
import { existsSync 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";
|
|
1304
1659
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1305
1660
|
|
|
1306
1661
|
// src/project/index.ts
|
|
1307
1662
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1308
|
-
import { existsSync as
|
|
1663
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1309
1664
|
import { homedir as homedir3 } from "node:os";
|
|
1310
|
-
import { basename as
|
|
1665
|
+
import { basename as basename3, delimiter, dirname as dirname4, join as join12, resolve as resolve2 } from "node:path";
|
|
1311
1666
|
import YAML2 from "yaml";
|
|
1312
1667
|
|
|
1313
1668
|
// src/project/RegistryStore.ts
|
|
@@ -1542,18 +1897,18 @@ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
|
1542
1897
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1543
1898
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1544
1899
|
"/home/delorenj/code/skillex/all-skills",
|
|
1545
|
-
|
|
1546
|
-
|
|
1900
|
+
join12(homedir3(), ".agents", "skills"),
|
|
1901
|
+
join12(homedir3(), ".codex", "skills")
|
|
1547
1902
|
];
|
|
1548
1903
|
function projectRegistryPath(env2 = process.env) {
|
|
1549
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1904
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join12(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1550
1905
|
}
|
|
1551
1906
|
function emptyProjectRegistry() {
|
|
1552
1907
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1553
1908
|
}
|
|
1554
1909
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1555
|
-
if (!
|
|
1556
|
-
const raw = YAML2.parse(
|
|
1910
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
1911
|
+
const raw = YAML2.parse(readFileSync5(path, "utf8"));
|
|
1557
1912
|
if (raw == null) return emptyProjectRegistry();
|
|
1558
1913
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1559
1914
|
const registry = raw;
|
|
@@ -1568,7 +1923,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1568
1923
|
validateProjectRegistry(registry);
|
|
1569
1924
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1570
1925
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1571
|
-
|
|
1926
|
+
writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1572
1927
|
renameSync(temp, path);
|
|
1573
1928
|
}
|
|
1574
1929
|
function validateProjectRegistry(registry) {
|
|
@@ -1583,7 +1938,7 @@ function validateProjectRegistry(registry) {
|
|
|
1583
1938
|
validateProjectRecord(project, slug);
|
|
1584
1939
|
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
1585
1940
|
slugs.add(project.slug);
|
|
1586
|
-
const repoKey =
|
|
1941
|
+
const repoKey = resolve2(project.repo_path);
|
|
1587
1942
|
const existingRepoSlug = repoPaths.get(repoKey);
|
|
1588
1943
|
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
1589
1944
|
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
@@ -1650,7 +2005,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1650
2005
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1651
2006
|
if (override === "0" || override === "false") return false;
|
|
1652
2007
|
if (override === "1" || override === "true") return true;
|
|
1653
|
-
return !
|
|
2008
|
+
return !existsSync8(join12(homedir3(), ".agents", "hooks"));
|
|
1654
2009
|
}
|
|
1655
2010
|
function jsonStable(value) {
|
|
1656
2011
|
return JSON.stringify(value);
|
|
@@ -1663,14 +2018,14 @@ function projectRecordEquivalent(a, b) {
|
|
|
1663
2018
|
}
|
|
1664
2019
|
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
1665
2020
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1666
|
-
return
|
|
2021
|
+
return resolve2(dirname4(resolve2(cwd)), compactName);
|
|
1667
2022
|
}
|
|
1668
2023
|
function sourceSkillRoots(env2 = process.env) {
|
|
1669
2024
|
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
|
|
1670
2025
|
const seen = /* @__PURE__ */ new Set();
|
|
1671
2026
|
const roots = [];
|
|
1672
2027
|
for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
|
|
1673
|
-
const normalized =
|
|
2028
|
+
const normalized = resolve2(expandHome(root));
|
|
1674
2029
|
if (seen.has(normalized)) continue;
|
|
1675
2030
|
seen.add(normalized);
|
|
1676
2031
|
roots.push(normalized);
|
|
@@ -1680,13 +2035,13 @@ function sourceSkillRoots(env2 = process.env) {
|
|
|
1680
2035
|
function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
1681
2036
|
if (!sourceSkill) return void 0;
|
|
1682
2037
|
const expanded = expandHome(sourceSkill);
|
|
1683
|
-
const direct =
|
|
1684
|
-
if (
|
|
1685
|
-
const name =
|
|
2038
|
+
const direct = resolve2(expanded);
|
|
2039
|
+
if (existsSync8(direct)) return direct;
|
|
2040
|
+
const name = basename3(sourceSkill);
|
|
1686
2041
|
const roots = sourceSkillRoots(env2);
|
|
1687
2042
|
for (const root of roots) {
|
|
1688
|
-
const candidate =
|
|
1689
|
-
if (
|
|
2043
|
+
const candidate = join12(root, name);
|
|
2044
|
+
if (existsSync8(candidate)) return candidate;
|
|
1690
2045
|
}
|
|
1691
2046
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1692
2047
|
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
@@ -1694,11 +2049,11 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1694
2049
|
}
|
|
1695
2050
|
function planProjectInit(input) {
|
|
1696
2051
|
if (!input.name.trim()) throw new Error("Project name is required");
|
|
1697
|
-
const registryPath2 =
|
|
2052
|
+
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
1698
2053
|
const registry = loadProjectRegistry(registryPath2);
|
|
1699
2054
|
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1700
2055
|
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
1701
|
-
const targetDir =
|
|
2056
|
+
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
1702
2057
|
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
1703
2058
|
const existing = registry.projects[slug];
|
|
1704
2059
|
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
@@ -1741,7 +2096,7 @@ function planProjectInit(input) {
|
|
|
1741
2096
|
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
1742
2097
|
};
|
|
1743
2098
|
validateNoDuplicateProject(registry, project, overwrite);
|
|
1744
|
-
const pjanglerRoot =
|
|
2099
|
+
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot());
|
|
1745
2100
|
const manifest = projectManifestFromRegistryProject(project);
|
|
1746
2101
|
const apply = input.apply ?? false;
|
|
1747
2102
|
const live = input.live ?? false;
|
|
@@ -1767,7 +2122,7 @@ function planProjectInit(input) {
|
|
|
1767
2122
|
}));
|
|
1768
2123
|
}
|
|
1769
2124
|
actions.push(
|
|
1770
|
-
{ kind: "project.write-manifest", path:
|
|
2125
|
+
{ kind: "project.write-manifest", path: join12(targetDir, ".project.json"), manifest },
|
|
1771
2126
|
{
|
|
1772
2127
|
kind: "ticket-provider.create-or-link",
|
|
1773
2128
|
enabled: live,
|
|
@@ -1820,7 +2175,7 @@ async function executeProjectInitPlan(plan) {
|
|
|
1820
2175
|
}
|
|
1821
2176
|
if (result.status !== 0) {
|
|
1822
2177
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1823
|
-
if (
|
|
2178
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1824
2179
|
break;
|
|
1825
2180
|
}
|
|
1826
2181
|
changedFiles.push(action.targetDir);
|
|
@@ -1828,9 +2183,9 @@ async function executeProjectInitPlan(plan) {
|
|
|
1828
2183
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1829
2184
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1830
2185
|
`;
|
|
1831
|
-
const current =
|
|
2186
|
+
const current = existsSync8(action.path) ? readFileSync5(action.path, "utf8") : void 0;
|
|
1832
2187
|
if (current !== next) {
|
|
1833
|
-
|
|
2188
|
+
writeFileSync5(action.path, next, "utf8");
|
|
1834
2189
|
changedFiles.push(action.path);
|
|
1835
2190
|
}
|
|
1836
2191
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1933,16 +2288,16 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1933
2288
|
const registry = loadProjectRegistry(registryPath2);
|
|
1934
2289
|
const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
|
|
1935
2290
|
for (const [projectSlug, project] of projects) {
|
|
1936
|
-
if (!
|
|
2291
|
+
if (!existsSync8(project.repo_path)) {
|
|
1937
2292
|
issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
|
|
1938
2293
|
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
1939
2294
|
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
1940
2295
|
} else {
|
|
1941
|
-
const manifestPath =
|
|
1942
|
-
if (!
|
|
2296
|
+
const manifestPath = join12(project.repo_path, ".project.json");
|
|
2297
|
+
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
1943
2298
|
}
|
|
1944
2299
|
for (const artifact of project.source_artifacts) {
|
|
1945
|
-
if (artifact.path && !
|
|
2300
|
+
if (artifact.path && !existsSync8(artifact.path)) {
|
|
1946
2301
|
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
1947
2302
|
}
|
|
1948
2303
|
}
|
|
@@ -1955,7 +2310,7 @@ function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
|
1955
2310
|
};
|
|
1956
2311
|
}
|
|
1957
2312
|
function buildCommonProjectCopierAction(input) {
|
|
1958
|
-
const templateDir =
|
|
2313
|
+
const templateDir = join12(input.pjanglerRoot, "templates", "commonproject");
|
|
1959
2314
|
const data = {
|
|
1960
2315
|
project_name: input.projectName,
|
|
1961
2316
|
project_description: input.projectDescription ?? "",
|
|
@@ -1984,19 +2339,19 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1984
2339
|
function resolvePjanglerRoot() {
|
|
1985
2340
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1986
2341
|
while (dir !== dirname4(dir)) {
|
|
1987
|
-
if (
|
|
2342
|
+
if (existsSync8(join12(dir, "package.json")) && existsSync8(join12(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1988
2343
|
dir = dirname4(dir);
|
|
1989
2344
|
}
|
|
1990
|
-
return
|
|
2345
|
+
return resolve2(process.cwd());
|
|
1991
2346
|
}
|
|
1992
2347
|
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
1993
2348
|
const existingSameSlug = registry.projects[project.slug];
|
|
1994
|
-
if (existingSameSlug && !overwrite &&
|
|
2349
|
+
if (existingSameSlug && !overwrite && resolve2(existingSameSlug.repo_path) !== resolve2(project.repo_path)) {
|
|
1995
2350
|
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
1996
2351
|
}
|
|
1997
2352
|
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
1998
2353
|
if (slug === project.slug) continue;
|
|
1999
|
-
if (
|
|
2354
|
+
if (resolve2(existing.repo_path) === resolve2(project.repo_path)) {
|
|
2000
2355
|
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
2001
2356
|
}
|
|
2002
2357
|
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
@@ -2016,7 +2371,7 @@ function validateProjectRecord(project, key) {
|
|
|
2016
2371
|
}
|
|
2017
2372
|
function expandHome(path) {
|
|
2018
2373
|
if (path === "~") return homedir3();
|
|
2019
|
-
if (path.startsWith("~/")) return
|
|
2374
|
+
if (path.startsWith("~/")) return join12(homedir3(), path.slice(2));
|
|
2020
2375
|
return path;
|
|
2021
2376
|
}
|
|
2022
2377
|
function isRecord(value) {
|
|
@@ -2033,16 +2388,16 @@ function resolveTemplateRoot() {
|
|
|
2033
2388
|
try {
|
|
2034
2389
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
2035
2390
|
for (let i = 0; i < 8; i++) {
|
|
2036
|
-
candidates.push(
|
|
2391
|
+
candidates.push(join13(dir, "templates", "commonproject", "template"));
|
|
2037
2392
|
const parent = dirname5(dir);
|
|
2038
2393
|
if (parent === dir) break;
|
|
2039
2394
|
dir = parent;
|
|
2040
2395
|
}
|
|
2041
2396
|
} catch {
|
|
2042
2397
|
}
|
|
2043
|
-
candidates.push(
|
|
2398
|
+
candidates.push(join13(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
2044
2399
|
for (const c of candidates) {
|
|
2045
|
-
if (
|
|
2400
|
+
if (existsSync9(join13(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
2046
2401
|
}
|
|
2047
2402
|
throw new Error(
|
|
2048
2403
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -2061,18 +2416,17 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
2061
2416
|
}
|
|
2062
2417
|
const items = [
|
|
2063
2418
|
{ rel: ".agents/hooks", dir: true },
|
|
2419
|
+
{ rel: ".agents/skills.json", dir: false },
|
|
2064
2420
|
{ rel: ".agents/local.example.json", dir: false },
|
|
2065
|
-
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
2066
|
-
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
2067
2421
|
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
2068
2422
|
];
|
|
2069
2423
|
const created = [];
|
|
2070
2424
|
const skipped = [];
|
|
2071
2425
|
for (const { rel, dir } of items) {
|
|
2072
|
-
const src =
|
|
2073
|
-
const dest =
|
|
2074
|
-
if (!
|
|
2075
|
-
if (
|
|
2426
|
+
const src = join13(templateRoot, rel);
|
|
2427
|
+
const dest = join13(this.context.targetDir, rel);
|
|
2428
|
+
if (!existsSync9(src)) continue;
|
|
2429
|
+
if (existsSync9(dest) && !this.context.force) {
|
|
2076
2430
|
skipped.push(rel);
|
|
2077
2431
|
continue;
|
|
2078
2432
|
}
|
|
@@ -2098,25 +2452,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2098
2452
|
if (!resolveAgentHooksLayer()) {
|
|
2099
2453
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
2100
2454
|
}
|
|
2101
|
-
const misePath =
|
|
2102
|
-
if (!
|
|
2455
|
+
const misePath = join13(this.context.targetDir, "mise.toml");
|
|
2456
|
+
if (!existsSync9(misePath)) {
|
|
2103
2457
|
return {
|
|
2104
2458
|
success: false,
|
|
2105
2459
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
2106
2460
|
};
|
|
2107
2461
|
}
|
|
2108
|
-
let content =
|
|
2462
|
+
let content = readFileSync6(misePath, "utf8");
|
|
2109
2463
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
2110
2464
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
2111
2465
|
}
|
|
2112
2466
|
const cr = _WireMiseAgentHooks.CR;
|
|
2113
|
-
const enterAdds = [
|
|
2114
|
-
` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
|
|
2115
|
-
` "${cr}/.agents/hooks/sync.py --install --quiet",`
|
|
2116
|
-
].join("\n");
|
|
2467
|
+
const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
|
|
2117
2468
|
const leaveBlock = [
|
|
2118
2469
|
"leave = [",
|
|
2119
|
-
` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
|
|
2120
2470
|
` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
|
|
2121
2471
|
"]"
|
|
2122
2472
|
].join("\n");
|
|
@@ -2124,16 +2474,15 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2124
2474
|
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
2125
2475
|
if (enterRe.test(content)) {
|
|
2126
2476
|
content = content.replace(enterRe, (_m, head, close) => {
|
|
2127
|
-
const
|
|
2128
|
-
return `${head}${
|
|
2477
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2478
|
+
return `${head}${sep2}
|
|
2129
2479
|
${enterAdds}${close}`;
|
|
2130
2480
|
});
|
|
2131
2481
|
const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
2132
2482
|
if (leaveRe.test(content)) {
|
|
2133
2483
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
2134
|
-
const
|
|
2135
|
-
return `${head}${
|
|
2136
|
-
"${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
|
|
2484
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2485
|
+
return `${head}${sep2}
|
|
2137
2486
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
2138
2487
|
});
|
|
2139
2488
|
} else {
|
|
@@ -2146,6 +2495,14 @@ ${leaveBlock}`);
|
|
|
2146
2495
|
"",
|
|
2147
2496
|
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
2148
2497
|
"[[watch_files]]",
|
|
2498
|
+
'patterns = [".agents/skills.json"]',
|
|
2499
|
+
'task = "skills-sync"',
|
|
2500
|
+
"",
|
|
2501
|
+
"[tasks.skills-sync]",
|
|
2502
|
+
'description = "Sync skills from manifest to local CLI dirs"',
|
|
2503
|
+
'run = "sync-skills.py --scope project"',
|
|
2504
|
+
"",
|
|
2505
|
+
"[[watch_files]]",
|
|
2149
2506
|
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
2150
2507
|
'task = "hooks-sync"',
|
|
2151
2508
|
"",
|
|
@@ -2161,18 +2518,6 @@ ${leaveBlock}`);
|
|
|
2161
2518
|
'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
|
|
2162
2519
|
`run = "${cr}/.agents/hooks/sync.py --uninstall"`,
|
|
2163
2520
|
"",
|
|
2164
|
-
"[tasks.link-project-skills-to-clis]",
|
|
2165
|
-
'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
|
|
2166
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
2167
|
-
"",
|
|
2168
|
-
"[tasks.unlink-project-skills-from-clis]",
|
|
2169
|
-
'description = "Remove project skill symlinks from shared per-CLI dirs"',
|
|
2170
|
-
`run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
|
|
2171
|
-
"",
|
|
2172
|
-
"[tasks.skills-relink]",
|
|
2173
|
-
'description = "Re-fan the project skill set to all CLIs"',
|
|
2174
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
2175
|
-
"",
|
|
2176
2521
|
"[tasks.hindsight-setup]",
|
|
2177
2522
|
`description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
|
|
2178
2523
|
`run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
|
|
@@ -2181,7 +2526,7 @@ ${leaveBlock}`);
|
|
|
2181
2526
|
""
|
|
2182
2527
|
].join("\n");
|
|
2183
2528
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
2184
|
-
if (!this.context.dryRun)
|
|
2529
|
+
if (!this.context.dryRun) writeFileSync6(misePath, content);
|
|
2185
2530
|
if (wiredHooks) {
|
|
2186
2531
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
2187
2532
|
}
|
|
@@ -2190,8 +2535,8 @@ ${leaveBlock}`);
|
|
|
2190
2535
|
message: this.formatMessage(
|
|
2191
2536
|
`\u2705 Added agent-hooks tasks to mise.toml.
|
|
2192
2537
|
\u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
|
|
2193
|
-
enter += "
|
|
2194
|
-
leave += "${cr}/.
|
|
2538
|
+
enter += "sync-skills.py --scope project", "${cr}/.agents/hooks/sync.py --install --quiet"
|
|
2539
|
+
leave += "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
|
|
2195
2540
|
)
|
|
2196
2541
|
};
|
|
2197
2542
|
}
|
|
@@ -2206,10 +2551,25 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
2206
2551
|
printNextSteps() {
|
|
2207
2552
|
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
2208
2553
|
console.log(" Next steps:");
|
|
2209
|
-
console.log(" 1. mise run
|
|
2210
|
-
console.log(" 2.
|
|
2211
|
-
console.log(" 3.
|
|
2212
|
-
console.log(
|
|
2554
|
+
console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
|
|
2555
|
+
console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
2556
|
+
console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
|
|
2557
|
+
console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
2558
|
+
console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
|
|
2559
|
+
}
|
|
2560
|
+
};
|
|
2561
|
+
|
|
2562
|
+
// src/recipes/MiseOpInjectRecipe.ts
|
|
2563
|
+
var MiseOpInjectRecipe = class extends Recipe {
|
|
2564
|
+
constructor(context) {
|
|
2565
|
+
super(context);
|
|
2566
|
+
this.addIngredient(WireMiseOpInject);
|
|
2567
|
+
}
|
|
2568
|
+
printNextSteps() {
|
|
2569
|
+
console.log("\u{1F389} Wired up .env.op 1Password resolution via mise!");
|
|
2570
|
+
console.log(" Next steps:");
|
|
2571
|
+
console.log(" 1. Create .env.op with your op:// secret references");
|
|
2572
|
+
console.log(" 2. Run `mise run secrets-inject` or simply cd out and back in to trigger the hook");
|
|
2213
2573
|
}
|
|
2214
2574
|
};
|
|
2215
2575
|
|
|
@@ -2252,6 +2612,12 @@ var RECIPE_REGISTRY = {
|
|
|
2252
2612
|
description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
|
|
2253
2613
|
class: AgentHooksRecipe,
|
|
2254
2614
|
commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
|
|
2615
|
+
},
|
|
2616
|
+
"mise-op-inject": {
|
|
2617
|
+
name: "mise-op-inject",
|
|
2618
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2619
|
+
class: MiseOpInjectRecipe,
|
|
2620
|
+
commands: ["WireMiseOpInject"]
|
|
2255
2621
|
}
|
|
2256
2622
|
};
|
|
2257
2623
|
var COMMAND_REGISTRY = {
|
|
@@ -2320,6 +2686,12 @@ var COMMAND_REGISTRY = {
|
|
|
2320
2686
|
description: "Create .env.example file",
|
|
2321
2687
|
group: "environment",
|
|
2322
2688
|
class: AddDotenv
|
|
2689
|
+
},
|
|
2690
|
+
WireMiseOpInject: {
|
|
2691
|
+
name: "WireMiseOpInject",
|
|
2692
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2693
|
+
group: "mise",
|
|
2694
|
+
class: WireMiseOpInject
|
|
2323
2695
|
}
|
|
2324
2696
|
};
|
|
2325
2697
|
function getRecipeNames() {
|
|
@@ -2354,29 +2726,61 @@ function createRecipe(name, context) {
|
|
|
2354
2726
|
import { cancel as cancel2, multiselect, text as text2, isCancel as isCancel5 } from "@clack/prompts";
|
|
2355
2727
|
|
|
2356
2728
|
// src/parity/index.ts
|
|
2357
|
-
import { existsSync as
|
|
2358
|
-
import { basename as
|
|
2359
|
-
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";
|
|
2360
2732
|
import { homedir as homedir5 } from "node:os";
|
|
2361
2733
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2362
2734
|
import YAML3 from "yaml";
|
|
2363
2735
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2364
2736
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
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";
|
|
2365
2739
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
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";
|
|
2366
2752
|
var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
|
|
2367
2753
|
# agent files to the main AGENTS.md file.
|
|
2368
2754
|
#
|
|
2369
2755
|
# TODO: Ensure this works for all levels of nesting.
|
|
2370
2756
|
# i.e. All linked agent files MUST be siblings at
|
|
2371
2757
|
# any given level of nesting.`;
|
|
2372
|
-
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
|
+
];
|
|
2373
2764
|
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
2374
2765
|
patterns = ["AGENTS.md"]
|
|
2375
2766
|
task = "link-agentfiles"
|
|
2376
2767
|
|
|
2768
|
+
[[watch_files]]
|
|
2769
|
+
patterns = [".agents/skills.json"]
|
|
2770
|
+
task = "skills-sync"
|
|
2771
|
+
|
|
2377
2772
|
[tasks.link-agentfiles]
|
|
2378
2773
|
description = "Symlink all agent files to AGENTS.md"
|
|
2379
|
-
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2774
|
+
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2775
|
+
|
|
2776
|
+
[tasks.skills-sync]
|
|
2777
|
+
description = "Sync skills from manifest to local CLI dirs"
|
|
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'"`;
|
|
2380
2784
|
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
2381
2785
|
[tasks."version"]
|
|
2382
2786
|
description = "Print the current version (vX.Y.Z)"
|
|
@@ -2406,7 +2810,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
|
|
|
2406
2810
|
function resolvePjanglerRoot2() {
|
|
2407
2811
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2408
2812
|
while (dir !== dirname6(dir)) {
|
|
2409
|
-
if (
|
|
2813
|
+
if (existsSync10(join14(dir, "package.json")) && existsSync10(join14(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2410
2814
|
return dir;
|
|
2411
2815
|
}
|
|
2412
2816
|
dir = dirname6(dir);
|
|
@@ -2417,17 +2821,17 @@ function normalizeNewlines(value) {
|
|
|
2417
2821
|
return value.replace(/\r\n/g, "\n");
|
|
2418
2822
|
}
|
|
2419
2823
|
function readText(path) {
|
|
2420
|
-
return normalizeNewlines(
|
|
2824
|
+
return normalizeNewlines(readFileSync7(path, "utf8"));
|
|
2421
2825
|
}
|
|
2422
2826
|
function safeReadText(path) {
|
|
2423
|
-
return
|
|
2827
|
+
return existsSync10(path) ? readText(path) : null;
|
|
2424
2828
|
}
|
|
2425
2829
|
function ensureParent(path) {
|
|
2426
2830
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
2427
2831
|
}
|
|
2428
2832
|
function writeText(path, content) {
|
|
2429
2833
|
ensureParent(path);
|
|
2430
|
-
|
|
2834
|
+
writeFileSync7(path, content);
|
|
2431
2835
|
}
|
|
2432
2836
|
function tryParseJson(text3) {
|
|
2433
2837
|
if (!text3) return null;
|
|
@@ -2444,7 +2848,7 @@ function titleCaseSlug(slug) {
|
|
|
2444
2848
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2445
2849
|
}
|
|
2446
2850
|
function readSymlinkTarget(path) {
|
|
2447
|
-
if (!
|
|
2851
|
+
if (!existsSync10(path)) return null;
|
|
2448
2852
|
try {
|
|
2449
2853
|
return readlinkSync(path);
|
|
2450
2854
|
} catch {
|
|
@@ -2452,8 +2856,8 @@ function readSymlinkTarget(path) {
|
|
|
2452
2856
|
}
|
|
2453
2857
|
}
|
|
2454
2858
|
function ensureSymlink(path, target, dryRun) {
|
|
2455
|
-
if (
|
|
2456
|
-
const stat =
|
|
2859
|
+
if (existsSync10(path)) {
|
|
2860
|
+
const stat = lstatSync2(path);
|
|
2457
2861
|
if (stat.isSymbolicLink()) {
|
|
2458
2862
|
const current = readSymlinkTarget(path);
|
|
2459
2863
|
if (current === target) return { changed: false };
|
|
@@ -2463,18 +2867,18 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2463
2867
|
}
|
|
2464
2868
|
return { changed: true };
|
|
2465
2869
|
}
|
|
2466
|
-
return { changed: false, blocked: `${
|
|
2870
|
+
return { changed: false, blocked: `${relative2(process.cwd(), path) || path} exists and is not a symlink` };
|
|
2467
2871
|
}
|
|
2468
2872
|
if (!dryRun) symlinkSync(target, path);
|
|
2469
2873
|
return { changed: true };
|
|
2470
2874
|
}
|
|
2471
2875
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2472
|
-
const agentsPath =
|
|
2473
|
-
if (
|
|
2876
|
+
const agentsPath = join14(repoRoot, "AGENTS.md");
|
|
2877
|
+
if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
|
|
2474
2878
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2475
|
-
const source =
|
|
2476
|
-
if (!
|
|
2477
|
-
const stat =
|
|
2879
|
+
const source = join14(repoRoot, file);
|
|
2880
|
+
if (!existsSync10(source)) continue;
|
|
2881
|
+
const stat = lstatSync2(source);
|
|
2478
2882
|
if (stat.isSymbolicLink()) continue;
|
|
2479
2883
|
if (stat.isFile()) {
|
|
2480
2884
|
if (!dryRun) renameSync2(source, agentsPath);
|
|
@@ -2482,9 +2886,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2482
2886
|
}
|
|
2483
2887
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2484
2888
|
}
|
|
2485
|
-
const readmePath =
|
|
2486
|
-
if (
|
|
2487
|
-
const stat =
|
|
2889
|
+
const readmePath = join14(repoRoot, "README.md");
|
|
2890
|
+
if (existsSync10(readmePath)) {
|
|
2891
|
+
const stat = lstatSync2(readmePath);
|
|
2488
2892
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2489
2893
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
2490
2894
|
return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
|
|
@@ -2522,12 +2926,12 @@ function yamlGet(text3, keyPath) {
|
|
|
2522
2926
|
return "";
|
|
2523
2927
|
}
|
|
2524
2928
|
function discoverRoles(repoRoot) {
|
|
2525
|
-
const rolesDir =
|
|
2526
|
-
if (!
|
|
2527
|
-
return
|
|
2528
|
-
const roleDir =
|
|
2529
|
-
const roleYamlPath =
|
|
2530
|
-
if (!
|
|
2929
|
+
const rolesDir = join14(repoRoot, "agents", "hermes");
|
|
2930
|
+
if (!existsSync10(rolesDir)) return [];
|
|
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");
|
|
2934
|
+
if (!existsSync10(roleYamlPath)) return null;
|
|
2531
2935
|
const text3 = readText(roleYamlPath);
|
|
2532
2936
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
2533
2937
|
return {
|
|
@@ -2555,7 +2959,7 @@ function discoverRoles(repoRoot) {
|
|
|
2555
2959
|
}).filter((value) => Boolean(value));
|
|
2556
2960
|
}
|
|
2557
2961
|
function registryPath(homeDir) {
|
|
2558
|
-
return
|
|
2962
|
+
return join14(homeDir, ".hermes", "agents-registry.yaml");
|
|
2559
2963
|
}
|
|
2560
2964
|
function systemctlUser(args) {
|
|
2561
2965
|
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -2566,8 +2970,8 @@ function systemctlUser(args) {
|
|
|
2566
2970
|
};
|
|
2567
2971
|
}
|
|
2568
2972
|
function templateScript(ctx, name) {
|
|
2569
|
-
const source =
|
|
2570
|
-
return
|
|
2973
|
+
const source = join14(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2974
|
+
return existsSync10(source) ? readText(source) : void 0;
|
|
2571
2975
|
}
|
|
2572
2976
|
function templateVersioningScript(ctx) {
|
|
2573
2977
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2579,8 +2983,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2579
2983
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2580
2984
|
if (override === "0" || override === "false") return false;
|
|
2581
2985
|
if (override === "1" || override === "true") return true;
|
|
2582
|
-
if (
|
|
2583
|
-
return !
|
|
2986
|
+
if (existsSync10(join14(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2987
|
+
return !existsSync10(join14(ctx.homeDir, ".agents", "hooks"));
|
|
2584
2988
|
}
|
|
2585
2989
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2586
2990
|
const out = [];
|
|
@@ -2606,121 +3010,540 @@ function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
|
2606
3010
|
}
|
|
2607
3011
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
2608
3012
|
const project = readProjectJson(ctx);
|
|
2609
|
-
const projectName = String(project?.project_name ??
|
|
3013
|
+
const projectName = String(project?.project_name ?? basename4(ctx.repoRoot) ?? "project");
|
|
2610
3014
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2611
3015
|
}
|
|
2612
3016
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2613
|
-
const targetPath =
|
|
2614
|
-
if (
|
|
2615
|
-
const sourcePath =
|
|
2616
|
-
if (!
|
|
3017
|
+
const targetPath = join14(ctx.repoRoot, "mise.toml");
|
|
3018
|
+
if (existsSync10(targetPath)) return false;
|
|
3019
|
+
const sourcePath = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
3020
|
+
if (!existsSync10(sourcePath)) return false;
|
|
2617
3021
|
changedFiles.push(targetPath);
|
|
2618
3022
|
if (!ctx.dryRun) {
|
|
2619
3023
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
2620
3024
|
}
|
|
2621
3025
|
return true;
|
|
2622
3026
|
}
|
|
2623
|
-
function
|
|
2624
|
-
const
|
|
2625
|
-
return
|
|
3027
|
+
function templateCommonProjectText(ctx, rel) {
|
|
3028
|
+
const path = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
3029
|
+
return existsSync10(path) ? readText(path) : void 0;
|
|
2626
3030
|
}
|
|
2627
|
-
function
|
|
2628
|
-
if (
|
|
2629
|
-
|
|
2630
|
-
}
|
|
2631
|
-
if (beforePattern) {
|
|
2632
|
-
const match = text3.match(beforePattern);
|
|
2633
|
-
if (match && typeof match.index === "number") {
|
|
2634
|
-
return `${text3.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
2635
|
-
|
|
2636
|
-
${text3.slice(match.index)}`;
|
|
2637
|
-
}
|
|
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)}`);
|
|
2638
3034
|
}
|
|
2639
|
-
return
|
|
2640
|
-
|
|
2641
|
-
${block}
|
|
2642
|
-
`;
|
|
3035
|
+
return name;
|
|
2643
3036
|
}
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3037
|
+
function lstatIfPresent(path) {
|
|
3038
|
+
try {
|
|
3039
|
+
return lstatSync2(path);
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
if (error.code === "ENOENT") return void 0;
|
|
3042
|
+
throw error;
|
|
2650
3043
|
}
|
|
2651
|
-
return required;
|
|
2652
3044
|
}
|
|
2653
|
-
function
|
|
2654
|
-
const
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
if (!pathLine) {
|
|
2667
|
-
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
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}`);
|
|
2668
3058
|
}
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
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
|
+
}
|
|
2673
3070
|
}
|
|
2674
|
-
|
|
2675
|
-
if (pathLine[0] === nextLine) return text3;
|
|
2676
|
-
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
3071
|
+
return { agentsDir, skillsDir };
|
|
2677
3072
|
}
|
|
2678
|
-
function
|
|
2679
|
-
const
|
|
2680
|
-
let
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
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`);
|
|
2690
3117
|
}
|
|
3118
|
+
} catch {
|
|
3119
|
+
issues.push(`${rel} canonical alias is broken`);
|
|
2691
3120
|
}
|
|
2692
|
-
|
|
3121
|
+
continue;
|
|
2693
3122
|
}
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
end = j;
|
|
2698
|
-
break;
|
|
2699
|
-
}
|
|
3123
|
+
if (!stat.isDirectory()) {
|
|
3124
|
+
issues.push(`${rel} is not a directory`);
|
|
3125
|
+
continue;
|
|
2700
3126
|
}
|
|
2701
|
-
if (
|
|
2702
|
-
|
|
2703
|
-
}
|
|
2704
|
-
if (start === -1) return text3;
|
|
2705
|
-
while (end > start + 1 && (lines[end - 1].trim() === "" || lines[end - 1].trim().startsWith("#"))) {
|
|
2706
|
-
end--;
|
|
2707
|
-
}
|
|
2708
|
-
if (options?.includePrecedingComments) {
|
|
2709
|
-
while (start > 0 && lines[start - 1].trim().startsWith("#")) {
|
|
2710
|
-
start--;
|
|
3127
|
+
if (!isContainedBy(projectRoot, realOrSelf(cliDir))) {
|
|
3128
|
+
issues.push(`${rel} resolves outside the project`);
|
|
2711
3129
|
}
|
|
2712
3130
|
}
|
|
2713
|
-
|
|
2714
|
-
return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
3131
|
+
return issues;
|
|
2715
3132
|
}
|
|
2716
|
-
function
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
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;
|
|
2722
3161
|
}
|
|
2723
|
-
|
|
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);
|
|
3167
|
+
return `${JSON.stringify(
|
|
3168
|
+
{
|
|
3169
|
+
...current ?? {},
|
|
3170
|
+
$schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
|
|
3171
|
+
inherit_global: true,
|
|
3172
|
+
registry: SKILLS_REGISTRY_URL,
|
|
3173
|
+
skills: [
|
|
3174
|
+
...existing.filter((entry) => !isPackManagedManifestEntry(entry, expectedNames, packRoot)),
|
|
3175
|
+
...packSkills
|
|
3176
|
+
]
|
|
3177
|
+
},
|
|
3178
|
+
null,
|
|
3179
|
+
2
|
|
3180
|
+
)}
|
|
3181
|
+
`;
|
|
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
|
+
}
|
|
3446
|
+
function templateVersionFilesConf(ctx, repoRoot) {
|
|
3447
|
+
const packageJson = join14(repoRoot, "package.json");
|
|
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";
|
|
3449
|
+
}
|
|
3450
|
+
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
3451
|
+
if (startMarker.test(text3)) {
|
|
3452
|
+
return text3.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
|
|
3453
|
+
}
|
|
3454
|
+
if (beforePattern) {
|
|
3455
|
+
const match = text3.match(beforePattern);
|
|
3456
|
+
if (match && typeof match.index === "number") {
|
|
3457
|
+
return `${text3.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
3458
|
+
|
|
3459
|
+
${text3.slice(match.index)}`;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
return `${text3.replace(/\s*$/, "")}
|
|
3463
|
+
|
|
3464
|
+
${block}
|
|
3465
|
+
`;
|
|
3466
|
+
}
|
|
3467
|
+
var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
|
|
3468
|
+
var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/hermes"];
|
|
3469
|
+
function requiredMisePathEntries(ctx) {
|
|
3470
|
+
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
3471
|
+
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
3472
|
+
if (existsSync10(join14(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
3473
|
+
}
|
|
3474
|
+
return required;
|
|
3475
|
+
}
|
|
3476
|
+
function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
|
|
3477
|
+
const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
3478
|
+
const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
|
|
3479
|
+
if (!envMatch || typeof envMatch.index !== "number") {
|
|
3480
|
+
return `[env]
|
|
3481
|
+
${render(required)}
|
|
3482
|
+
|
|
3483
|
+
${text3.replace(/^\s+/, "")}`;
|
|
3484
|
+
}
|
|
3485
|
+
const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
|
|
3486
|
+
const section = envMatch[2];
|
|
3487
|
+
const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
|
|
3488
|
+
const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
|
|
3489
|
+
if (!pathLine) {
|
|
3490
|
+
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
3491
|
+
}
|
|
3492
|
+
const current = [...pathLine[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
3493
|
+
const merged = [...current];
|
|
3494
|
+
for (const value of required) {
|
|
3495
|
+
if (!merged.includes(value)) merged.push(value);
|
|
3496
|
+
}
|
|
3497
|
+
const nextLine = render(merged);
|
|
3498
|
+
if (pathLine[0] === nextLine) return text3;
|
|
3499
|
+
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
3500
|
+
}
|
|
3501
|
+
function removeTomlSection(text3, headerPattern, marker, options) {
|
|
3502
|
+
const lines = text3.split("\n");
|
|
3503
|
+
let start = -1;
|
|
3504
|
+
let end = -1;
|
|
3505
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3506
|
+
if (!headerPattern.test(lines[i])) continue;
|
|
3507
|
+
if (marker) {
|
|
3508
|
+
let hasMarker = false;
|
|
3509
|
+
for (let j = i + 1; j < lines.length && !/^\[[^\]]+\]/.test(lines[j]); j++) {
|
|
3510
|
+
if (marker.test(lines[j])) {
|
|
3511
|
+
hasMarker = true;
|
|
3512
|
+
break;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
if (!hasMarker) continue;
|
|
3516
|
+
}
|
|
3517
|
+
start = i;
|
|
3518
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3519
|
+
if (/^\[[^\]]+\]/.test(lines[j])) {
|
|
3520
|
+
end = j;
|
|
3521
|
+
break;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
if (end === -1) end = lines.length;
|
|
3525
|
+
break;
|
|
3526
|
+
}
|
|
3527
|
+
if (start === -1) return text3;
|
|
3528
|
+
while (end > start + 1 && (lines[end - 1].trim() === "" || lines[end - 1].trim().startsWith("#"))) {
|
|
3529
|
+
end--;
|
|
3530
|
+
}
|
|
3531
|
+
if (options?.includePrecedingComments) {
|
|
3532
|
+
while (start > 0 && lines[start - 1].trim().startsWith("#")) {
|
|
3533
|
+
start--;
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
|
|
3537
|
+
return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
3538
|
+
}
|
|
3539
|
+
function insertTomlBlockBeforeVersioning(text3, block) {
|
|
3540
|
+
const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
|
|
3541
|
+
if (versioningIndex >= 0) {
|
|
3542
|
+
return `${text3.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
|
|
3543
|
+
|
|
3544
|
+
${text3.slice(versioningIndex)}`;
|
|
3545
|
+
}
|
|
3546
|
+
return `${text3.replace(/\s*$/, "")}
|
|
2724
3547
|
|
|
2725
3548
|
${block}
|
|
2726
3549
|
`;
|
|
@@ -2747,6 +3570,12 @@ function stripTomlStringsAndComments(line) {
|
|
|
2747
3570
|
function isManagedHookEntry(value) {
|
|
2748
3571
|
const trimmed = value.trim();
|
|
2749
3572
|
if (trimmed === OP_INJECT_SCRIPT) return true;
|
|
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;
|
|
3577
|
+
if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
3578
|
+
if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2750
3579
|
return /link-agentfiles\.sh'?\s*$/.test(trimmed);
|
|
2751
3580
|
}
|
|
2752
3581
|
function normalizeHookScript(script) {
|
|
@@ -2849,12 +3678,18 @@ function upsertLinkAgentfilesHooks(text3) {
|
|
|
2849
3678
|
function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
2850
3679
|
const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
|
|
2851
3680
|
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
3681
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
|
|
3682
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-provision-bmad\]$/, void 0, { includePrecedingComments: false });
|
|
3683
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3684
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3685
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
|
|
2852
3686
|
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
3687
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
|
|
2853
3688
|
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
2854
3689
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2855
3690
|
}
|
|
2856
3691
|
function readProjectJson(ctx) {
|
|
2857
|
-
return tryParseJson(safeReadText(
|
|
3692
|
+
return tryParseJson(safeReadText(join14(ctx.repoRoot, ".project.json")));
|
|
2858
3693
|
}
|
|
2859
3694
|
function boolSetting(value, fallback) {
|
|
2860
3695
|
if (typeof value === "boolean") return value;
|
|
@@ -2892,7 +3727,7 @@ function canonicalProjectJson(ctx) {
|
|
|
2892
3727
|
role.agentId || `${slug}-${role.role}`,
|
|
2893
3728
|
{
|
|
2894
3729
|
role: role.role,
|
|
2895
|
-
role_dir:
|
|
3730
|
+
role_dir: relative2(ctx.repoRoot, role.roleDir)
|
|
2896
3731
|
}
|
|
2897
3732
|
])
|
|
2898
3733
|
);
|
|
@@ -2929,12 +3764,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2929
3764
|
};
|
|
2930
3765
|
}
|
|
2931
3766
|
function projectJsonFinding(ctx) {
|
|
2932
|
-
const projectPath =
|
|
2933
|
-
const planeJsonPath =
|
|
3767
|
+
const projectPath = join14(ctx.repoRoot, ".project.json");
|
|
3768
|
+
const planeJsonPath = join14(ctx.repoRoot, ".plane.json");
|
|
2934
3769
|
const details = [];
|
|
2935
3770
|
const data = readProjectJson(ctx);
|
|
2936
3771
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2937
|
-
if (!
|
|
3772
|
+
if (!existsSync10(projectPath)) {
|
|
2938
3773
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2939
3774
|
}
|
|
2940
3775
|
if (!data) {
|
|
@@ -2952,8 +3787,8 @@ function projectJsonFinding(ctx) {
|
|
|
2952
3787
|
continue;
|
|
2953
3788
|
}
|
|
2954
3789
|
if (agent.role !== role.role) details.push(`agents.${role.agentId}.role should be ${role.role}`);
|
|
2955
|
-
if (agent.role_dir !==
|
|
2956
|
-
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)}`);
|
|
2957
3792
|
}
|
|
2958
3793
|
}
|
|
2959
3794
|
const ticketProvider = data.ticket_provider ?? {};
|
|
@@ -2969,7 +3804,7 @@ function projectJsonFinding(ctx) {
|
|
|
2969
3804
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2970
3805
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2971
3806
|
}
|
|
2972
|
-
if (
|
|
3807
|
+
if (existsSync10(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2973
3808
|
return {
|
|
2974
3809
|
id: "sot.project-json",
|
|
2975
3810
|
title: "Canonical .project.json",
|
|
@@ -2982,8 +3817,7 @@ function projectJsonFinding(ctx) {
|
|
|
2982
3817
|
function renderSoul(role) {
|
|
2983
3818
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
2984
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.";
|
|
2985
|
-
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
|
|
2986
|
-
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.`;
|
|
2987
3821
|
return `# ${role.displayName || role.agentId}
|
|
2988
3822
|
|
|
2989
3823
|
You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisioned to work inside the
|
|
@@ -3002,7 +3836,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
|
|
|
3002
3836
|
|
|
3003
3837
|
## Scope
|
|
3004
3838
|
|
|
3005
|
-
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.
|
|
3006
3840
|
|
|
3007
3841
|
## Tone
|
|
3008
3842
|
|
|
@@ -3014,12 +3848,12 @@ ${roleSpecific}
|
|
|
3014
3848
|
|
|
3015
3849
|
## Memory hygiene
|
|
3016
3850
|
|
|
3017
|
-
Your memory is
|
|
3851
|
+
Your memory is stored locally at \`./runtime/memories/\`. Use durable memory deliberately and keep \`memories/MEMORY.md\` current.
|
|
3018
3852
|
`;
|
|
3019
3853
|
}
|
|
3020
3854
|
function renderHermesWrapper(role) {
|
|
3021
3855
|
return `#!/usr/bin/env bash
|
|
3022
|
-
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime
|
|
3856
|
+
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the local runtime.
|
|
3023
3857
|
|
|
3024
3858
|
set -euo pipefail
|
|
3025
3859
|
|
|
@@ -3032,35 +3866,49 @@ if [[ -f "$FLEET_ENV" ]]; then
|
|
|
3032
3866
|
source "$FLEET_ENV"
|
|
3033
3867
|
fi
|
|
3034
3868
|
|
|
3035
|
-
HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN
|
|
3036
|
-
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}}"
|
|
3037
3870
|
CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
|
|
3038
3871
|
|
|
3039
3872
|
FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
3040
3873
|
PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
|
|
3041
|
-
|
|
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"
|
|
3042
3884
|
|
|
3043
3885
|
if [[ ! -d "$RUNTIME_HOME" ]]; then
|
|
3044
|
-
echo "hermes: runtime
|
|
3045
|
-
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
|
|
3046
3894
|
exit 1
|
|
3047
3895
|
fi
|
|
3048
3896
|
|
|
3049
|
-
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" "$@"
|
|
3050
3898
|
`.replace(/\u0010/g, "$");
|
|
3051
3899
|
}
|
|
3052
3900
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
3053
|
-
if (!
|
|
3901
|
+
if (!existsSync10(sourceDir)) return;
|
|
3054
3902
|
mkdirSync6(targetDir, { recursive: true });
|
|
3055
|
-
for (const entry of
|
|
3056
|
-
const sourcePath =
|
|
3903
|
+
for (const entry of readdirSync3(sourceDir, { withFileTypes: true })) {
|
|
3904
|
+
const sourcePath = join14(sourceDir, entry.name);
|
|
3057
3905
|
if (skip?.(sourcePath)) continue;
|
|
3058
|
-
const targetPath =
|
|
3906
|
+
const targetPath = join14(targetDir, entry.name);
|
|
3059
3907
|
if (entry.isDirectory()) {
|
|
3060
3908
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
3061
3909
|
continue;
|
|
3062
3910
|
}
|
|
3063
|
-
if (
|
|
3911
|
+
if (existsSync10(targetPath)) continue;
|
|
3064
3912
|
changedFiles.push(targetPath);
|
|
3065
3913
|
if (!dryRun) {
|
|
3066
3914
|
ensureParent(targetPath);
|
|
@@ -3068,21 +3916,78 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
3068
3916
|
}
|
|
3069
3917
|
}
|
|
3070
3918
|
}
|
|
3071
|
-
function
|
|
3072
|
-
const
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
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");
|
|
3929
|
+
const current = safeReadText(gitmodulesPath) ?? "";
|
|
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");
|
|
3079
3936
|
const current = safeReadText(gitmodulesPath) ?? "";
|
|
3080
|
-
|
|
3081
|
-
|
|
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();
|
|
3082
3941
|
changedFiles.push(gitmodulesPath);
|
|
3083
|
-
if (!dryRun) writeText(gitmodulesPath,
|
|
3942
|
+
if (!dryRun) writeText(gitmodulesPath, next ? `${next}
|
|
3943
|
+
` : "");
|
|
3084
3944
|
return [gitmodulesPath];
|
|
3085
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
|
+
}
|
|
3086
3991
|
function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
3087
3992
|
const path = registryPath(homeDir);
|
|
3088
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";
|
|
@@ -3101,9 +4006,11 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
3101
4006
|
project_id: ${ctxEscape(role.ticketProviderBoardId)}
|
|
3102
4007
|
identifier: ${ctxEscape(role.ticketProviderIdentifier)}
|
|
3103
4008
|
runtime_repo: ${ctxEscape(role.runtimeRepo)}
|
|
4009
|
+
bloodbank:
|
|
4010
|
+
gateway_scope: fleet
|
|
4011
|
+
target_agent_id: ${role.agentId}
|
|
3104
4012
|
systemd:
|
|
3105
4013
|
gateway_unit: hermes-${role.agentId}-gateway.service
|
|
3106
|
-
consumer_unit: hermes-${role.agentId}-consumer.service
|
|
3107
4014
|
heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
|
|
3108
4015
|
`;
|
|
3109
4016
|
const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
|
|
@@ -3167,6 +4074,7 @@ function checkUnit(unit) {
|
|
|
3167
4074
|
var BMAD_NPM_PACKAGE = "bmad-method";
|
|
3168
4075
|
var BMAD_TARGET_CHANNEL = "next";
|
|
3169
4076
|
var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
|
|
4077
|
+
var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
|
|
3170
4078
|
var BMAD_INSTALL_TOOLS = [
|
|
3171
4079
|
"claude-code",
|
|
3172
4080
|
"codex",
|
|
@@ -3214,7 +4122,53 @@ var BMAD_INSTALL_TOOLS = [
|
|
|
3214
4122
|
"windsurf",
|
|
3215
4123
|
"zencoder"
|
|
3216
4124
|
];
|
|
3217
|
-
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";
|
|
3218
4172
|
return [
|
|
3219
4173
|
"-y",
|
|
3220
4174
|
`${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
|
|
@@ -3223,20 +4177,20 @@ function bmadInstallArgs(repoRoot) {
|
|
|
3223
4177
|
"--directory",
|
|
3224
4178
|
repoRoot,
|
|
3225
4179
|
"--modules",
|
|
3226
|
-
|
|
4180
|
+
installerModules,
|
|
3227
4181
|
"--tools",
|
|
3228
4182
|
BMAD_INSTALL_TOOLS.join(",")
|
|
3229
4183
|
];
|
|
3230
4184
|
}
|
|
3231
|
-
function runBmadInstall(repoRoot) {
|
|
3232
|
-
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" });
|
|
3233
4187
|
if (result.status !== 0) {
|
|
3234
4188
|
return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
|
|
3235
4189
|
}
|
|
3236
4190
|
return { ok: true };
|
|
3237
4191
|
}
|
|
3238
4192
|
function readInstalledBmadVersion(repoRoot) {
|
|
3239
|
-
const raw = safeReadText(
|
|
4193
|
+
const raw = safeReadText(join14(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
3240
4194
|
if (!raw) return void 0;
|
|
3241
4195
|
try {
|
|
3242
4196
|
const parsed = YAML3.parse(raw);
|
|
@@ -3247,8 +4201,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
3247
4201
|
}
|
|
3248
4202
|
}
|
|
3249
4203
|
function bmadCachePath(homeDir) {
|
|
3250
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
3251
|
-
return
|
|
4204
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join14(homeDir, ".cache");
|
|
4205
|
+
return join14(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
3252
4206
|
}
|
|
3253
4207
|
function readBmadDistTagsCache(homeDir) {
|
|
3254
4208
|
const raw = safeReadText(bmadCachePath(homeDir));
|
|
@@ -3291,7 +4245,7 @@ function resolveBmadDistTags(homeDir) {
|
|
|
3291
4245
|
try {
|
|
3292
4246
|
const path = bmadCachePath(homeDir);
|
|
3293
4247
|
mkdirSync6(dirname6(path), { recursive: true });
|
|
3294
|
-
|
|
4248
|
+
writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
|
|
3295
4249
|
} catch {
|
|
3296
4250
|
}
|
|
3297
4251
|
return { distTags: fetched, stale: false };
|
|
@@ -3331,19 +4285,233 @@ function compareBmadVersions(a, b) {
|
|
|
3331
4285
|
}
|
|
3332
4286
|
return 0;
|
|
3333
4287
|
}
|
|
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
|
+
}
|
|
3334
4502
|
var RULES = [
|
|
3335
4503
|
{
|
|
3336
4504
|
id: "mise.config-root",
|
|
3337
4505
|
title: "mise config_root + AGENTS link hooks",
|
|
3338
4506
|
audit: (ctx) => {
|
|
3339
|
-
const misePath =
|
|
3340
|
-
if (!
|
|
4507
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
4508
|
+
if (!existsSync10(misePath)) {
|
|
3341
4509
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
3342
4510
|
}
|
|
3343
4511
|
const text3 = readText(misePath);
|
|
3344
4512
|
const details = [];
|
|
3345
|
-
const linkAgentfilesPath =
|
|
3346
|
-
if (!
|
|
4513
|
+
const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
4514
|
+
if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
3347
4515
|
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
3348
4516
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
3349
4517
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -3361,10 +4529,10 @@ var RULES = [
|
|
|
3361
4529
|
};
|
|
3362
4530
|
},
|
|
3363
4531
|
migrate: (ctx, finding) => {
|
|
3364
|
-
const path =
|
|
4532
|
+
const path = join14(ctx.repoRoot, "mise.toml");
|
|
3365
4533
|
const changedFiles = [];
|
|
3366
4534
|
const details = [];
|
|
3367
|
-
if (!
|
|
4535
|
+
if (!existsSync10(path)) {
|
|
3368
4536
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3369
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: [] };
|
|
3370
4538
|
}
|
|
@@ -3380,7 +4548,7 @@ var RULES = [
|
|
|
3380
4548
|
if (!ctx.dryRun) writeText(path, next);
|
|
3381
4549
|
text3 = next;
|
|
3382
4550
|
}
|
|
3383
|
-
const linkAgentfilesPath =
|
|
4551
|
+
const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3384
4552
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
3385
4553
|
if (expectedScript === void 0) {
|
|
3386
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: [] };
|
|
@@ -3389,7 +4557,7 @@ var RULES = [
|
|
|
3389
4557
|
changedFiles.push(linkAgentfilesPath);
|
|
3390
4558
|
if (!ctx.dryRun) {
|
|
3391
4559
|
writeText(linkAgentfilesPath, expectedScript);
|
|
3392
|
-
|
|
4560
|
+
chmodSync4(linkAgentfilesPath, 493);
|
|
3393
4561
|
}
|
|
3394
4562
|
}
|
|
3395
4563
|
return {
|
|
@@ -3407,13 +4575,13 @@ var RULES = [
|
|
|
3407
4575
|
title: "managed mise versioning block",
|
|
3408
4576
|
audit: (ctx) => {
|
|
3409
4577
|
const details = [];
|
|
3410
|
-
const misePath =
|
|
3411
|
-
const versioningPath =
|
|
3412
|
-
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");
|
|
3413
4581
|
const text3 = safeReadText(misePath);
|
|
3414
4582
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
3415
|
-
if (!
|
|
3416
|
-
if (!
|
|
4583
|
+
if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
4584
|
+
if (!existsSync10(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
3417
4585
|
return {
|
|
3418
4586
|
id: "mise.versioning",
|
|
3419
4587
|
title: "managed mise versioning block",
|
|
@@ -3426,8 +4594,8 @@ var RULES = [
|
|
|
3426
4594
|
migrate: (ctx, finding) => {
|
|
3427
4595
|
const changedFiles = [];
|
|
3428
4596
|
const details = [];
|
|
3429
|
-
const misePath =
|
|
3430
|
-
if (!
|
|
4597
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
4598
|
+
if (!existsSync10(misePath)) {
|
|
3431
4599
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3432
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: [] };
|
|
3433
4601
|
}
|
|
@@ -3436,46 +4604,266 @@ var RULES = [
|
|
|
3436
4604
|
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
3437
4605
|
}
|
|
3438
4606
|
}
|
|
3439
|
-
const currentMise = readText(misePath);
|
|
3440
|
-
let cleanedMise = currentMise;
|
|
3441
|
-
if (!currentMise.includes("# >>> mise-versioning >>>")) {
|
|
3442
|
-
const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
|
|
3443
|
-
for (const taskName of taskNames) {
|
|
3444
|
-
const escaped = taskName.replace(/:/g, "\\:");
|
|
3445
|
-
const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
|
|
3446
|
-
cleanedMise = removeTomlSection(cleanedMise, headerPattern);
|
|
4607
|
+
const currentMise = readText(misePath);
|
|
4608
|
+
let cleanedMise = currentMise;
|
|
4609
|
+
if (!currentMise.includes("# >>> mise-versioning >>>")) {
|
|
4610
|
+
const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
|
|
4611
|
+
for (const taskName of taskNames) {
|
|
4612
|
+
const escaped = taskName.replace(/:/g, "\\:");
|
|
4613
|
+
const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
|
|
4614
|
+
cleanedMise = removeTomlSection(cleanedMise, headerPattern);
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
const nextMise = replaceOrAppendManagedBlock(cleanedMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
4618
|
+
if (nextMise !== currentMise) {
|
|
4619
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
4620
|
+
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
4621
|
+
}
|
|
4622
|
+
const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
4623
|
+
const expectedScript = templateVersioningScript(ctx);
|
|
4624
|
+
if (expectedScript === void 0) {
|
|
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: [] };
|
|
4626
|
+
}
|
|
4627
|
+
if (safeReadText(versioningPath) !== expectedScript) {
|
|
4628
|
+
changedFiles.push(versioningPath);
|
|
4629
|
+
if (!ctx.dryRun) {
|
|
4630
|
+
writeText(versioningPath, expectedScript);
|
|
4631
|
+
chmodSync4(versioningPath, 493);
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
|
|
4635
|
+
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
4636
|
+
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
4637
|
+
changedFiles.push(manifestPath);
|
|
4638
|
+
if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
|
|
4639
|
+
}
|
|
4640
|
+
return {
|
|
4641
|
+
id: finding.id,
|
|
4642
|
+
title: finding.title,
|
|
4643
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
4644
|
+
summary: changedFiles.length ? "Versioning block/script/manifest normalized" : "No changes required",
|
|
4645
|
+
changedFiles,
|
|
4646
|
+
details: []
|
|
4647
|
+
};
|
|
4648
|
+
}
|
|
4649
|
+
},
|
|
4650
|
+
{
|
|
4651
|
+
id: "skills.project-manifest",
|
|
4652
|
+
title: "Skillex project skills manifest",
|
|
4653
|
+
audit: (ctx) => {
|
|
4654
|
+
const details = [];
|
|
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");
|
|
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);
|
|
4672
|
+
const manifest = tryParseJson(safeReadText(manifestPath));
|
|
4673
|
+
if (!manifest) {
|
|
4674
|
+
details.push(".agents/skills.json missing or invalid JSON");
|
|
4675
|
+
} else {
|
|
4676
|
+
if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
|
|
4677
|
+
if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
|
|
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
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
const invalidBmadLinkNames = /* @__PURE__ */ new Set();
|
|
4691
|
+
if (existsSync10(legacyDir)) {
|
|
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
|
+
}
|
|
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`);
|
|
4721
|
+
}
|
|
4722
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
4723
|
+
if (existsSync10(join14(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
|
|
4724
|
+
}
|
|
4725
|
+
const localExample = tryParseJson(safeReadText(localExamplePath));
|
|
4726
|
+
if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
|
|
4727
|
+
details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
|
|
4728
|
+
}
|
|
4729
|
+
const mise = safeReadText(misePath);
|
|
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
|
+
}
|
|
4738
|
+
if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
|
|
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
|
+
}
|
|
4761
|
+
if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
|
|
4762
|
+
details.push("mise.toml still contains legacy skill-link wiring");
|
|
4763
|
+
}
|
|
4764
|
+
return {
|
|
4765
|
+
id: "skills.project-manifest",
|
|
4766
|
+
title: "Skillex project skills manifest",
|
|
4767
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
4768
|
+
summary: details.length === 0 ? "Skillex skills manifest parity verified" : `${details.length} Skillex migration issue(s) detected`,
|
|
4769
|
+
details,
|
|
4770
|
+
fixable
|
|
4771
|
+
};
|
|
4772
|
+
},
|
|
4773
|
+
migrate: (ctx, finding) => {
|
|
4774
|
+
const changedFiles = [];
|
|
4775
|
+
const details = [];
|
|
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
|
+
};
|
|
4793
|
+
}
|
|
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
|
+
};
|
|
4831
|
+
}
|
|
4832
|
+
changedFiles.push(...provisioned.changedFiles);
|
|
4833
|
+
if (provisioned.changedFiles.includes(manifestPath)) details.push(`Recorded BMAD pack ${BMAD_PACK_VERSION} in .agents/skills.json`);
|
|
4834
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
4835
|
+
const path = join14(ctx.repoRoot, rel);
|
|
4836
|
+
if (existsSync10(path)) {
|
|
4837
|
+
changedFiles.push(path);
|
|
4838
|
+
if (!ctx.dryRun) unlinkSync3(path);
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
const templateLocalExample = templateCommonProjectText(ctx, ".agents/local.example.json");
|
|
4842
|
+
const currentLocalExample = safeReadText(localExamplePath);
|
|
4843
|
+
if (templateLocalExample && currentLocalExample && currentLocalExample !== templateLocalExample) {
|
|
4844
|
+
changedFiles.push(localExamplePath);
|
|
4845
|
+
if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
|
|
4846
|
+
}
|
|
4847
|
+
normalizeExecutableTemplate(ctx, provisionScriptPath, expectedProvisionScript, changedFiles);
|
|
4848
|
+
normalizeExecutableTemplate(ctx, syncScriptPath, expectedSyncScript, changedFiles);
|
|
4849
|
+
if (!existsSync10(misePath)) {
|
|
4850
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
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 };
|
|
3447
4852
|
}
|
|
3448
4853
|
}
|
|
3449
|
-
const
|
|
4854
|
+
const currentMise = readText(misePath);
|
|
4855
|
+
const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
|
|
3450
4856
|
if (nextMise !== currentMise) {
|
|
3451
4857
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3452
4858
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3453
4859
|
}
|
|
3454
|
-
const versioningPath = join11(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3455
|
-
const expectedScript = templateVersioningScript(ctx);
|
|
3456
|
-
if (expectedScript === void 0) {
|
|
3457
|
-
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: [] };
|
|
3458
|
-
}
|
|
3459
|
-
if (safeReadText(versioningPath) !== expectedScript) {
|
|
3460
|
-
changedFiles.push(versioningPath);
|
|
3461
|
-
if (!ctx.dryRun) {
|
|
3462
|
-
writeText(versioningPath, expectedScript);
|
|
3463
|
-
chmodSync2(versioningPath, 493);
|
|
3464
|
-
}
|
|
3465
|
-
}
|
|
3466
|
-
const manifestPath = join11(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3467
|
-
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
3468
|
-
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3469
|
-
changedFiles.push(manifestPath);
|
|
3470
|
-
if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
|
|
3471
|
-
}
|
|
3472
4860
|
return {
|
|
3473
4861
|
id: finding.id,
|
|
3474
4862
|
title: finding.title,
|
|
3475
4863
|
status: changedFiles.length ? "applied" : "noop",
|
|
3476
|
-
summary: changedFiles.length ? "
|
|
4864
|
+
summary: changedFiles.length ? "Skillex skills manifest contract normalized" : "No changes required",
|
|
3477
4865
|
changedFiles,
|
|
3478
|
-
details
|
|
4866
|
+
details
|
|
3479
4867
|
};
|
|
3480
4868
|
}
|
|
3481
4869
|
},
|
|
@@ -3483,9 +4871,9 @@ var RULES = [
|
|
|
3483
4871
|
id: "sot.agent-symlinks",
|
|
3484
4872
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
3485
4873
|
audit: (ctx) => {
|
|
3486
|
-
const agentsPath =
|
|
3487
|
-
if (!
|
|
3488
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
4874
|
+
const agentsPath = join14(ctx.repoRoot, "AGENTS.md");
|
|
4875
|
+
if (!existsSync10(agentsPath)) {
|
|
4876
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join14(ctx.repoRoot, file)));
|
|
3489
4877
|
if (fallbackSources.length === 0) {
|
|
3490
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 };
|
|
3491
4879
|
}
|
|
@@ -3500,7 +4888,7 @@ var RULES = [
|
|
|
3500
4888
|
}
|
|
3501
4889
|
const details = [];
|
|
3502
4890
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3503
|
-
const full =
|
|
4891
|
+
const full = join14(ctx.repoRoot, file);
|
|
3504
4892
|
const target = readSymlinkTarget(full);
|
|
3505
4893
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
3506
4894
|
}
|
|
@@ -3524,7 +4912,7 @@ var RULES = [
|
|
|
3524
4912
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
3525
4913
|
}
|
|
3526
4914
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3527
|
-
const full =
|
|
4915
|
+
const full = join14(ctx.repoRoot, file);
|
|
3528
4916
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
3529
4917
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
3530
4918
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3546,7 +4934,7 @@ var RULES = [
|
|
|
3546
4934
|
migrate: (ctx, finding) => {
|
|
3547
4935
|
const changedFiles = [];
|
|
3548
4936
|
const details = [];
|
|
3549
|
-
const path =
|
|
4937
|
+
const path = join14(ctx.repoRoot, ".project.json");
|
|
3550
4938
|
const existing = readProjectJson(ctx) ?? {};
|
|
3551
4939
|
const canonical = canonicalProjectJson(ctx);
|
|
3552
4940
|
const merged = { ...existing, ...canonical };
|
|
@@ -3556,11 +4944,11 @@ var RULES = [
|
|
|
3556
4944
|
changedFiles.push(path);
|
|
3557
4945
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3558
4946
|
}
|
|
3559
|
-
const planeJson =
|
|
3560
|
-
if (
|
|
4947
|
+
const planeJson = join14(ctx.repoRoot, ".plane.json");
|
|
4948
|
+
if (existsSync10(planeJson)) {
|
|
3561
4949
|
const backup = `${planeJson}.migrated-backup`;
|
|
3562
|
-
if (
|
|
3563
|
-
details.push(`cannot back up .plane.json because ${
|
|
4950
|
+
if (existsSync10(backup)) {
|
|
4951
|
+
details.push(`cannot back up .plane.json because ${relative2(ctx.repoRoot, backup)} already exists`);
|
|
3564
4952
|
} else {
|
|
3565
4953
|
changedFiles.push(backup);
|
|
3566
4954
|
if (!ctx.dryRun) renameSync2(planeJson, backup);
|
|
@@ -3581,17 +4969,21 @@ var RULES = [
|
|
|
3581
4969
|
title: ".env.op + gitignore secrets contract",
|
|
3582
4970
|
audit: (ctx) => {
|
|
3583
4971
|
const details = [];
|
|
3584
|
-
const envOp = safeReadText(
|
|
3585
|
-
const gitignore = safeReadText(
|
|
4972
|
+
const envOp = safeReadText(join14(ctx.repoRoot, ".env.op"));
|
|
4973
|
+
const gitignore = safeReadText(join14(ctx.repoRoot, ".gitignore"));
|
|
3586
4974
|
if (!envOp) {
|
|
3587
4975
|
details.push(".env.op missing");
|
|
3588
4976
|
} else {
|
|
3589
|
-
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 }) => {
|
|
3590
4982
|
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
3591
4983
|
const quotedLiteral = /^"[^"\r\n]*"$/.test(value) || /^'[^'\r\n]*'$/.test(value);
|
|
3592
4984
|
return !value.startsWith("op://") && !/^https?:\/\//.test(value) && !/^[A-Za-z0-9_.:-]+$/.test(value) && !quotedLiteral;
|
|
3593
4985
|
});
|
|
3594
|
-
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(", ")}`);
|
|
3595
4987
|
}
|
|
3596
4988
|
if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
|
|
3597
4989
|
if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
|
|
@@ -3608,12 +5000,23 @@ var RULES = [
|
|
|
3608
5000
|
migrate: (ctx, finding) => {
|
|
3609
5001
|
const changedFiles = [];
|
|
3610
5002
|
const details = [];
|
|
3611
|
-
const envOpPath =
|
|
3612
|
-
if (!
|
|
5003
|
+
const envOpPath = join14(ctx.repoRoot, ".env.op");
|
|
5004
|
+
if (!existsSync10(envOpPath)) {
|
|
3613
5005
|
changedFiles.push(envOpPath);
|
|
3614
|
-
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
|
+
}
|
|
3615
5018
|
}
|
|
3616
|
-
const gitignorePath =
|
|
5019
|
+
const gitignorePath = join14(ctx.repoRoot, ".gitignore");
|
|
3617
5020
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3618
5021
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3619
5022
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3640,7 +5043,7 @@ var RULES = [
|
|
|
3640
5043
|
title: ".copier-answers.yml provenance + drift report",
|
|
3641
5044
|
audit: (ctx) => {
|
|
3642
5045
|
const details = [];
|
|
3643
|
-
const path =
|
|
5046
|
+
const path = join14(ctx.repoRoot, ".copier-answers.yml");
|
|
3644
5047
|
const text3 = safeReadText(path);
|
|
3645
5048
|
const project = readProjectJson(ctx);
|
|
3646
5049
|
if (!text3) {
|
|
@@ -3671,12 +5074,12 @@ var RULES = [
|
|
|
3671
5074
|
const changedFiles = [];
|
|
3672
5075
|
const project = canonicalProjectJson(ctx);
|
|
3673
5076
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3674
|
-
_src_path: ${
|
|
5077
|
+
_src_path: ${join14(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3675
5078
|
project_description: ${String(project.project_description)}
|
|
3676
5079
|
project_name: ${String(project.project_name)}
|
|
3677
5080
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3678
5081
|
`;
|
|
3679
|
-
const path =
|
|
5082
|
+
const path = join14(ctx.repoRoot, ".copier-answers.yml");
|
|
3680
5083
|
if (safeReadText(path) !== text3) {
|
|
3681
5084
|
changedFiles.push(path);
|
|
3682
5085
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -3695,14 +5098,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3695
5098
|
id: "bmad.scaffold",
|
|
3696
5099
|
title: "BMAD modules/docs scaffold",
|
|
3697
5100
|
audit: (ctx) => {
|
|
3698
|
-
const
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
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)));
|
|
3706
5116
|
return {
|
|
3707
5117
|
id: "bmad.scaffold",
|
|
3708
5118
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3714,9 +5124,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3714
5124
|
},
|
|
3715
5125
|
migrate: (ctx, finding) => {
|
|
3716
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];
|
|
3717
5139
|
if (ctx.dryRun) {
|
|
3718
5140
|
for (const detail of finding.details) {
|
|
3719
|
-
changedFiles.push(
|
|
5141
|
+
changedFiles.push(join14(ctx.repoRoot, detail));
|
|
3720
5142
|
}
|
|
3721
5143
|
return {
|
|
3722
5144
|
id: finding.id,
|
|
@@ -3725,11 +5147,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3725
5147
|
summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
|
|
3726
5148
|
changedFiles,
|
|
3727
5149
|
details: [
|
|
3728
|
-
`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(","), "...")}`
|
|
3729
5151
|
]
|
|
3730
5152
|
};
|
|
3731
5153
|
}
|
|
3732
|
-
const
|
|
5154
|
+
const preservedSkillsManifest = tryParseJson(
|
|
5155
|
+
safeReadText(join14(ctx.repoRoot, ".agents", "skills.json"))
|
|
5156
|
+
);
|
|
5157
|
+
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
3733
5158
|
if (!install.ok) {
|
|
3734
5159
|
return {
|
|
3735
5160
|
id: finding.id,
|
|
@@ -3740,16 +5165,28 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3740
5165
|
details: [install.error ?? "Unknown error"]
|
|
3741
5166
|
};
|
|
3742
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
|
+
}
|
|
3743
5179
|
for (const detail of finding.details) {
|
|
3744
|
-
if (
|
|
3745
|
-
changedFiles.push(
|
|
5180
|
+
if (existsSync10(join14(ctx.repoRoot, detail))) {
|
|
5181
|
+
changedFiles.push(join14(ctx.repoRoot, detail));
|
|
3746
5182
|
}
|
|
3747
5183
|
}
|
|
5184
|
+
changedFiles.push(...provisioned.changedFiles);
|
|
3748
5185
|
return {
|
|
3749
5186
|
id: finding.id,
|
|
3750
5187
|
title: finding.title,
|
|
3751
5188
|
status: changedFiles.length ? "applied" : "noop",
|
|
3752
|
-
summary: changedFiles.length ?
|
|
5189
|
+
summary: changedFiles.length ? `Installed BMAD scaffold with Skillex pack ${BMAD_PACK_VERSION} skills` : "No changes required",
|
|
3753
5190
|
changedFiles,
|
|
3754
5191
|
details: []
|
|
3755
5192
|
};
|
|
@@ -3765,7 +5202,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3765
5202
|
id: "bmad.version",
|
|
3766
5203
|
title: "BMAD version currency",
|
|
3767
5204
|
status: "skip",
|
|
3768
|
-
summary:
|
|
5205
|
+
summary: existsSync10(join14(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
3769
5206
|
details: [],
|
|
3770
5207
|
fixable: false
|
|
3771
5208
|
};
|
|
@@ -3820,7 +5257,19 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3820
5257
|
}
|
|
3821
5258
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3822
5259
|
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3823
|
-
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];
|
|
3824
5273
|
if (ctx.dryRun) {
|
|
3825
5274
|
return {
|
|
3826
5275
|
id: finding.id,
|
|
@@ -3829,11 +5278,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3829
5278
|
summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
|
|
3830
5279
|
changedFiles: [manifestPath],
|
|
3831
5280
|
details: [
|
|
3832
|
-
`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(","), "...")}`
|
|
3833
5282
|
]
|
|
3834
5283
|
};
|
|
3835
5284
|
}
|
|
3836
|
-
const
|
|
5285
|
+
const preservedSkillsManifest = tryParseJson(
|
|
5286
|
+
safeReadText(join14(ctx.repoRoot, ".agents", "skills.json"))
|
|
5287
|
+
);
|
|
5288
|
+
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
3837
5289
|
if (!install.ok) {
|
|
3838
5290
|
return {
|
|
3839
5291
|
id: finding.id,
|
|
@@ -3844,6 +5296,17 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3844
5296
|
details: [install.error ?? "Unknown error"]
|
|
3845
5297
|
};
|
|
3846
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
|
+
}
|
|
3847
5310
|
const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
|
|
3848
5311
|
const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
|
|
3849
5312
|
return {
|
|
@@ -3851,7 +5314,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3851
5314
|
title: finding.title,
|
|
3852
5315
|
status: upgraded ? "applied" : "noop",
|
|
3853
5316
|
summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
|
|
3854
|
-
changedFiles:
|
|
5317
|
+
changedFiles: Array.from(/* @__PURE__ */ new Set([
|
|
5318
|
+
...upgraded ? [manifestPath] : [],
|
|
5319
|
+
...provisioned.changedFiles
|
|
5320
|
+
])),
|
|
3855
5321
|
details: []
|
|
3856
5322
|
};
|
|
3857
5323
|
}
|
|
@@ -3866,12 +5332,13 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3866
5332
|
return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
|
|
3867
5333
|
}
|
|
3868
5334
|
const details = [];
|
|
3869
|
-
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"
|
|
3870
|
-
if (!
|
|
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))}`);
|
|
3871
5337
|
}
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
5338
|
+
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
|
|
5339
|
+
details.push(".gitmodules contains retired pm runtime submodule mapping");
|
|
5340
|
+
}
|
|
5341
|
+
if (!profileMetaInheritsDefault(join14(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3875
5342
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3876
5343
|
}
|
|
3877
5344
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3892,21 +5359,32 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3892
5359
|
if (!role) {
|
|
3893
5360
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3894
5361
|
}
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
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");
|
|
5383
|
+
if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
|
|
3905
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);
|
|
3906
5385
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3907
5386
|
}
|
|
3908
|
-
|
|
3909
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(join11(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
5387
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join14(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3910
5388
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3911
5389
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3912
5390
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3937,8 +5415,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3937
5415
|
}
|
|
3938
5416
|
const details = [];
|
|
3939
5417
|
for (const role of roles) {
|
|
3940
|
-
const roleRelDir =
|
|
3941
|
-
const runtimeRelPath =
|
|
5418
|
+
const roleRelDir = relative2(ctx.repoRoot, role.roleDir);
|
|
5419
|
+
const runtimeRelPath = join14(roleRelDir, "runtime");
|
|
3942
5420
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3943
5421
|
cwd: ctx.repoRoot,
|
|
3944
5422
|
encoding: "utf8"
|
|
@@ -3946,15 +5424,18 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3946
5424
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3947
5425
|
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3948
5426
|
}
|
|
3949
|
-
|
|
3950
|
-
|
|
5427
|
+
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
|
|
5428
|
+
details.push(`stale .gitmodules mapping exists for ${runtimeRelPath}`);
|
|
5429
|
+
}
|
|
5430
|
+
const gitignorePath = join14(role.roleDir, ".gitignore");
|
|
5431
|
+
if (existsSync10(gitignorePath)) {
|
|
3951
5432
|
const content = safeReadText(gitignorePath) ?? "";
|
|
3952
5433
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3953
5434
|
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
3954
|
-
details.push(`.gitignore missing runtime/ ignore entry in ${
|
|
5435
|
+
details.push(`.gitignore missing runtime/ ignore entry in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
3955
5436
|
}
|
|
3956
5437
|
} else {
|
|
3957
|
-
details.push(`.gitignore is missing in ${
|
|
5438
|
+
details.push(`.gitignore is missing in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
3958
5439
|
}
|
|
3959
5440
|
}
|
|
3960
5441
|
return {
|
|
@@ -3971,32 +5452,28 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3971
5452
|
const changedFiles = [];
|
|
3972
5453
|
const details = [];
|
|
3973
5454
|
for (const role of roles) {
|
|
3974
|
-
const
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
cwd: ctx.repoRoot,
|
|
3986
|
-
encoding: "utf8"
|
|
3987
|
-
});
|
|
3988
|
-
}
|
|
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
|
+
};
|
|
3989
5466
|
}
|
|
3990
|
-
const gitignorePath =
|
|
5467
|
+
const gitignorePath = join14(role.roleDir, ".gitignore");
|
|
3991
5468
|
let content = "";
|
|
3992
5469
|
let isIgnored = false;
|
|
3993
|
-
if (
|
|
5470
|
+
if (existsSync10(gitignorePath)) {
|
|
3994
5471
|
content = safeReadText(gitignorePath) ?? "";
|
|
3995
5472
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3996
5473
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
3997
5474
|
}
|
|
3998
5475
|
if (!isIgnored) {
|
|
3999
|
-
details.push(`ignore runtime/ in ${
|
|
5476
|
+
details.push(`ignore runtime/ in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
4000
5477
|
changedFiles.push(gitignorePath);
|
|
4001
5478
|
if (!ctx.dryRun) {
|
|
4002
5479
|
if (content && !content.endsWith("\n")) {
|
|
@@ -4031,7 +5508,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4031
5508
|
}
|
|
4032
5509
|
const details = [];
|
|
4033
5510
|
for (const role of roles) {
|
|
4034
|
-
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`]) {
|
|
4035
5512
|
const state = checkUnit(unit);
|
|
4036
5513
|
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
4037
5514
|
}
|
|
@@ -4057,9 +5534,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4057
5534
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
4058
5535
|
}
|
|
4059
5536
|
for (const role of roles) {
|
|
4060
|
-
const sysDir =
|
|
4061
|
-
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-
|
|
4062
|
-
const allUnitsPresent = units.every((unit) =>
|
|
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)));
|
|
4063
5540
|
if (allUnitsPresent) {
|
|
4064
5541
|
if (ctx.dryRun) {
|
|
4065
5542
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -4071,8 +5548,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4071
5548
|
}
|
|
4072
5549
|
continue;
|
|
4073
5550
|
}
|
|
4074
|
-
for (const script of [
|
|
4075
|
-
if (!script || !
|
|
5551
|
+
for (const script of [join14(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
5552
|
+
if (!script || !existsSync10(script)) continue;
|
|
4076
5553
|
if (ctx.dryRun) {
|
|
4077
5554
|
details.push(`would run: bash ${script}`);
|
|
4078
5555
|
} else {
|
|
@@ -4090,6 +5567,349 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4090
5567
|
details
|
|
4091
5568
|
};
|
|
4092
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
|
+
}
|
|
4093
5913
|
}
|
|
4094
5914
|
];
|
|
4095
5915
|
function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
@@ -4099,7 +5919,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
4099
5919
|
changedFiles.push(path);
|
|
4100
5920
|
if (!dryRun) {
|
|
4101
5921
|
writeText(path, normalized);
|
|
4102
|
-
if (mode)
|
|
5922
|
+
if (mode) chmodSync4(path, mode);
|
|
4103
5923
|
}
|
|
4104
5924
|
}
|
|
4105
5925
|
function getParityRuleIds() {
|
|
@@ -4108,7 +5928,7 @@ function getParityRuleIds() {
|
|
|
4108
5928
|
function runAudit(repoArg) {
|
|
4109
5929
|
const pjanglerRoot = resolvePjanglerRoot2();
|
|
4110
5930
|
const ctx = {
|
|
4111
|
-
repoRoot:
|
|
5931
|
+
repoRoot: resolve3(repoArg ?? process.cwd()),
|
|
4112
5932
|
dryRun: true,
|
|
4113
5933
|
pjanglerRoot,
|
|
4114
5934
|
homeDir: homedir5()
|
|
@@ -4124,7 +5944,7 @@ function runAudit(repoArg) {
|
|
|
4124
5944
|
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
4125
5945
|
const pjanglerRoot = resolvePjanglerRoot2();
|
|
4126
5946
|
const ctx = {
|
|
4127
|
-
repoRoot:
|
|
5947
|
+
repoRoot: resolve3(repoArg ?? process.cwd()),
|
|
4128
5948
|
dryRun,
|
|
4129
5949
|
pjanglerRoot,
|
|
4130
5950
|
homeDir: homedir5()
|
|
@@ -4158,7 +5978,22 @@ function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
|
4158
5978
|
};
|
|
4159
5979
|
}
|
|
4160
5980
|
function runMigration(selector, repoArg, dryRun, all) {
|
|
4161
|
-
|
|
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] : [];
|
|
4162
5997
|
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
4163
5998
|
}
|
|
4164
5999
|
function prettyTimestamp(iso) {
|
|
@@ -4211,15 +6046,15 @@ function formatMigrationReport(report) {
|
|
|
4211
6046
|
}
|
|
4212
6047
|
|
|
4213
6048
|
// src/utils/version.ts
|
|
4214
|
-
import { readFileSync as
|
|
4215
|
-
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";
|
|
4216
6051
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4217
6052
|
var PJANGLER_VERSION = (() => {
|
|
4218
6053
|
try {
|
|
4219
6054
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
4220
6055
|
for (let i = 0; i < 4; i++) {
|
|
4221
6056
|
try {
|
|
4222
|
-
const raw =
|
|
6057
|
+
const raw = readFileSync8(join15(dir, "package.json"), "utf8");
|
|
4223
6058
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
4224
6059
|
} catch {
|
|
4225
6060
|
const parent = dirname7(dir);
|
|
@@ -4262,9 +6097,9 @@ async function promptForRuleIds(rules) {
|
|
|
4262
6097
|
return selected;
|
|
4263
6098
|
}
|
|
4264
6099
|
function readJson(path) {
|
|
4265
|
-
if (!
|
|
6100
|
+
if (!existsSync11(path)) return void 0;
|
|
4266
6101
|
try {
|
|
4267
|
-
const parsed = JSON.parse(
|
|
6102
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
4268
6103
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
4269
6104
|
} catch {
|
|
4270
6105
|
return void 0;
|
|
@@ -4273,7 +6108,7 @@ function readJson(path) {
|
|
|
4273
6108
|
function findGitRoot(cwd) {
|
|
4274
6109
|
const result = spawnSync7("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
4275
6110
|
if (result.status !== 0) return void 0;
|
|
4276
|
-
return
|
|
6111
|
+
return resolve4(result.stdout.trim());
|
|
4277
6112
|
}
|
|
4278
6113
|
function packageNameToProjectName(value) {
|
|
4279
6114
|
if (!value) return void 0;
|
|
@@ -4281,9 +6116,9 @@ function packageNameToProjectName(value) {
|
|
|
4281
6116
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
4282
6117
|
}
|
|
4283
6118
|
function deriveProjectDefaults(targetDir) {
|
|
4284
|
-
const manifest = readJson(
|
|
4285
|
-
const pkg = readJson(
|
|
4286
|
-
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";
|
|
4287
6122
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
4288
6123
|
return {
|
|
4289
6124
|
name,
|
|
@@ -4338,7 +6173,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
4338
6173
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
4339
6174
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
4340
6175
|
`;
|
|
4341
|
-
return !
|
|
6176
|
+
return !existsSync11(action.path) || readFileSync9(action.path, "utf8") !== next;
|
|
4342
6177
|
}
|
|
4343
6178
|
if (kind === "copier.copy.commonproject") return true;
|
|
4344
6179
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -4386,26 +6221,26 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
4386
6221
|
const interactive = isInteractiveProjectInit(options);
|
|
4387
6222
|
const cwd = process.cwd();
|
|
4388
6223
|
const cwdGitRoot = findGitRoot(cwd);
|
|
4389
|
-
let targetDir = options.targetDir ?
|
|
6224
|
+
let targetDir = options.targetDir ? resolve4(options.targetDir) : void 0;
|
|
4390
6225
|
if (!targetDir && cwdGitRoot) {
|
|
4391
6226
|
targetDir = cwdGitRoot;
|
|
4392
6227
|
}
|
|
4393
6228
|
if (!targetDir && interactive) {
|
|
4394
|
-
const defaultName = name ??
|
|
6229
|
+
const defaultName = name ?? basename5(cwd);
|
|
4395
6230
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
4396
|
-
const defaultDir =
|
|
6231
|
+
const defaultDir = join16(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
4397
6232
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
4398
6233
|
name = promptedName;
|
|
4399
6234
|
}
|
|
4400
6235
|
if (!targetDir) {
|
|
4401
6236
|
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
4402
|
-
targetDir =
|
|
6237
|
+
targetDir = resolve4(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
4403
6238
|
}
|
|
4404
|
-
const targetExists =
|
|
6239
|
+
const targetExists = existsSync11(targetDir);
|
|
4405
6240
|
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
4406
6241
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
4407
|
-
const syncMode = Boolean(targetGitRoot &&
|
|
4408
|
-
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: "" };
|
|
4409
6244
|
if (!name && interactive && !syncMode) {
|
|
4410
6245
|
name = await promptTextValue("Project name", defaults.name);
|
|
4411
6246
|
}
|
|
@@ -4789,7 +6624,7 @@ program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to op
|
|
|
4789
6624
|
process.exit(1);
|
|
4790
6625
|
}
|
|
4791
6626
|
});
|
|
4792
|
-
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) => {
|
|
4793
6628
|
const isDarwin = process.platform === "darwin";
|
|
4794
6629
|
const local = options.local ?? false;
|
|
4795
6630
|
const context = {
|