@delorenj/pjangler 1.2.1 → 1.2.3
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/mcp-server.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/mcp-server.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { basename as basename2, dirname as dirname7, join as join10, resolve as resolve2 } from "node:path";
|
|
4
|
+
import { existsSync as existsSync9, statSync as statSync2 } from "node:fs";
|
|
5
|
+
import { basename as basename4, dirname as dirname8, join as join12, resolve as resolve3 } from "node:path";
|
|
7
6
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
8
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -45,6 +44,63 @@ var Command = class {
|
|
|
45
44
|
}
|
|
46
45
|
};
|
|
47
46
|
|
|
47
|
+
// src/utils/style.ts
|
|
48
|
+
var env = process.env;
|
|
49
|
+
function detectColor() {
|
|
50
|
+
if ("NO_COLOR" in env && env.NO_COLOR !== "") return false;
|
|
51
|
+
const force = env.FORCE_COLOR;
|
|
52
|
+
if (force === "0" || force === "false") return false;
|
|
53
|
+
if (force !== void 0 && force !== "") return true;
|
|
54
|
+
if (env.TERM === "dumb") return false;
|
|
55
|
+
return Boolean(process.stdout.isTTY);
|
|
56
|
+
}
|
|
57
|
+
var colorEnabled = detectColor();
|
|
58
|
+
function sgr(open, close) {
|
|
59
|
+
const prefix = `\x1B[${open}m`;
|
|
60
|
+
const suffix = `\x1B[${close}m`;
|
|
61
|
+
return (value) => colorEnabled ? `${prefix}${value}${suffix}` : String(value);
|
|
62
|
+
}
|
|
63
|
+
var bold = sgr(1, 22);
|
|
64
|
+
var dim = sgr(2, 22);
|
|
65
|
+
var italic = sgr(3, 23);
|
|
66
|
+
var underline = sgr(4, 24);
|
|
67
|
+
var red = sgr(31, 39);
|
|
68
|
+
var green = sgr(32, 39);
|
|
69
|
+
var yellow = sgr(33, 39);
|
|
70
|
+
var blue = sgr(34, 39);
|
|
71
|
+
var magenta = sgr(35, 39);
|
|
72
|
+
var cyan = sgr(36, 39);
|
|
73
|
+
var gray = sgr(90, 39);
|
|
74
|
+
var glyph = {
|
|
75
|
+
pass: "\u2714",
|
|
76
|
+
fail: "\u2716",
|
|
77
|
+
warn: "\u26A0",
|
|
78
|
+
skip: "\u25CB",
|
|
79
|
+
info: "\u2139",
|
|
80
|
+
arrow: "\u21B3",
|
|
81
|
+
bullet: "\u2022",
|
|
82
|
+
dot: "\xB7",
|
|
83
|
+
add: "+",
|
|
84
|
+
chevron: "\u25B8",
|
|
85
|
+
pointer: "\u276F"
|
|
86
|
+
};
|
|
87
|
+
var STATUS_STYLES = {
|
|
88
|
+
pass: { glyph: glyph.pass, color: green, label: "pass" },
|
|
89
|
+
fail: { glyph: glyph.fail, color: red, label: "fail" },
|
|
90
|
+
warn: { glyph: glyph.warn, color: yellow, label: "warn" },
|
|
91
|
+
skip: { glyph: glyph.skip, color: gray, label: "skip" },
|
|
92
|
+
applied: { glyph: glyph.pass, color: green, label: "applied" },
|
|
93
|
+
noop: { glyph: glyph.skip, color: gray, label: "noop" },
|
|
94
|
+
blocked: { glyph: glyph.fail, color: red, label: "blocked" },
|
|
95
|
+
skipped: { glyph: glyph.skip, color: gray, label: "skipped" }
|
|
96
|
+
};
|
|
97
|
+
function statusStyle(status) {
|
|
98
|
+
return STATUS_STYLES[status] ?? { glyph: glyph.dot, color: dim, label: status };
|
|
99
|
+
}
|
|
100
|
+
function joinDot(fragments) {
|
|
101
|
+
return fragments.join(dim(` ${glyph.dot} `));
|
|
102
|
+
}
|
|
103
|
+
|
|
48
104
|
// src/recipes/Recipe.ts
|
|
49
105
|
var Recipe = class {
|
|
50
106
|
context;
|
|
@@ -57,26 +113,22 @@ var Recipe = class {
|
|
|
57
113
|
return this;
|
|
58
114
|
}
|
|
59
115
|
async execute() {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
116
|
+
const subsystem = this.constructor.name.replace("Recipe", "").toLowerCase();
|
|
117
|
+
const dryRun = this.context.dryRun;
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log(` ${cyan(bold(glyph.chevron))} ${bold(`Initializing ${subsystem} subsystem`)}${dryRun ? ` ${dim(glyph.dot)} ${yellow("dry run")}` : ""}`);
|
|
120
|
+
console.log("");
|
|
66
121
|
for (const command of this.ingredients) {
|
|
67
122
|
const result = await command.invoke();
|
|
68
|
-
|
|
69
|
-
console.log(result.message);
|
|
70
|
-
} else {
|
|
71
|
-
console.log(result.message);
|
|
72
|
-
}
|
|
123
|
+
console.log(result.message.split("\n").map((line) => line ? ` ${line}` : line).join("\n"));
|
|
73
124
|
}
|
|
74
|
-
if (!
|
|
125
|
+
if (!dryRun) {
|
|
75
126
|
this.printNextSteps();
|
|
76
127
|
} else {
|
|
77
128
|
console.log("");
|
|
78
|
-
console.log("
|
|
79
|
-
console.log("
|
|
129
|
+
console.log(` ${green(glyph.pass)} ${dim("Dry-run complete \u2014 no files were modified.")}`);
|
|
130
|
+
console.log(` ${dim("Remove --dry-run to apply changes.")}`);
|
|
131
|
+
console.log("");
|
|
80
132
|
}
|
|
81
133
|
}
|
|
82
134
|
};
|
|
@@ -219,11 +271,111 @@ if __name__ == "__main__":
|
|
|
219
271
|
}
|
|
220
272
|
};
|
|
221
273
|
|
|
274
|
+
// src/commands/AddMiseCodegraphScript.ts
|
|
275
|
+
import { chmodSync } from "fs";
|
|
276
|
+
import { join as join2 } from "path";
|
|
277
|
+
var AddMiseCodegraphScript = class extends Command {
|
|
278
|
+
async invoke() {
|
|
279
|
+
const filePath = ".mise/scripts/codegraph.sh";
|
|
280
|
+
if (this.fileExists(filePath) && !this.context.force) {
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph.sh already exists"),
|
|
284
|
+
filePath
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const content = `#!/usr/bin/env bash
|
|
288
|
+
# Mise enter hook: ensure the CodeGraph CLI is available and initialize the
|
|
289
|
+
# project index. If \`codegraph\` is not installed, this script installs it
|
|
290
|
+
# non-interactively into the project-local .mise/bin directory and retries.
|
|
291
|
+
#
|
|
292
|
+
# This is intended to run from a mise enter hook so onboarding a new host is
|
|
293
|
+
# fully automatic.
|
|
294
|
+
|
|
295
|
+
set -euo pipefail
|
|
296
|
+
|
|
297
|
+
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
298
|
+
PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
|
|
299
|
+
mkdir -p "$PROJECT_BIN_DIR"
|
|
300
|
+
|
|
301
|
+
# Install the CodeGraph CLI into the project-local bin directory.
|
|
302
|
+
install_codegraph() {
|
|
303
|
+
echo "[mise] codegraph not found. Installing non-interactively..."
|
|
304
|
+
export CODEGRAPH_BIN_DIR="$PROJECT_BIN_DIR"
|
|
305
|
+
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
|
|
306
|
+
|
|
307
|
+
if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
|
|
308
|
+
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
309
|
+
else
|
|
310
|
+
echo "[mise] codegraph install did not place a binary at $PROJECT_BIN_DIR/codegraph" >&2
|
|
311
|
+
return 1
|
|
312
|
+
fi
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Ensure a codegraph binary is available on PATH.
|
|
316
|
+
ensure_codegraph() {
|
|
317
|
+
if command -v codegraph >/dev/null 2>&1; then
|
|
318
|
+
return 0
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
# Check the project-local bin dir first (previous install from this hook).
|
|
322
|
+
if [ -x "$PROJECT_BIN_DIR/codegraph" ]; then
|
|
323
|
+
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
324
|
+
return 0
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
# Check typical user-level install locations before fetching anything.
|
|
328
|
+
for d in "$HOME/.local/bin" "$HOME/.codegraph/current/bin"; do
|
|
329
|
+
if [ -x "$d/codegraph" ]; then
|
|
330
|
+
export PATH="$d:$PATH"
|
|
331
|
+
return 0
|
|
332
|
+
fi
|
|
333
|
+
done
|
|
334
|
+
|
|
335
|
+
install_codegraph
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# Attempt to initialize the project graph. If the command is missing, install
|
|
339
|
+
# it and retry once.
|
|
340
|
+
init_project() {
|
|
341
|
+
local err_file
|
|
342
|
+
err_file="$(mktemp)"
|
|
343
|
+
trap 'rm -f "$err_file"' RETURN
|
|
344
|
+
|
|
345
|
+
if codegraph init -i "$REPO_ROOT" 2>"$err_file"; then
|
|
346
|
+
return 0
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
# If the failure looks like a missing binary, install and retry.
|
|
350
|
+
if grep -qiE 'command not found|not installed|No such file|executable file not found' "$err_file" 2>/dev/null; then
|
|
351
|
+
ensure_codegraph
|
|
352
|
+
codegraph init -i "$REPO_ROOT"
|
|
353
|
+
return 0
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
cat "$err_file" >&2
|
|
357
|
+
return 1
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
init_project
|
|
361
|
+
`;
|
|
362
|
+
this.writeFile(filePath, content);
|
|
363
|
+
if (!this.context.dryRun) {
|
|
364
|
+
chmodSync(join2(this.context.targetDir, filePath), 493);
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
success: true,
|
|
368
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
369
|
+
filePath
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
222
374
|
// src/recipes/MiseRecipe.ts
|
|
223
375
|
var MiseRecipe = class extends Recipe {
|
|
224
376
|
constructor(context) {
|
|
225
377
|
super(context);
|
|
226
|
-
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript);
|
|
378
|
+
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
|
|
227
379
|
}
|
|
228
380
|
printNextSteps() {
|
|
229
381
|
console.log("\u{1F389} Mise subsystem initialized successfully!");
|
|
@@ -453,19 +605,19 @@ var NodeRecipe = class extends Recipe {
|
|
|
453
605
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
454
606
|
import { homedir, platform } from "node:os";
|
|
455
607
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
456
|
-
import { join as
|
|
608
|
+
import { join as join3, dirname as dirname2 } from "node:path";
|
|
457
609
|
function resolveTemplateConfigPath() {
|
|
458
610
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
459
611
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
460
612
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
461
|
-
const base = xdg && xdg.length ? xdg :
|
|
462
|
-
return
|
|
613
|
+
const base = xdg && xdg.length ? xdg : join3(homedir(), ".config");
|
|
614
|
+
return join3(base, "hermes-agent-template", "config.toml");
|
|
463
615
|
}
|
|
464
616
|
function detectHermesBin(home) {
|
|
465
617
|
const candidates = [
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
618
|
+
join3(home, "code", "hermes-agent", "venv", "bin", "hermes"),
|
|
619
|
+
join3(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
|
|
620
|
+
join3(home, ".local", "bin", "hermes")
|
|
469
621
|
];
|
|
470
622
|
for (const c of candidates) {
|
|
471
623
|
if (existsSync2(c)) return c;
|
|
@@ -475,9 +627,9 @@ function detectHermesBin(home) {
|
|
|
475
627
|
function renderHostConfig() {
|
|
476
628
|
const home = homedir();
|
|
477
629
|
const hermesBin = detectHermesBin(home);
|
|
478
|
-
const hermesRepo =
|
|
479
|
-
const scaffoldDir =
|
|
480
|
-
const skillsDir =
|
|
630
|
+
const hermesRepo = join3(home, "code", "hermes-agent");
|
|
631
|
+
const scaffoldDir = join3(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
632
|
+
const skillsDir = join3(home, ".agents", "skills");
|
|
481
633
|
return `# hermes-agent-template \u2014 host configuration
|
|
482
634
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
483
635
|
#
|
|
@@ -544,7 +696,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
544
696
|
};
|
|
545
697
|
|
|
546
698
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
547
|
-
import { basename, join as
|
|
699
|
+
import { basename, join as join4 } from "node:path";
|
|
548
700
|
import { readFileSync } from "node:fs";
|
|
549
701
|
import * as p from "@clack/prompts";
|
|
550
702
|
|
|
@@ -573,7 +725,7 @@ function deriveProfileName(repo, role) {
|
|
|
573
725
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
574
726
|
function detectTicketProvider(targetDir) {
|
|
575
727
|
try {
|
|
576
|
-
const t = JSON.parse(readFileSync(
|
|
728
|
+
const t = JSON.parse(readFileSync(join4(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
577
729
|
return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
|
|
578
730
|
} catch {
|
|
579
731
|
return void 0;
|
|
@@ -709,7 +861,7 @@ var PromptForAgentConfig = class extends Command {
|
|
|
709
861
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
710
862
|
import { spawnSync } from "node:child_process";
|
|
711
863
|
import { homedir as homedir2 } from "node:os";
|
|
712
|
-
import { join as
|
|
864
|
+
import { join as join5, dirname as dirname3 } from "node:path";
|
|
713
865
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
714
866
|
import { fileURLToPath } from "node:url";
|
|
715
867
|
import * as p2 from "@clack/prompts";
|
|
@@ -721,8 +873,8 @@ function resolveVendoredTemplate(name) {
|
|
|
721
873
|
return void 0;
|
|
722
874
|
}
|
|
723
875
|
for (let i = 0; i < 8; i++) {
|
|
724
|
-
const candidate =
|
|
725
|
-
if (existsSync3(
|
|
876
|
+
const candidate = join5(dir, "templates", name);
|
|
877
|
+
if (existsSync3(join5(candidate, "copier.yml"))) return candidate;
|
|
726
878
|
const parent = dirname3(dir);
|
|
727
879
|
if (parent === dir) break;
|
|
728
880
|
dir = parent;
|
|
@@ -741,7 +893,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
741
893
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
742
894
|
};
|
|
743
895
|
}
|
|
744
|
-
const roleDir =
|
|
896
|
+
const roleDir = join5(ctx.targetDir, "agents", "hermes", role);
|
|
745
897
|
ctx.roleDir = roleDir;
|
|
746
898
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
747
899
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -751,7 +903,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
751
903
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
752
904
|
};
|
|
753
905
|
}
|
|
754
|
-
if (existsSync3(
|
|
906
|
+
if (existsSync3(join5(roleDir, "role.yaml")) && !ctx.force) {
|
|
755
907
|
if (ctx.yes) {
|
|
756
908
|
ctx.force = true;
|
|
757
909
|
} else {
|
|
@@ -768,7 +920,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
768
920
|
ctx.force = true;
|
|
769
921
|
}
|
|
770
922
|
}
|
|
771
|
-
const
|
|
923
|
+
const env2 = {
|
|
772
924
|
...process.env,
|
|
773
925
|
SKIP_TELEGRAM: "1",
|
|
774
926
|
SKIP_EMAIL: "1",
|
|
@@ -778,9 +930,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
778
930
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
779
931
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
780
932
|
};
|
|
781
|
-
const LOCAL_TEMPLATE =
|
|
933
|
+
const LOCAL_TEMPLATE = join5(homedir2(), "code", "hermes-agent-template");
|
|
782
934
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
783
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(
|
|
935
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join5(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
784
936
|
const args = [
|
|
785
937
|
"copy",
|
|
786
938
|
templateSrc,
|
|
@@ -811,13 +963,13 @@ var RunCopierTemplate = class extends Command {
|
|
|
811
963
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
812
964
|
};
|
|
813
965
|
}
|
|
814
|
-
mkdirSync3(
|
|
966
|
+
mkdirSync3(join5(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
815
967
|
const spinner4 = p2.spinner();
|
|
816
968
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
817
969
|
const result = spawnSync("copier", args, {
|
|
818
970
|
stdio: "inherit",
|
|
819
971
|
// pass the interactive output through; copier prints its own progress
|
|
820
|
-
env,
|
|
972
|
+
env: env2,
|
|
821
973
|
cwd: ctx.targetDir
|
|
822
974
|
});
|
|
823
975
|
spinner4.stop(result.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
|
|
@@ -836,7 +988,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
836
988
|
|
|
837
989
|
// src/commands/hermes/WireTelegram.ts
|
|
838
990
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
839
|
-
import { join as
|
|
991
|
+
import { join as join6 } from "node:path";
|
|
840
992
|
import { existsSync as existsSync4, unlinkSync } from "node:fs";
|
|
841
993
|
import * as p3 from "@clack/prompts";
|
|
842
994
|
var WireTelegram = class extends Command {
|
|
@@ -925,14 +1077,14 @@ var WireTelegram = class extends Command {
|
|
|
925
1077
|
if (p3.isCancel(allowedAnswer)) {
|
|
926
1078
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
927
1079
|
}
|
|
928
|
-
const script =
|
|
1080
|
+
const script = join6(roleDir, ".scripts", "30-telegram.sh");
|
|
929
1081
|
if (!existsSync4(script)) {
|
|
930
1082
|
return {
|
|
931
1083
|
success: false,
|
|
932
1084
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
933
1085
|
};
|
|
934
1086
|
}
|
|
935
|
-
const marker =
|
|
1087
|
+
const marker = join6(roleDir, ".scripts", ".done-30-telegram");
|
|
936
1088
|
if (existsSync4(marker)) unlinkSync(marker);
|
|
937
1089
|
const spinner4 = p3.spinner();
|
|
938
1090
|
spinner4.start("Verifying token + wiring profile");
|
|
@@ -960,7 +1112,7 @@ function cap(s) {
|
|
|
960
1112
|
|
|
961
1113
|
// src/commands/hermes/WireEmail.ts
|
|
962
1114
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
963
|
-
import { join as
|
|
1115
|
+
import { join as join7 } from "node:path";
|
|
964
1116
|
import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
965
1117
|
import * as p4 from "@clack/prompts";
|
|
966
1118
|
var WireEmail = class extends Command {
|
|
@@ -976,7 +1128,7 @@ var WireEmail = class extends Command {
|
|
|
976
1128
|
if (!targetRepo || !role || !roleDir) {
|
|
977
1129
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
978
1130
|
}
|
|
979
|
-
const script =
|
|
1131
|
+
const script = join7(roleDir, ".scripts", "50-email.sh");
|
|
980
1132
|
if (!existsSync5(script)) {
|
|
981
1133
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
982
1134
|
}
|
|
@@ -1039,7 +1191,7 @@ var WireEmail = class extends Command {
|
|
|
1039
1191
|
}
|
|
1040
1192
|
}
|
|
1041
1193
|
}
|
|
1042
|
-
const marker =
|
|
1194
|
+
const marker = join7(roleDir, ".scripts", ".done-50-email");
|
|
1043
1195
|
if (existsSync5(marker)) unlinkSync2(marker);
|
|
1044
1196
|
const spinner4 = p4.spinner();
|
|
1045
1197
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
@@ -1128,7 +1280,7 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1128
1280
|
|
|
1129
1281
|
// src/commands/AgentHooksCommands.ts
|
|
1130
1282
|
import { homedir as homedir3 } from "node:os";
|
|
1131
|
-
import { join as
|
|
1283
|
+
import { join as join8, dirname as dirname4 } from "node:path";
|
|
1132
1284
|
import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1133
1285
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1134
1286
|
function resolveTemplateRoot() {
|
|
@@ -1139,16 +1291,16 @@ function resolveTemplateRoot() {
|
|
|
1139
1291
|
try {
|
|
1140
1292
|
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
1141
1293
|
for (let i = 0; i < 8; i++) {
|
|
1142
|
-
candidates.push(
|
|
1294
|
+
candidates.push(join8(dir, "templates", "commonproject", "template"));
|
|
1143
1295
|
const parent = dirname4(dir);
|
|
1144
1296
|
if (parent === dir) break;
|
|
1145
1297
|
dir = parent;
|
|
1146
1298
|
}
|
|
1147
1299
|
} catch {
|
|
1148
1300
|
}
|
|
1149
|
-
candidates.push(
|
|
1301
|
+
candidates.push(join8(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1150
1302
|
for (const c of candidates) {
|
|
1151
|
-
if (existsSync6(
|
|
1303
|
+
if (existsSync6(join8(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1152
1304
|
}
|
|
1153
1305
|
throw new Error(
|
|
1154
1306
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1172,8 +1324,8 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1172
1324
|
const created = [];
|
|
1173
1325
|
const skipped = [];
|
|
1174
1326
|
for (const { rel, dir } of items) {
|
|
1175
|
-
const src =
|
|
1176
|
-
const dest =
|
|
1327
|
+
const src = join8(templateRoot, rel);
|
|
1328
|
+
const dest = join8(this.context.targetDir, rel);
|
|
1177
1329
|
if (!existsSync6(src)) continue;
|
|
1178
1330
|
if (existsSync6(dest) && !this.context.force) {
|
|
1179
1331
|
skipped.push(rel);
|
|
@@ -1198,7 +1350,7 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1198
1350
|
static CR = "{{config_root}}";
|
|
1199
1351
|
// mise's own runtime var — emitted literally
|
|
1200
1352
|
async invoke() {
|
|
1201
|
-
const misePath =
|
|
1353
|
+
const misePath = join8(this.context.targetDir, "mise.toml");
|
|
1202
1354
|
if (!existsSync6(misePath)) {
|
|
1203
1355
|
return {
|
|
1204
1356
|
success: false,
|
|
@@ -1319,7 +1471,7 @@ var RECIPE_REGISTRY = {
|
|
|
1319
1471
|
name: "mise",
|
|
1320
1472
|
description: "Mise task runner and environment setup",
|
|
1321
1473
|
class: MiseRecipe,
|
|
1322
|
-
commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript"]
|
|
1474
|
+
commands: ["AddMiseToml", "AddDotenv", "AddMiseTasksStructure", "AddMiseBaseToml", "AddMiseBaseScript", "AddMiseCodegraphScript"]
|
|
1323
1475
|
},
|
|
1324
1476
|
docker: {
|
|
1325
1477
|
name: "docker",
|
|
@@ -1409,6 +1561,12 @@ var COMMAND_REGISTRY = {
|
|
|
1409
1561
|
group: "mise",
|
|
1410
1562
|
class: AddMiseBaseScript
|
|
1411
1563
|
},
|
|
1564
|
+
AddMiseCodegraphScript: {
|
|
1565
|
+
name: "AddMiseCodegraphScript",
|
|
1566
|
+
description: "Create .mise/scripts/codegraph.sh enter hook",
|
|
1567
|
+
group: "mise",
|
|
1568
|
+
class: AddMiseCodegraphScript
|
|
1569
|
+
},
|
|
1412
1570
|
AddDotenv: {
|
|
1413
1571
|
name: "AddDotenv",
|
|
1414
1572
|
description: "Create .env.example file",
|
|
@@ -1430,14 +1588,14 @@ function createRecipe(name, context) {
|
|
|
1430
1588
|
|
|
1431
1589
|
// src/utils/version.ts
|
|
1432
1590
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1433
|
-
import { dirname as dirname5, join as
|
|
1591
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
1434
1592
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1435
1593
|
var PJANGLER_VERSION = (() => {
|
|
1436
1594
|
try {
|
|
1437
1595
|
let dir = dirname5(fileURLToPath3(import.meta.url));
|
|
1438
1596
|
for (let i = 0; i < 4; i++) {
|
|
1439
1597
|
try {
|
|
1440
|
-
const raw = readFileSync3(
|
|
1598
|
+
const raw = readFileSync3(join9(dir, "package.json"), "utf8");
|
|
1441
1599
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
1442
1600
|
} catch {
|
|
1443
1601
|
const parent = dirname5(dir);
|
|
@@ -1451,8 +1609,8 @@ var PJANGLER_VERSION = (() => {
|
|
|
1451
1609
|
})();
|
|
1452
1610
|
|
|
1453
1611
|
// src/parity/index.ts
|
|
1454
|
-
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, copyFileSync } from "node:fs";
|
|
1455
|
-
import { dirname as dirname6, join as
|
|
1612
|
+
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync as chmodSync2, copyFileSync } from "node:fs";
|
|
1613
|
+
import { basename as basename2, dirname as dirname6, join as join10, relative, resolve } from "node:path";
|
|
1456
1614
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1457
1615
|
import { homedir as homedir4 } from "node:os";
|
|
1458
1616
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
@@ -1472,6 +1630,28 @@ enter = [
|
|
|
1472
1630
|
patterns = ["AGENTS.md"]
|
|
1473
1631
|
task = "link-agentfiles"
|
|
1474
1632
|
|
|
1633
|
+
[tasks.link-agentfiles]
|
|
1634
|
+
description = "Symlink all agent files to AGENTS.md"
|
|
1635
|
+
run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
|
|
1636
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
1637
|
+
"{{config_root}}/.mise/scripts/link-agentfiles.sh",
|
|
1638
|
+
"op inject -i .env.op > .env"
|
|
1639
|
+
];
|
|
1640
|
+
var LINK_AGENTFILES_HOOKS_BLOCK = `# This block will handle the linking of
|
|
1641
|
+
# agent files to the main AGENTS.md file.
|
|
1642
|
+
#
|
|
1643
|
+
# TODO: Ensure this works for all levels of nesting.
|
|
1644
|
+
# i.e. All linked agent files MUST be siblings at
|
|
1645
|
+
# any given level of nesting.
|
|
1646
|
+
[hooks]
|
|
1647
|
+
enter = [
|
|
1648
|
+
"{{config_root}}/.mise/scripts/link-agentfiles.sh",
|
|
1649
|
+
"op inject -i .env.op > .env",
|
|
1650
|
+
]`;
|
|
1651
|
+
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
1652
|
+
patterns = ["AGENTS.md"]
|
|
1653
|
+
task = "link-agentfiles"
|
|
1654
|
+
|
|
1475
1655
|
[tasks.link-agentfiles]
|
|
1476
1656
|
description = "Symlink all agent files to AGENTS.md"
|
|
1477
1657
|
run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
|
|
@@ -1504,7 +1684,7 @@ run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
|
1504
1684
|
function resolvePjanglerRoot() {
|
|
1505
1685
|
let dir = dirname6(fileURLToPath4(import.meta.url));
|
|
1506
1686
|
while (dir !== dirname6(dir)) {
|
|
1507
|
-
if (existsSync7(
|
|
1687
|
+
if (existsSync7(join10(dir, "package.json")) && existsSync7(join10(dir, "templates", "commonproject", "copier.yml"))) {
|
|
1508
1688
|
return dir;
|
|
1509
1689
|
}
|
|
1510
1690
|
dir = dirname6(dir);
|
|
@@ -1567,10 +1747,10 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
1567
1747
|
return { changed: true };
|
|
1568
1748
|
}
|
|
1569
1749
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
1570
|
-
const agentsPath =
|
|
1750
|
+
const agentsPath = join10(repoRoot, "AGENTS.md");
|
|
1571
1751
|
if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
|
|
1572
1752
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1573
|
-
const source =
|
|
1753
|
+
const source = join10(repoRoot, file);
|
|
1574
1754
|
if (!existsSync7(source)) continue;
|
|
1575
1755
|
const stat = lstatSync(source);
|
|
1576
1756
|
if (stat.isSymbolicLink()) continue;
|
|
@@ -1580,7 +1760,7 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
1580
1760
|
}
|
|
1581
1761
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
1582
1762
|
}
|
|
1583
|
-
const readmePath =
|
|
1763
|
+
const readmePath = join10(repoRoot, "README.md");
|
|
1584
1764
|
if (existsSync7(readmePath)) {
|
|
1585
1765
|
const stat = lstatSync(readmePath);
|
|
1586
1766
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
@@ -1620,11 +1800,11 @@ function yamlGet(text3, keyPath) {
|
|
|
1620
1800
|
return "";
|
|
1621
1801
|
}
|
|
1622
1802
|
function discoverRoles(repoRoot) {
|
|
1623
|
-
const rolesDir =
|
|
1803
|
+
const rolesDir = join10(repoRoot, "agents", "hermes");
|
|
1624
1804
|
if (!existsSync7(rolesDir)) return [];
|
|
1625
1805
|
return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
1626
|
-
const roleDir =
|
|
1627
|
-
const roleYamlPath =
|
|
1806
|
+
const roleDir = join10(rolesDir, entry.name);
|
|
1807
|
+
const roleYamlPath = join10(roleDir, "role.yaml");
|
|
1628
1808
|
if (!existsSync7(roleYamlPath)) return null;
|
|
1629
1809
|
const text3 = readText(roleYamlPath);
|
|
1630
1810
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
@@ -1649,7 +1829,7 @@ function discoverRoles(repoRoot) {
|
|
|
1649
1829
|
}).filter((value) => Boolean(value));
|
|
1650
1830
|
}
|
|
1651
1831
|
function registryPath(homeDir) {
|
|
1652
|
-
return
|
|
1832
|
+
return join10(homeDir, ".hermes", "agents-registry.yaml");
|
|
1653
1833
|
}
|
|
1654
1834
|
function systemctlUser(args) {
|
|
1655
1835
|
const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -1660,7 +1840,7 @@ function systemctlUser(args) {
|
|
|
1660
1840
|
};
|
|
1661
1841
|
}
|
|
1662
1842
|
function templateScript(ctx, name) {
|
|
1663
|
-
const source =
|
|
1843
|
+
const source = join10(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
1664
1844
|
return existsSync7(source) ? readText(source) : void 0;
|
|
1665
1845
|
}
|
|
1666
1846
|
function templateVersioningScript(ctx) {
|
|
@@ -1669,8 +1849,24 @@ function templateVersioningScript(ctx) {
|
|
|
1669
1849
|
function templateLinkAgentfilesScript(ctx) {
|
|
1670
1850
|
return templateScript(ctx, "link-agentfiles.sh");
|
|
1671
1851
|
}
|
|
1852
|
+
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
1853
|
+
const project = readProjectJson(ctx);
|
|
1854
|
+
const projectName = String(project?.project_name ?? basename2(ctx.repoRoot) ?? "project");
|
|
1855
|
+
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
1856
|
+
}
|
|
1857
|
+
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
1858
|
+
const targetPath = join10(ctx.repoRoot, "mise.toml");
|
|
1859
|
+
if (existsSync7(targetPath)) return false;
|
|
1860
|
+
const sourcePath = join10(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
1861
|
+
if (!existsSync7(sourcePath)) return false;
|
|
1862
|
+
changedFiles.push(targetPath);
|
|
1863
|
+
if (!ctx.dryRun) {
|
|
1864
|
+
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
1865
|
+
}
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1672
1868
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1673
|
-
const packageJson =
|
|
1869
|
+
const packageJson = join10(repoRoot, "package.json");
|
|
1674
1870
|
return existsSync7(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";
|
|
1675
1871
|
}
|
|
1676
1872
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
@@ -1695,7 +1891,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
1695
1891
|
function requiredMisePathEntries(ctx) {
|
|
1696
1892
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
1697
1893
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
1698
|
-
if (existsSync7(
|
|
1894
|
+
if (existsSync7(join10(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1699
1895
|
}
|
|
1700
1896
|
return required;
|
|
1701
1897
|
}
|
|
@@ -1724,25 +1920,127 @@ ${text3.replace(/^\s+/, "")}`;
|
|
|
1724
1920
|
if (pathLine[0] === nextLine) return text3;
|
|
1725
1921
|
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
1726
1922
|
}
|
|
1727
|
-
function
|
|
1728
|
-
const
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1923
|
+
function removeTomlSection(text3, headerPattern, marker, options) {
|
|
1924
|
+
const lines = text3.split("\n");
|
|
1925
|
+
let start = -1;
|
|
1926
|
+
let end = -1;
|
|
1927
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1928
|
+
if (!headerPattern.test(lines[i])) continue;
|
|
1929
|
+
if (marker) {
|
|
1930
|
+
let hasMarker = false;
|
|
1931
|
+
for (let j = i + 1; j < lines.length && !/^\[[^\]]+\]/.test(lines[j]); j++) {
|
|
1932
|
+
if (marker.test(lines[j])) {
|
|
1933
|
+
hasMarker = true;
|
|
1934
|
+
break;
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
if (!hasMarker) continue;
|
|
1938
|
+
}
|
|
1939
|
+
start = i;
|
|
1940
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1941
|
+
if (/^\[[^\]]+\]/.test(lines[j])) {
|
|
1942
|
+
end = j;
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
if (end === -1) end = lines.length;
|
|
1947
|
+
break;
|
|
1732
1948
|
}
|
|
1733
|
-
|
|
1949
|
+
if (start === -1) return text3;
|
|
1950
|
+
if (options?.includePrecedingComments) {
|
|
1951
|
+
while (start > 0 && lines[start - 1].trim().startsWith("#")) {
|
|
1952
|
+
start--;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
|
|
1956
|
+
return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1957
|
+
}
|
|
1958
|
+
function insertTomlBlockBeforeVersioning(text3, block) {
|
|
1959
|
+
const versioningIndex = text3.indexOf("# >>> mise-versioning >>>");
|
|
1734
1960
|
if (versioningIndex >= 0) {
|
|
1735
|
-
return `${
|
|
1961
|
+
return `${text3.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
|
|
1736
1962
|
|
|
1737
|
-
${
|
|
1963
|
+
${text3.slice(versioningIndex)}`;
|
|
1738
1964
|
}
|
|
1739
|
-
return `${
|
|
1965
|
+
return `${text3.replace(/\s*$/, "")}
|
|
1740
1966
|
|
|
1741
|
-
${
|
|
1967
|
+
${block}
|
|
1742
1968
|
`;
|
|
1743
1969
|
}
|
|
1970
|
+
function extractTomlStrings(text3) {
|
|
1971
|
+
const values = [];
|
|
1972
|
+
const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
|
|
1973
|
+
for (const match of text3.matchAll(stringPattern)) {
|
|
1974
|
+
if (match[1] !== void 0) {
|
|
1975
|
+
try {
|
|
1976
|
+
values.push(JSON.parse(`"${match[1]}"`));
|
|
1977
|
+
} catch {
|
|
1978
|
+
values.push(match[1]);
|
|
1979
|
+
}
|
|
1980
|
+
} else if (match[2] !== void 0) {
|
|
1981
|
+
values.push(match[2]);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return values;
|
|
1985
|
+
}
|
|
1986
|
+
function isManagedHookEntry(value) {
|
|
1987
|
+
const trimmed = value.trim();
|
|
1988
|
+
return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
|
|
1989
|
+
}
|
|
1990
|
+
function renderHookEntries(entries, indent = "") {
|
|
1991
|
+
return [
|
|
1992
|
+
`${indent}enter = [`,
|
|
1993
|
+
...entries.map((entry) => `${indent} ${JSON.stringify(entry)},`),
|
|
1994
|
+
`${indent}]`
|
|
1995
|
+
];
|
|
1996
|
+
}
|
|
1997
|
+
function upsertLinkAgentfilesHooks(text3) {
|
|
1998
|
+
const lines = text3.split("\n");
|
|
1999
|
+
const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
|
|
2000
|
+
if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text3, LINK_AGENTFILES_HOOKS_BLOCK);
|
|
2001
|
+
let hooksEnd = lines.length;
|
|
2002
|
+
for (let i = hooksStart + 1; i < lines.length; i++) {
|
|
2003
|
+
if (/^\[[^\]]+\]/.test(lines[i].trim())) {
|
|
2004
|
+
hooksEnd = i;
|
|
2005
|
+
break;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
let enterStart = -1;
|
|
2009
|
+
let enterEnd = -1;
|
|
2010
|
+
for (let i = hooksStart + 1; i < hooksEnd; i++) {
|
|
2011
|
+
if (!/^\s*enter\s*=/.test(lines[i])) continue;
|
|
2012
|
+
enterStart = i;
|
|
2013
|
+
enterEnd = i + 1;
|
|
2014
|
+
const afterEquals = lines[i].slice(lines[i].indexOf("=") + 1);
|
|
2015
|
+
if (afterEquals.includes("[") && !afterEquals.includes("]")) {
|
|
2016
|
+
while (enterEnd < hooksEnd && !lines[enterEnd].includes("]")) enterEnd++;
|
|
2017
|
+
if (enterEnd < hooksEnd) enterEnd++;
|
|
2018
|
+
}
|
|
2019
|
+
break;
|
|
2020
|
+
}
|
|
2021
|
+
const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
|
|
2022
|
+
const preserved = extractTomlStrings(existingBlock).filter((entry) => !isManagedHookEntry(entry));
|
|
2023
|
+
const merged = [...LINK_AGENTFILES_HOOK_ENTRIES];
|
|
2024
|
+
for (const entry of preserved) {
|
|
2025
|
+
if (!merged.includes(entry)) merged.push(entry);
|
|
2026
|
+
}
|
|
2027
|
+
const indent = enterStart >= 0 ? lines[enterStart].match(/^\s*/)?.[0] ?? "" : "";
|
|
2028
|
+
const rendered = renderHookEntries(merged, indent);
|
|
2029
|
+
if (enterStart >= 0) {
|
|
2030
|
+
return lines.slice(0, enterStart).concat(rendered, lines.slice(enterEnd)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
2031
|
+
}
|
|
2032
|
+
return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
2033
|
+
}
|
|
2034
|
+
function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
2035
|
+
const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
|
|
2036
|
+
if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
|
|
2037
|
+
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
2038
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
2039
|
+
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
2040
|
+
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2041
|
+
}
|
|
1744
2042
|
function readProjectJson(ctx) {
|
|
1745
|
-
return tryParseJson(safeReadText(
|
|
2043
|
+
return tryParseJson(safeReadText(join10(ctx.repoRoot, ".project.json")));
|
|
1746
2044
|
}
|
|
1747
2045
|
function canonicalProjectJson(ctx) {
|
|
1748
2046
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -1754,28 +2052,40 @@ function canonicalProjectJson(ctx) {
|
|
|
1754
2052
|
workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
|
|
1755
2053
|
identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
|
|
1756
2054
|
board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
|
|
1757
|
-
board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || "")
|
|
2055
|
+
board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || ""),
|
|
2056
|
+
state: String((existing.ticket_provider?.state ?? "planned") || "planned")
|
|
1758
2057
|
};
|
|
2058
|
+
const existingAgents = existing.agents ?? {};
|
|
2059
|
+
const discoveredAgents = Object.fromEntries(
|
|
2060
|
+
roles.map((role) => [
|
|
2061
|
+
role.agentId || `${slug}-${role.role}`,
|
|
2062
|
+
{
|
|
2063
|
+
role: role.role,
|
|
2064
|
+
role_dir: relative(ctx.repoRoot, role.roleDir)
|
|
2065
|
+
}
|
|
2066
|
+
])
|
|
2067
|
+
);
|
|
2068
|
+
const agents = { ...existingAgents };
|
|
2069
|
+
for (const [agentId, discovered] of Object.entries(discoveredAgents)) {
|
|
2070
|
+
const existingAgent = existingAgents[agentId] ?? {};
|
|
2071
|
+
agents[agentId] = {
|
|
2072
|
+
role: discovered.role,
|
|
2073
|
+
role_dir: discovered.role_dir,
|
|
2074
|
+
provisioning_state: existingAgent.provisioning_state
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
1759
2077
|
return {
|
|
1760
2078
|
project_name: String(existing.project_name ?? titleCaseSlug(slug)),
|
|
1761
2079
|
project_description: String(existing.project_description ?? ""),
|
|
1762
2080
|
project_slug: slug,
|
|
1763
2081
|
repo_path: ctx.repoRoot,
|
|
1764
2082
|
ticket_provider: ticketProvider,
|
|
1765
|
-
agents
|
|
1766
|
-
roles.map((role) => [
|
|
1767
|
-
role.agentId || `${slug}-${role.role}`,
|
|
1768
|
-
{
|
|
1769
|
-
role: role.role,
|
|
1770
|
-
role_dir: relative(ctx.repoRoot, role.roleDir)
|
|
1771
|
-
}
|
|
1772
|
-
])
|
|
1773
|
-
)
|
|
2083
|
+
agents
|
|
1774
2084
|
};
|
|
1775
2085
|
}
|
|
1776
2086
|
function projectJsonFinding(ctx) {
|
|
1777
|
-
const projectPath =
|
|
1778
|
-
const planeJsonPath =
|
|
2087
|
+
const projectPath = join10(ctx.repoRoot, ".project.json");
|
|
2088
|
+
const planeJsonPath = join10(ctx.repoRoot, ".plane.json");
|
|
1779
2089
|
const details = [];
|
|
1780
2090
|
const data = readProjectJson(ctx);
|
|
1781
2091
|
const roles = discoverRoles(ctx.repoRoot);
|
|
@@ -1802,7 +2112,7 @@ function projectJsonFinding(ctx) {
|
|
|
1802
2112
|
}
|
|
1803
2113
|
}
|
|
1804
2114
|
const ticketProvider = data.ticket_provider ?? {};
|
|
1805
|
-
for (const key of ["type", "workspace", "identifier", "board_id", "board_url"]) {
|
|
2115
|
+
for (const key of ["type", "workspace", "identifier", "board_id", "board_url", "state"]) {
|
|
1806
2116
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
1807
2117
|
}
|
|
1808
2118
|
if (existsSync7(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
@@ -1889,9 +2199,9 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
1889
2199
|
if (!existsSync7(sourceDir)) return;
|
|
1890
2200
|
mkdirSync5(targetDir, { recursive: true });
|
|
1891
2201
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
1892
|
-
const sourcePath =
|
|
2202
|
+
const sourcePath = join10(sourceDir, entry.name);
|
|
1893
2203
|
if (skip?.(sourcePath)) continue;
|
|
1894
|
-
const targetPath =
|
|
2204
|
+
const targetPath = join10(targetDir, entry.name);
|
|
1895
2205
|
if (entry.isDirectory()) {
|
|
1896
2206
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
1897
2207
|
continue;
|
|
@@ -1905,7 +2215,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
1905
2215
|
}
|
|
1906
2216
|
}
|
|
1907
2217
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
1908
|
-
const gitmodulesPath =
|
|
2218
|
+
const gitmodulesPath = join10(repoRoot, ".gitmodules");
|
|
1909
2219
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
1910
2220
|
const owner = role.runtimeOwner || "delorenj";
|
|
1911
2221
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2005,13 +2315,13 @@ var RULES = [
|
|
|
2005
2315
|
id: "mise.config-root",
|
|
2006
2316
|
title: "mise config_root + AGENTS link hooks",
|
|
2007
2317
|
audit: (ctx) => {
|
|
2008
|
-
const misePath =
|
|
2318
|
+
const misePath = join10(ctx.repoRoot, "mise.toml");
|
|
2009
2319
|
if (!existsSync7(misePath)) {
|
|
2010
2320
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2011
2321
|
}
|
|
2012
2322
|
const text3 = readText(misePath);
|
|
2013
2323
|
const details = [];
|
|
2014
|
-
const linkAgentfilesPath =
|
|
2324
|
+
const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2015
2325
|
if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2016
2326
|
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2017
2327
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
@@ -2030,19 +2340,26 @@ var RULES = [
|
|
|
2030
2340
|
};
|
|
2031
2341
|
},
|
|
2032
2342
|
migrate: (ctx, finding) => {
|
|
2033
|
-
const path =
|
|
2343
|
+
const path = join10(ctx.repoRoot, "mise.toml");
|
|
2034
2344
|
const changedFiles = [];
|
|
2345
|
+
const details = [];
|
|
2035
2346
|
if (!existsSync7(path)) {
|
|
2036
|
-
|
|
2347
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2348
|
+
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: [] };
|
|
2349
|
+
}
|
|
2350
|
+
details.push("Initialized mise.toml from generated-project template");
|
|
2351
|
+
if (ctx.dryRun) {
|
|
2352
|
+
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
2353
|
+
}
|
|
2037
2354
|
}
|
|
2038
2355
|
let text3 = readText(path);
|
|
2039
2356
|
const next = upsertLinkAgentfilesBlock(text3, ctx);
|
|
2040
2357
|
if (next !== text3) {
|
|
2041
|
-
changedFiles.push(path);
|
|
2358
|
+
if (!changedFiles.includes(path)) changedFiles.push(path);
|
|
2042
2359
|
if (!ctx.dryRun) writeText(path, next);
|
|
2043
2360
|
text3 = next;
|
|
2044
2361
|
}
|
|
2045
|
-
const linkAgentfilesPath =
|
|
2362
|
+
const linkAgentfilesPath = join10(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2046
2363
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2047
2364
|
if (expectedScript === void 0) {
|
|
2048
2365
|
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: [] };
|
|
@@ -2051,7 +2368,7 @@ var RULES = [
|
|
|
2051
2368
|
changedFiles.push(linkAgentfilesPath);
|
|
2052
2369
|
if (!ctx.dryRun) {
|
|
2053
2370
|
writeText(linkAgentfilesPath, expectedScript);
|
|
2054
|
-
|
|
2371
|
+
chmodSync2(linkAgentfilesPath, 493);
|
|
2055
2372
|
}
|
|
2056
2373
|
}
|
|
2057
2374
|
return {
|
|
@@ -2069,9 +2386,9 @@ var RULES = [
|
|
|
2069
2386
|
title: "managed mise versioning block",
|
|
2070
2387
|
audit: (ctx) => {
|
|
2071
2388
|
const details = [];
|
|
2072
|
-
const misePath =
|
|
2073
|
-
const versioningPath =
|
|
2074
|
-
const manifestPath =
|
|
2389
|
+
const misePath = join10(ctx.repoRoot, "mise.toml");
|
|
2390
|
+
const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2391
|
+
const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2075
2392
|
const text3 = safeReadText(misePath);
|
|
2076
2393
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2077
2394
|
if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
@@ -2087,17 +2404,24 @@ var RULES = [
|
|
|
2087
2404
|
},
|
|
2088
2405
|
migrate: (ctx, finding) => {
|
|
2089
2406
|
const changedFiles = [];
|
|
2090
|
-
const
|
|
2407
|
+
const details = [];
|
|
2408
|
+
const misePath = join10(ctx.repoRoot, "mise.toml");
|
|
2091
2409
|
if (!existsSync7(misePath)) {
|
|
2092
|
-
|
|
2410
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2411
|
+
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: [] };
|
|
2412
|
+
}
|
|
2413
|
+
details.push("Initialized mise.toml from generated-project template");
|
|
2414
|
+
if (ctx.dryRun) {
|
|
2415
|
+
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
2416
|
+
}
|
|
2093
2417
|
}
|
|
2094
2418
|
const currentMise = readText(misePath);
|
|
2095
2419
|
const nextMise = replaceOrAppendManagedBlock(currentMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
2096
2420
|
if (nextMise !== currentMise) {
|
|
2097
|
-
changedFiles.push(misePath);
|
|
2421
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
2098
2422
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2099
2423
|
}
|
|
2100
|
-
const versioningPath =
|
|
2424
|
+
const versioningPath = join10(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2101
2425
|
const expectedScript = templateVersioningScript(ctx);
|
|
2102
2426
|
if (expectedScript === void 0) {
|
|
2103
2427
|
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: [] };
|
|
@@ -2106,10 +2430,10 @@ var RULES = [
|
|
|
2106
2430
|
changedFiles.push(versioningPath);
|
|
2107
2431
|
if (!ctx.dryRun) {
|
|
2108
2432
|
writeText(versioningPath, expectedScript);
|
|
2109
|
-
|
|
2433
|
+
chmodSync2(versioningPath, 493);
|
|
2110
2434
|
}
|
|
2111
2435
|
}
|
|
2112
|
-
const manifestPath =
|
|
2436
|
+
const manifestPath = join10(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2113
2437
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2114
2438
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2115
2439
|
changedFiles.push(manifestPath);
|
|
@@ -2129,9 +2453,9 @@ var RULES = [
|
|
|
2129
2453
|
id: "sot.agent-symlinks",
|
|
2130
2454
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2131
2455
|
audit: (ctx) => {
|
|
2132
|
-
const agentsPath =
|
|
2456
|
+
const agentsPath = join10(ctx.repoRoot, "AGENTS.md");
|
|
2133
2457
|
if (!existsSync7(agentsPath)) {
|
|
2134
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(
|
|
2458
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join10(ctx.repoRoot, file)));
|
|
2135
2459
|
if (fallbackSources.length === 0) {
|
|
2136
2460
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2137
2461
|
}
|
|
@@ -2146,7 +2470,7 @@ var RULES = [
|
|
|
2146
2470
|
}
|
|
2147
2471
|
const details = [];
|
|
2148
2472
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2149
|
-
const full =
|
|
2473
|
+
const full = join10(ctx.repoRoot, file);
|
|
2150
2474
|
const target = readSymlinkTarget(full);
|
|
2151
2475
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2152
2476
|
}
|
|
@@ -2170,7 +2494,7 @@ var RULES = [
|
|
|
2170
2494
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2171
2495
|
}
|
|
2172
2496
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2173
|
-
const full =
|
|
2497
|
+
const full = join10(ctx.repoRoot, file);
|
|
2174
2498
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2175
2499
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2176
2500
|
if (result.changed) changedFiles.push(full);
|
|
@@ -2192,7 +2516,7 @@ var RULES = [
|
|
|
2192
2516
|
migrate: (ctx, finding) => {
|
|
2193
2517
|
const changedFiles = [];
|
|
2194
2518
|
const details = [];
|
|
2195
|
-
const path =
|
|
2519
|
+
const path = join10(ctx.repoRoot, ".project.json");
|
|
2196
2520
|
const existing = readProjectJson(ctx) ?? {};
|
|
2197
2521
|
const canonical = canonicalProjectJson(ctx);
|
|
2198
2522
|
const merged = { ...existing, ...canonical };
|
|
@@ -2202,7 +2526,7 @@ var RULES = [
|
|
|
2202
2526
|
changedFiles.push(path);
|
|
2203
2527
|
if (!ctx.dryRun) writeText(path, expected);
|
|
2204
2528
|
}
|
|
2205
|
-
const planeJson =
|
|
2529
|
+
const planeJson = join10(ctx.repoRoot, ".plane.json");
|
|
2206
2530
|
if (existsSync7(planeJson)) {
|
|
2207
2531
|
const backup = `${planeJson}.migrated-backup`;
|
|
2208
2532
|
if (existsSync7(backup)) {
|
|
@@ -2227,14 +2551,15 @@ var RULES = [
|
|
|
2227
2551
|
title: ".env.op + gitignore secrets contract",
|
|
2228
2552
|
audit: (ctx) => {
|
|
2229
2553
|
const details = [];
|
|
2230
|
-
const envOp = safeReadText(
|
|
2231
|
-
const gitignore = safeReadText(
|
|
2554
|
+
const envOp = safeReadText(join10(ctx.repoRoot, ".env.op"));
|
|
2555
|
+
const gitignore = safeReadText(join10(ctx.repoRoot, ".gitignore"));
|
|
2232
2556
|
if (!envOp) {
|
|
2233
2557
|
details.push(".env.op missing");
|
|
2234
2558
|
} else {
|
|
2235
2559
|
const invalidLines = envOp.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).filter((line) => {
|
|
2236
2560
|
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
2237
|
-
|
|
2561
|
+
const quotedLiteral = /^"[^"\r\n]*"$/.test(value) || /^'[^'\r\n]*'$/.test(value);
|
|
2562
|
+
return !value.startsWith("op://") && !/^https?:\/\//.test(value) && !/^[A-Za-z0-9_.:-]+$/.test(value) && !quotedLiteral;
|
|
2238
2563
|
});
|
|
2239
2564
|
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals: ${invalidLines.join(", ")}`);
|
|
2240
2565
|
}
|
|
@@ -2253,12 +2578,12 @@ var RULES = [
|
|
|
2253
2578
|
migrate: (ctx, finding) => {
|
|
2254
2579
|
const changedFiles = [];
|
|
2255
2580
|
const details = [];
|
|
2256
|
-
const envOpPath =
|
|
2581
|
+
const envOpPath = join10(ctx.repoRoot, ".env.op");
|
|
2257
2582
|
if (!existsSync7(envOpPath)) {
|
|
2258
2583
|
changedFiles.push(envOpPath);
|
|
2259
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2584
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join10(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2260
2585
|
}
|
|
2261
|
-
const gitignorePath =
|
|
2586
|
+
const gitignorePath = join10(ctx.repoRoot, ".gitignore");
|
|
2262
2587
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
2263
2588
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
2264
2589
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2285,7 +2610,7 @@ var RULES = [
|
|
|
2285
2610
|
title: ".copier-answers.yml provenance + drift report",
|
|
2286
2611
|
audit: (ctx) => {
|
|
2287
2612
|
const details = [];
|
|
2288
|
-
const path =
|
|
2613
|
+
const path = join10(ctx.repoRoot, ".copier-answers.yml");
|
|
2289
2614
|
const text3 = safeReadText(path);
|
|
2290
2615
|
const project = readProjectJson(ctx);
|
|
2291
2616
|
if (!text3) {
|
|
@@ -2316,12 +2641,12 @@ var RULES = [
|
|
|
2316
2641
|
const changedFiles = [];
|
|
2317
2642
|
const project = canonicalProjectJson(ctx);
|
|
2318
2643
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2319
|
-
_src_path: ${
|
|
2644
|
+
_src_path: ${join10(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2320
2645
|
project_description: ${String(project.project_description)}
|
|
2321
2646
|
project_name: ${String(project.project_name)}
|
|
2322
2647
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2323
2648
|
`;
|
|
2324
|
-
const path =
|
|
2649
|
+
const path = join10(ctx.repoRoot, ".copier-answers.yml");
|
|
2325
2650
|
if (safeReadText(path) !== text3) {
|
|
2326
2651
|
changedFiles.push(path);
|
|
2327
2652
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -2340,15 +2665,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2340
2665
|
id: "bmad.scaffold",
|
|
2341
2666
|
title: "BMAD modules/docs scaffold",
|
|
2342
2667
|
audit: (ctx) => {
|
|
2343
|
-
const sourceRoot =
|
|
2344
|
-
const targetRoot =
|
|
2668
|
+
const sourceRoot = join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2669
|
+
const targetRoot = join10(ctx.repoRoot, "_bmad");
|
|
2345
2670
|
const sentinels = [
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2671
|
+
join10("core", "config.yaml"),
|
|
2672
|
+
join10("custom", "config.yaml"),
|
|
2673
|
+
join10("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
2674
|
+
join10("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
2350
2675
|
];
|
|
2351
|
-
const missing = sentinels.filter((file) => existsSync7(
|
|
2676
|
+
const missing = sentinels.filter((file) => existsSync7(join10(sourceRoot, file)) && !existsSync7(join10(targetRoot, file)));
|
|
2352
2677
|
return {
|
|
2353
2678
|
id: "bmad.scaffold",
|
|
2354
2679
|
title: "BMAD modules/docs scaffold",
|
|
@@ -2360,7 +2685,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2360
2685
|
},
|
|
2361
2686
|
migrate: (ctx, finding) => {
|
|
2362
2687
|
const changedFiles = [];
|
|
2363
|
-
copyMissingRecursive(
|
|
2688
|
+
copyMissingRecursive(join10(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join10(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2364
2689
|
return {
|
|
2365
2690
|
id: finding.id,
|
|
2366
2691
|
title: finding.title,
|
|
@@ -2382,11 +2707,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2382
2707
|
}
|
|
2383
2708
|
const details = [];
|
|
2384
2709
|
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", "runtime/bloodbank-consumer.py"]) {
|
|
2385
|
-
if (!existsSync7(
|
|
2710
|
+
if (!existsSync7(join10(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join10(role.roleDir, rel))}`);
|
|
2386
2711
|
}
|
|
2387
|
-
const gitmodules = safeReadText(
|
|
2712
|
+
const gitmodules = safeReadText(join10(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2388
2713
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2389
|
-
if (!profileMetaInheritsDefault(
|
|
2714
|
+
if (!profileMetaInheritsDefault(join10(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2390
2715
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2391
2716
|
}
|
|
2392
2717
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -2407,21 +2732,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2407
2732
|
if (!role) {
|
|
2408
2733
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2409
2734
|
}
|
|
2410
|
-
const templateRoleDir =
|
|
2411
|
-
writeIfDifferent(
|
|
2412
|
-
writeIfDifferent(
|
|
2413
|
-
writeIfDifferent(
|
|
2414
|
-
copyMissingRecursive(
|
|
2415
|
-
copyMissingRecursive(
|
|
2416
|
-
copyMissingRecursive(
|
|
2417
|
-
const promptSource =
|
|
2418
|
-
const promptTarget =
|
|
2735
|
+
const templateRoleDir = join10(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
2736
|
+
writeIfDifferent(join10(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
2737
|
+
writeIfDifferent(join10(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
2738
|
+
writeIfDifferent(join10(role.roleDir, ".gitignore"), readText(join10(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
2739
|
+
copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
2740
|
+
copyMissingRecursive(join10(templateRoleDir, ".runtime-scaffold"), join10(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
2741
|
+
copyMissingRecursive(join10(templateRoleDir, ".scripts"), join10(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
2742
|
+
const promptSource = join10(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
2743
|
+
const promptTarget = join10(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
2419
2744
|
if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
|
|
2420
2745
|
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);
|
|
2421
2746
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2422
2747
|
}
|
|
2423
2748
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2424
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
2749
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join10(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2425
2750
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2426
2751
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2427
2752
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -2475,9 +2800,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2475
2800
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2476
2801
|
}
|
|
2477
2802
|
for (const role of roles) {
|
|
2478
|
-
const sysDir =
|
|
2803
|
+
const sysDir = join10(ctx.homeDir, ".config", "systemd", "user");
|
|
2479
2804
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
2480
|
-
const allUnitsPresent = units.every((unit) => existsSync7(
|
|
2805
|
+
const allUnitsPresent = units.every((unit) => existsSync7(join10(sysDir, unit)));
|
|
2481
2806
|
if (allUnitsPresent) {
|
|
2482
2807
|
if (ctx.dryRun) {
|
|
2483
2808
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -2489,7 +2814,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2489
2814
|
}
|
|
2490
2815
|
continue;
|
|
2491
2816
|
}
|
|
2492
|
-
for (const script of [
|
|
2817
|
+
for (const script of [join10(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
2493
2818
|
if (!script || !existsSync7(script)) continue;
|
|
2494
2819
|
if (ctx.dryRun) {
|
|
2495
2820
|
details.push(`would run: bash ${script}`);
|
|
@@ -2517,7 +2842,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
2517
2842
|
changedFiles.push(path);
|
|
2518
2843
|
if (!dryRun) {
|
|
2519
2844
|
writeText(path, normalized);
|
|
2520
|
-
if (mode)
|
|
2845
|
+
if (mode) chmodSync2(path, mode);
|
|
2521
2846
|
}
|
|
2522
2847
|
}
|
|
2523
2848
|
function getParityRuleIds() {
|
|
@@ -2539,7 +2864,7 @@ function runAudit(repoArg) {
|
|
|
2539
2864
|
rules
|
|
2540
2865
|
};
|
|
2541
2866
|
}
|
|
2542
|
-
function
|
|
2867
|
+
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
2543
2868
|
const pjanglerRoot = resolvePjanglerRoot();
|
|
2544
2869
|
const ctx = {
|
|
2545
2870
|
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
@@ -2547,9 +2872,9 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2547
2872
|
pjanglerRoot,
|
|
2548
2873
|
homeDir: homedir4()
|
|
2549
2874
|
};
|
|
2550
|
-
const selected =
|
|
2875
|
+
const selected = RULES.filter((rule) => ruleIds.includes(rule.id));
|
|
2551
2876
|
if (!selected.length) {
|
|
2552
|
-
throw new Error(`Unknown parity
|
|
2877
|
+
throw new Error(`Unknown parity rules: ${ruleIds.join(", ")}`);
|
|
2553
2878
|
}
|
|
2554
2879
|
const results = selected.map((rule) => {
|
|
2555
2880
|
try {
|
|
@@ -2575,14 +2900,391 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2575
2900
|
changedFiles
|
|
2576
2901
|
};
|
|
2577
2902
|
}
|
|
2903
|
+
function runMigration(selector, repoArg, dryRun, all) {
|
|
2904
|
+
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
2905
|
+
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
2906
|
+
}
|
|
2907
|
+
function prettyTimestamp(iso) {
|
|
2908
|
+
const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/.exec(iso);
|
|
2909
|
+
return match ? `${match[1]} ${match[2]} UTC` : iso;
|
|
2910
|
+
}
|
|
2578
2911
|
function formatAuditReport(report) {
|
|
2579
|
-
const
|
|
2912
|
+
const counts = {};
|
|
2913
|
+
for (const rule of report.rules) counts[rule.status] = (counts[rule.status] ?? 0) + 1;
|
|
2914
|
+
const idWidth = report.rules.reduce((width, rule) => Math.max(width, rule.id.length), 0);
|
|
2915
|
+
const tally = [];
|
|
2916
|
+
if (counts.pass) tally.push(green(`${counts.pass} passed`));
|
|
2917
|
+
if (counts.fail) tally.push(red(`${counts.fail} failed`));
|
|
2918
|
+
if (counts.warn) tally.push(yellow(`${counts.warn} warning${counts.warn === 1 ? "" : "s"}`));
|
|
2919
|
+
if (counts.skip) tally.push(gray(`${counts.skip} skipped`));
|
|
2920
|
+
const overall = report.ok ? `${green(glyph.pass)} ${bold("Parity audit passed")}` : `${red(glyph.fail)} ${bold("Parity audit failed")}`;
|
|
2921
|
+
const lines = [""];
|
|
2922
|
+
lines.push(` ${overall}${tally.length ? ` ${dim(glyph.dot)} ${joinDot(tally)}` : ""}`);
|
|
2923
|
+
lines.push(` ${dim(report.repo)} ${dim(glyph.dot)} ${dim(prettyTimestamp(report.auditedAt))}`);
|
|
2924
|
+
lines.push("");
|
|
2580
2925
|
for (const rule of report.rules) {
|
|
2581
|
-
|
|
2582
|
-
|
|
2926
|
+
const style = statusStyle(rule.status);
|
|
2927
|
+
lines.push(` ${style.color(style.glyph)} ${style.color(rule.id.padEnd(idWidth))} ${rule.summary}`);
|
|
2928
|
+
for (const detail of rule.details) lines.push(` ${dim(glyph.arrow)} ${dim(detail)}`);
|
|
2929
|
+
}
|
|
2930
|
+
lines.push("");
|
|
2931
|
+
return lines.join("\n");
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
// src/project/index.ts
|
|
2935
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2936
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync5, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2937
|
+
import { homedir as homedir5 } from "node:os";
|
|
2938
|
+
import { basename as basename3, dirname as dirname7, join as join11, resolve as resolve2 } from "node:path";
|
|
2939
|
+
import YAML from "yaml";
|
|
2940
|
+
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
2941
|
+
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
2942
|
+
var KNOWN_SKILL_ROOTS = [
|
|
2943
|
+
"/home/delorenj/code/skillex/all-skills",
|
|
2944
|
+
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
2945
|
+
"/home/delorenj/code/pjangler/.agents/skills",
|
|
2946
|
+
join11(homedir5(), ".codex", "skills")
|
|
2947
|
+
];
|
|
2948
|
+
function projectRegistryPath(env2 = process.env) {
|
|
2949
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir5(), ".config", "pjangler", "projects.yaml"));
|
|
2950
|
+
}
|
|
2951
|
+
function emptyProjectRegistry() {
|
|
2952
|
+
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
2953
|
+
}
|
|
2954
|
+
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
2955
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
2956
|
+
const raw = YAML.parse(readFileSync5(path, "utf8"));
|
|
2957
|
+
if (raw == null) return emptyProjectRegistry();
|
|
2958
|
+
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
2959
|
+
const registry = raw;
|
|
2960
|
+
const normalized = {
|
|
2961
|
+
schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
|
|
2962
|
+
projects: isRecord(registry.projects) ? registry.projects : {}
|
|
2963
|
+
};
|
|
2964
|
+
validateProjectRegistry(normalized);
|
|
2965
|
+
return normalized;
|
|
2966
|
+
}
|
|
2967
|
+
function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
2968
|
+
validateProjectRegistry(registry);
|
|
2969
|
+
mkdirSync6(dirname7(path), { recursive: true });
|
|
2970
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
2971
|
+
writeFileSync5(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
2972
|
+
renameSync2(temp, path);
|
|
2973
|
+
}
|
|
2974
|
+
function validateProjectRegistry(registry) {
|
|
2975
|
+
if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
|
|
2976
|
+
throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
|
|
2977
|
+
}
|
|
2978
|
+
if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
|
|
2979
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2980
|
+
const repoPaths = /* @__PURE__ */ new Map();
|
|
2981
|
+
const identifiers = /* @__PURE__ */ new Map();
|
|
2982
|
+
for (const [slug, project] of Object.entries(registry.projects)) {
|
|
2983
|
+
validateProjectRecord(project, slug);
|
|
2984
|
+
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
2985
|
+
slugs.add(project.slug);
|
|
2986
|
+
const repoKey = resolve2(project.repo_path);
|
|
2987
|
+
const existingRepoSlug = repoPaths.get(repoKey);
|
|
2988
|
+
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
2989
|
+
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
2990
|
+
}
|
|
2991
|
+
repoPaths.set(repoKey, slug);
|
|
2992
|
+
const identifier = project.ticket_provider.identifier?.toUpperCase();
|
|
2993
|
+
if (identifier) {
|
|
2994
|
+
const existingIdentifierSlug = identifiers.get(identifier);
|
|
2995
|
+
if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
|
|
2996
|
+
throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
|
|
2997
|
+
}
|
|
2998
|
+
identifiers.set(identifier, slug);
|
|
2999
|
+
}
|
|
2583
3000
|
}
|
|
2584
|
-
|
|
3001
|
+
}
|
|
3002
|
+
function slugifyProjectName(value) {
|
|
3003
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
3004
|
+
}
|
|
3005
|
+
function deriveProjectIdentifier(value) {
|
|
3006
|
+
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
3007
|
+
const identifier = compact.slice(0, 4) || "PROJ";
|
|
3008
|
+
return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
|
|
3009
|
+
}
|
|
3010
|
+
function normalizeAgentRole(value) {
|
|
3011
|
+
return value?.trim() || "pm";
|
|
3012
|
+
}
|
|
3013
|
+
function jsonStable(value) {
|
|
3014
|
+
return JSON.stringify(value);
|
|
3015
|
+
}
|
|
3016
|
+
function projectRecordEquivalent(a, b) {
|
|
3017
|
+
if (!a) return false;
|
|
3018
|
+
const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
|
|
3019
|
+
const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
|
|
3020
|
+
return jsonStable(aComparable) === jsonStable(bComparable);
|
|
3021
|
+
}
|
|
3022
|
+
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
3023
|
+
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
3024
|
+
return resolve2(dirname7(resolve2(cwd)), compactName);
|
|
3025
|
+
}
|
|
3026
|
+
function resolveSourceSkillPath(sourceSkill) {
|
|
3027
|
+
if (!sourceSkill) return void 0;
|
|
3028
|
+
const expanded = expandHome(sourceSkill);
|
|
3029
|
+
const direct = resolve2(expanded);
|
|
3030
|
+
if (existsSync8(direct)) return direct;
|
|
3031
|
+
const name = basename3(sourceSkill);
|
|
3032
|
+
for (const root of KNOWN_SKILL_ROOTS) {
|
|
3033
|
+
const candidate = join11(root, name);
|
|
3034
|
+
if (existsSync8(candidate)) return candidate;
|
|
3035
|
+
}
|
|
3036
|
+
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
3037
|
+
const hint = existsSync8(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
|
|
3038
|
+
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
3039
|
+
}
|
|
3040
|
+
function planProjectInit(input) {
|
|
3041
|
+
if (!input.name.trim()) throw new Error("Project name is required");
|
|
3042
|
+
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
3043
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
3044
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
3045
|
+
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
3046
|
+
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
3047
|
+
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
3048
|
+
const existing = registry.projects[slug];
|
|
3049
|
+
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
3050
|
+
const overwrite = input.overwrite ?? input.force ?? false;
|
|
3051
|
+
const agentRole = normalizeAgentRole(input.agentRole);
|
|
3052
|
+
const agents = input.provisionAgent ? {
|
|
3053
|
+
...existing?.agents ?? {},
|
|
3054
|
+
[agentRole]: {
|
|
3055
|
+
role: agentRole,
|
|
3056
|
+
provisioning_state: "planned"
|
|
3057
|
+
}
|
|
3058
|
+
} : existing?.agents ?? {};
|
|
3059
|
+
const scaffold = input.scaffold ?? true;
|
|
3060
|
+
const candidateProject = {
|
|
3061
|
+
name: input.name,
|
|
3062
|
+
slug,
|
|
3063
|
+
repo_path: targetDir,
|
|
3064
|
+
description: input.description ?? "",
|
|
3065
|
+
status: "planned",
|
|
3066
|
+
source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
|
|
3067
|
+
template: {
|
|
3068
|
+
commonproject: {
|
|
3069
|
+
enabled: true,
|
|
3070
|
+
primary_language: input.primaryLanguage ?? "python"
|
|
3071
|
+
}
|
|
3072
|
+
},
|
|
3073
|
+
ticket_provider: {
|
|
3074
|
+
type: input.ticketProvider ?? "plane",
|
|
3075
|
+
workspace: input.planeWorkspace ?? "33god",
|
|
3076
|
+
identifier,
|
|
3077
|
+
board_id: input.planeProjectId ?? "",
|
|
3078
|
+
board_url: input.planeProjectId ? `https://plane.delo.sh/${input.planeWorkspace ?? "33god"}/projects/${input.planeProjectId}/issues/` : "",
|
|
3079
|
+
state: input.live ? "planned" : "planned"
|
|
3080
|
+
},
|
|
3081
|
+
agents,
|
|
3082
|
+
created_at: existing?.created_at ?? now,
|
|
3083
|
+
updated_at: now
|
|
3084
|
+
};
|
|
3085
|
+
const project = {
|
|
3086
|
+
...candidateProject,
|
|
3087
|
+
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
3088
|
+
};
|
|
3089
|
+
validateNoDuplicateProject(registry, project, overwrite);
|
|
3090
|
+
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot2());
|
|
3091
|
+
const manifest = projectManifestFromRegistryProject(project);
|
|
3092
|
+
const apply = input.apply ?? false;
|
|
3093
|
+
const live = input.live ?? false;
|
|
3094
|
+
const actions = [
|
|
3095
|
+
{ kind: "registry.upsert", registryPath: registryPath2, slug, project }
|
|
3096
|
+
];
|
|
3097
|
+
if (scaffold) {
|
|
3098
|
+
actions.push(buildCommonProjectCopierAction({
|
|
3099
|
+
pjanglerRoot,
|
|
3100
|
+
targetDir,
|
|
3101
|
+
projectName: project.name,
|
|
3102
|
+
projectDescription: project.description,
|
|
3103
|
+
projectSlug: project.slug,
|
|
3104
|
+
ticketProvider: project.ticket_provider.type,
|
|
3105
|
+
planeWorkspace: project.ticket_provider.workspace ?? "33god",
|
|
3106
|
+
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
3107
|
+
projectIdentifier: identifier,
|
|
3108
|
+
primaryLanguage: project.template.commonproject.primary_language,
|
|
3109
|
+
overwrite
|
|
3110
|
+
}));
|
|
3111
|
+
}
|
|
3112
|
+
actions.push(
|
|
3113
|
+
{ kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
|
|
3114
|
+
{
|
|
3115
|
+
kind: "plane.create-or-link",
|
|
3116
|
+
enabled: live,
|
|
3117
|
+
live,
|
|
3118
|
+
workspace: project.ticket_provider.workspace ?? "33god",
|
|
3119
|
+
identifier,
|
|
3120
|
+
state: live ? "planned" : "planned",
|
|
3121
|
+
reason: live ? void 0 : "network/cloud actions require --live"
|
|
3122
|
+
},
|
|
3123
|
+
{
|
|
3124
|
+
kind: "hermes.provision-agent",
|
|
3125
|
+
enabled: input.provisionAgent ?? false,
|
|
3126
|
+
local: !live,
|
|
3127
|
+
targetDir,
|
|
3128
|
+
targetRepo: slug,
|
|
3129
|
+
role: agentRole,
|
|
3130
|
+
context: {
|
|
3131
|
+
skipRuntimeRepo: !live,
|
|
3132
|
+
skipPlane: !live,
|
|
3133
|
+
skipBloodbank: !live,
|
|
3134
|
+
skipSystemd: !live || process.platform === "darwin"
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
);
|
|
3138
|
+
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
3139
|
+
}
|
|
3140
|
+
function executeProjectInitPlan(plan) {
|
|
3141
|
+
const logs = [];
|
|
3142
|
+
const errors = [];
|
|
3143
|
+
const changedFiles = [];
|
|
3144
|
+
if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
|
|
3145
|
+
const registry = loadProjectRegistry(plan.registryPath);
|
|
3146
|
+
let pendingRegistryAction;
|
|
3147
|
+
for (const action of plan.actions) {
|
|
3148
|
+
if (action.kind === "copier.copy.commonproject") {
|
|
3149
|
+
mkdirSync6(dirname7(action.targetDir), { recursive: true });
|
|
3150
|
+
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
3151
|
+
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
3152
|
+
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
3153
|
+
if (result.error) {
|
|
3154
|
+
const code = result.error.code;
|
|
3155
|
+
errors.push(
|
|
3156
|
+
code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
|
|
3157
|
+
);
|
|
3158
|
+
break;
|
|
3159
|
+
}
|
|
3160
|
+
if (result.status !== 0) {
|
|
3161
|
+
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
3162
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
3163
|
+
break;
|
|
3164
|
+
}
|
|
3165
|
+
changedFiles.push(action.targetDir);
|
|
3166
|
+
} else if (action.kind === "project.write-manifest") {
|
|
3167
|
+
mkdirSync6(dirname7(action.path), { recursive: true });
|
|
3168
|
+
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
2585
3169
|
`;
|
|
3170
|
+
const current = existsSync8(action.path) ? readFileSync5(action.path, "utf8") : void 0;
|
|
3171
|
+
if (current !== next) {
|
|
3172
|
+
writeFileSync5(action.path, next, "utf8");
|
|
3173
|
+
changedFiles.push(action.path);
|
|
3174
|
+
}
|
|
3175
|
+
} else if (action.kind === "registry.upsert") {
|
|
3176
|
+
pendingRegistryAction = action;
|
|
3177
|
+
} else if (action.kind === "plane.create-or-link") {
|
|
3178
|
+
logs.push(action.enabled ? "plane.create-or-link requires a live provider integration" : "plane.create-or-link skipped (requires --live)");
|
|
3179
|
+
} else if (action.kind === "hermes.provision-agent") {
|
|
3180
|
+
logs.push(action.enabled ? "hermes.provision-agent planned for the caller to execute" : "hermes.provision-agent skipped");
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
if (pendingRegistryAction && errors.length === 0) {
|
|
3184
|
+
if (!projectRecordEquivalent(registry.projects[pendingRegistryAction.slug], pendingRegistryAction.project)) {
|
|
3185
|
+
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
3186
|
+
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
3187
|
+
changedFiles.push(pendingRegistryAction.registryPath);
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
3191
|
+
}
|
|
3192
|
+
function projectManifestFromRegistryProject(project) {
|
|
3193
|
+
const agents = Object.fromEntries(
|
|
3194
|
+
Object.entries(project.agents).map(([name, agent]) => [
|
|
3195
|
+
`${project.slug}-${name}`,
|
|
3196
|
+
{
|
|
3197
|
+
role: agent.role,
|
|
3198
|
+
role_dir: agent.role_dir,
|
|
3199
|
+
provisioning_state: agent.provisioning_state
|
|
3200
|
+
}
|
|
3201
|
+
])
|
|
3202
|
+
);
|
|
3203
|
+
return {
|
|
3204
|
+
project_name: project.name,
|
|
3205
|
+
project_description: project.description,
|
|
3206
|
+
project_slug: project.slug,
|
|
3207
|
+
repo_path: project.repo_path,
|
|
3208
|
+
ticket_provider: {
|
|
3209
|
+
type: project.ticket_provider.type,
|
|
3210
|
+
workspace: project.ticket_provider.workspace ?? "",
|
|
3211
|
+
identifier: project.ticket_provider.identifier ?? "",
|
|
3212
|
+
board_id: project.ticket_provider.board_id ?? "",
|
|
3213
|
+
board_url: project.ticket_provider.board_url ?? "",
|
|
3214
|
+
state: project.ticket_provider.state
|
|
3215
|
+
},
|
|
3216
|
+
agents
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
function getProject(registry, slug) {
|
|
3220
|
+
const project = registry.projects[slug];
|
|
3221
|
+
if (!project) throw new Error(`Project not found in registry: ${slug}`);
|
|
3222
|
+
return project;
|
|
3223
|
+
}
|
|
3224
|
+
function buildCommonProjectCopierAction(input) {
|
|
3225
|
+
const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
|
|
3226
|
+
const data = {
|
|
3227
|
+
project_name: input.projectName,
|
|
3228
|
+
project_description: input.projectDescription ?? "",
|
|
3229
|
+
project_slug: input.projectSlug,
|
|
3230
|
+
ticket_provider: input.ticketProvider,
|
|
3231
|
+
plane_workspace: input.planeWorkspace,
|
|
3232
|
+
plane_project_id: input.planeProjectId ?? "",
|
|
3233
|
+
project_identifier: input.projectIdentifier,
|
|
3234
|
+
primary_language: input.primaryLanguage
|
|
3235
|
+
};
|
|
3236
|
+
const command = ["copier", "copy", "--trust", templateDir, input.targetDir, "--defaults"];
|
|
3237
|
+
for (const [key, value] of Object.entries(data)) command.push("--data", `${key}=${value}`);
|
|
3238
|
+
if (input.overwrite) command.push("--overwrite");
|
|
3239
|
+
return {
|
|
3240
|
+
kind: "copier.copy.commonproject",
|
|
3241
|
+
cwd: input.pjanglerRoot,
|
|
3242
|
+
command,
|
|
3243
|
+
targetDir: input.targetDir,
|
|
3244
|
+
data,
|
|
3245
|
+
overwrite: input.overwrite
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
function resolvePjanglerRoot2() {
|
|
3249
|
+
let dir = dirname7(new URL(import.meta.url).pathname);
|
|
3250
|
+
while (dir !== dirname7(dir)) {
|
|
3251
|
+
if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
3252
|
+
dir = dirname7(dir);
|
|
3253
|
+
}
|
|
3254
|
+
return resolve2(process.cwd());
|
|
3255
|
+
}
|
|
3256
|
+
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
3257
|
+
const existingSameSlug = registry.projects[project.slug];
|
|
3258
|
+
if (existingSameSlug && !overwrite && resolve2(existingSameSlug.repo_path) !== resolve2(project.repo_path)) {
|
|
3259
|
+
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
3260
|
+
}
|
|
3261
|
+
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
3262
|
+
if (slug === project.slug) continue;
|
|
3263
|
+
if (resolve2(existing.repo_path) === resolve2(project.repo_path)) {
|
|
3264
|
+
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
3265
|
+
}
|
|
3266
|
+
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
3267
|
+
throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
function validateProjectRecord(project, key) {
|
|
3272
|
+
if (!isRecord(project)) throw new Error(`Project ${key} must be a mapping`);
|
|
3273
|
+
if (!project.name) throw new Error(`Project ${key} missing name`);
|
|
3274
|
+
if (!project.slug) throw new Error(`Project ${key} missing slug`);
|
|
3275
|
+
if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
|
|
3276
|
+
if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
|
|
3277
|
+
if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
|
|
3278
|
+
if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
|
|
3279
|
+
if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
|
|
3280
|
+
}
|
|
3281
|
+
function expandHome(path) {
|
|
3282
|
+
if (path === "~") return homedir5();
|
|
3283
|
+
if (path.startsWith("~/")) return join11(homedir5(), path.slice(2));
|
|
3284
|
+
return path;
|
|
3285
|
+
}
|
|
3286
|
+
function isRecord(value) {
|
|
3287
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2586
3288
|
}
|
|
2587
3289
|
|
|
2588
3290
|
// src/mcp-server.ts
|
|
@@ -2592,22 +3294,22 @@ var server = new McpServer({
|
|
|
2592
3294
|
});
|
|
2593
3295
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "linear", "trello"]);
|
|
2594
3296
|
function resolveTargetDir(targetDir) {
|
|
2595
|
-
const dir =
|
|
2596
|
-
if (!
|
|
3297
|
+
const dir = resolve3(targetDir ?? process.cwd());
|
|
3298
|
+
if (!existsSync9(dir)) {
|
|
2597
3299
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
2598
3300
|
}
|
|
2599
|
-
if (!
|
|
3301
|
+
if (!statSync2(dir).isDirectory()) {
|
|
2600
3302
|
throw new Error(`Target path is not a directory: ${dir}`);
|
|
2601
3303
|
}
|
|
2602
3304
|
return dir;
|
|
2603
3305
|
}
|
|
2604
|
-
function
|
|
2605
|
-
let dir =
|
|
2606
|
-
while (dir !==
|
|
2607
|
-
if (
|
|
3306
|
+
function resolvePjanglerRoot3() {
|
|
3307
|
+
let dir = dirname8(fileURLToPath5(import.meta.url));
|
|
3308
|
+
while (dir !== dirname8(dir)) {
|
|
3309
|
+
if (existsSync9(join12(dir, "package.json")) && existsSync9(join12(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2608
3310
|
return dir;
|
|
2609
3311
|
}
|
|
2610
|
-
dir =
|
|
3312
|
+
dir = dirname8(dir);
|
|
2611
3313
|
}
|
|
2612
3314
|
throw new Error("Unable to resolve pjangler root");
|
|
2613
3315
|
}
|
|
@@ -2676,26 +3378,6 @@ async function runRecipeWithCapture(recipeName, context) {
|
|
|
2676
3378
|
console.error = origError;
|
|
2677
3379
|
}
|
|
2678
3380
|
}
|
|
2679
|
-
function buildCommonProjectCopierAction(input) {
|
|
2680
|
-
const data = {
|
|
2681
|
-
project_name: input.projectName,
|
|
2682
|
-
project_description: input.projectDescription ?? "",
|
|
2683
|
-
project_slug: input.projectSlug,
|
|
2684
|
-
ticket_provider: input.ticketProvider,
|
|
2685
|
-
plane_workspace: input.planeWorkspace,
|
|
2686
|
-
plane_project_id: input.planeProjectId ?? "",
|
|
2687
|
-
project_identifier: input.projectIdentifier,
|
|
2688
|
-
primary_language: input.primaryLanguage
|
|
2689
|
-
};
|
|
2690
|
-
const args = ["copy", "--trust", input.templateDir, input.targetDir, "--defaults"];
|
|
2691
|
-
for (const [key, value] of Object.entries(data)) args.push("--data", `${key}=${value}`);
|
|
2692
|
-
if (input.overwrite) args.push("--overwrite");
|
|
2693
|
-
return {
|
|
2694
|
-
kind: "copier.copy.commonproject",
|
|
2695
|
-
command: ["copier", ...args],
|
|
2696
|
-
data
|
|
2697
|
-
};
|
|
2698
|
-
}
|
|
2699
3381
|
server.registerTool(
|
|
2700
3382
|
"pjangler_list_capabilities",
|
|
2701
3383
|
{
|
|
@@ -2718,7 +3400,7 @@ server.registerTool(
|
|
|
2718
3400
|
parityRules: getParityRuleIds(),
|
|
2719
3401
|
recommendedWorkflows: {
|
|
2720
3402
|
existingProject: ["pjangler_audit_project", "pjangler_migrate_project"],
|
|
2721
|
-
new33godProject: ["pjangler_bootstrap_33god_project", "pjangler_audit_project"],
|
|
3403
|
+
new33godProject: ["pjangler_project_init", "pjangler_bootstrap_33god_project", "pjangler_audit_project"],
|
|
2722
3404
|
hermesAgentProvisioning: ["pjangler_deploy_hermes_agent", "pjangler_audit_project"]
|
|
2723
3405
|
},
|
|
2724
3406
|
skillSynergy: parityGuidance()
|
|
@@ -2812,16 +3494,19 @@ server.registerTool(
|
|
|
2812
3494
|
local: z.boolean().optional(),
|
|
2813
3495
|
force: z.boolean().optional(),
|
|
2814
3496
|
overwrite: z.boolean().optional(),
|
|
2815
|
-
dryRun: z.boolean().optional()
|
|
3497
|
+
dryRun: z.boolean().optional(),
|
|
3498
|
+
registryPath: z.string().optional(),
|
|
3499
|
+
sourceSkill: z.string().optional(),
|
|
3500
|
+
live: z.boolean().optional()
|
|
2816
3501
|
}
|
|
2817
3502
|
},
|
|
2818
3503
|
async (input) => {
|
|
2819
3504
|
try {
|
|
2820
|
-
const pjanglerRoot =
|
|
3505
|
+
const pjanglerRoot = resolvePjanglerRoot3();
|
|
2821
3506
|
const projectSlug = input.projectSlug ?? slugify(input.projectName);
|
|
2822
|
-
const parentDir =
|
|
2823
|
-
if (!
|
|
2824
|
-
const targetDir =
|
|
3507
|
+
const parentDir = resolve3(input.parentDir ?? process.cwd());
|
|
3508
|
+
if (!existsSync9(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
3509
|
+
const targetDir = resolve3(input.targetDir ?? join12(parentDir, projectSlug));
|
|
2825
3510
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
2826
3511
|
const dryRun = input.dryRun ?? true;
|
|
2827
3512
|
const local = input.local ?? true;
|
|
@@ -2830,57 +3515,31 @@ server.registerTool(
|
|
|
2830
3515
|
if (!skipPlane && !planeProjectId) {
|
|
2831
3516
|
throw new Error("planeProjectId is required when skipPlane=false; keep skipPlane=true for safe local bootstrap");
|
|
2832
3517
|
}
|
|
2833
|
-
if (
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
3518
|
+
if (!dryRun && existsSync9(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
3519
|
+
const plan = planProjectInit({
|
|
3520
|
+
name: input.projectName,
|
|
3521
|
+
description: input.projectDescription,
|
|
2837
3522
|
targetDir,
|
|
2838
|
-
projectName: input.projectName,
|
|
2839
|
-
projectDescription: input.projectDescription,
|
|
2840
3523
|
projectSlug,
|
|
3524
|
+
sourceSkill: input.sourceSkill,
|
|
3525
|
+
primaryLanguage: input.primaryLanguage ?? "python",
|
|
3526
|
+
provisionAgent: input.provisionAgent ?? false,
|
|
3527
|
+
agentRole: input.agentRole ?? "pm",
|
|
3528
|
+
apply: !dryRun,
|
|
3529
|
+
live: input.live ?? false,
|
|
3530
|
+
registryPath: input.registryPath,
|
|
3531
|
+
projectIdentifier: input.projectIdentifier ?? projectSlug.slice(0, 4).toUpperCase(),
|
|
2841
3532
|
ticketProvider: input.ticketProvider ?? "plane",
|
|
2842
3533
|
planeWorkspace: input.planeWorkspace ?? "33god",
|
|
2843
3534
|
planeProjectId,
|
|
2844
|
-
|
|
2845
|
-
primaryLanguage: input.primaryLanguage ?? "python",
|
|
3535
|
+
pjanglerRoot,
|
|
2846
3536
|
overwrite
|
|
2847
3537
|
});
|
|
2848
|
-
const actions = [
|
|
2849
|
-
{ kind: "ensure.parent", path: dirname7(targetDir) },
|
|
2850
|
-
copierAction
|
|
2851
|
-
];
|
|
2852
|
-
if (input.provisionAgent) {
|
|
2853
|
-
actions.push({
|
|
2854
|
-
kind: "pjangler.recipe.hermes-agent",
|
|
2855
|
-
targetDir,
|
|
2856
|
-
context: {
|
|
2857
|
-
targetRepo: projectSlug,
|
|
2858
|
-
role: input.agentRole ?? "pm",
|
|
2859
|
-
agentPurpose: input.agentPurpose ?? `Project manager for ${input.projectName}`,
|
|
2860
|
-
local,
|
|
2861
|
-
dryRun,
|
|
2862
|
-
force: overwrite,
|
|
2863
|
-
skipTelegram: true,
|
|
2864
|
-
skipEmail: true,
|
|
2865
|
-
skipRuntimeRepo: local,
|
|
2866
|
-
skipPlane: skipPlane || local,
|
|
2867
|
-
skipBloodbank: local,
|
|
2868
|
-
skipSystemd: local || process.platform === "darwin"
|
|
2869
|
-
}
|
|
2870
|
-
});
|
|
2871
|
-
}
|
|
2872
3538
|
if (dryRun) {
|
|
2873
|
-
return asText({
|
|
2874
|
-
}
|
|
2875
|
-
const which = spawnSync5("which", ["copier"], { encoding: "utf8" });
|
|
2876
|
-
if (which.status !== 0) throw new Error("copier not found on PATH. Install with: uv tool install copier or pip install copier");
|
|
2877
|
-
mkdirSync6(dirname7(targetDir), { recursive: true });
|
|
2878
|
-
const result = spawnSync5(copierAction.command[0], copierAction.command.slice(1), { encoding: "utf8", cwd: pjanglerRoot });
|
|
2879
|
-
const logs = [result.stdout.trim()].filter(Boolean);
|
|
2880
|
-
const errors = [result.stderr.trim()].filter(Boolean);
|
|
2881
|
-
if (result.status !== 0) {
|
|
2882
|
-
return asText({ ok: false, dryRun, targetDir, actions, logs, errors, exitCode: result.status });
|
|
3539
|
+
return asText({ ...plan, guidance: parityGuidance() });
|
|
2883
3540
|
}
|
|
3541
|
+
const result = executeProjectInitPlan(plan);
|
|
3542
|
+
if (!result.ok) return asText({ ...result, guidance: parityGuidance() });
|
|
2884
3543
|
let agentResult;
|
|
2885
3544
|
if (input.provisionAgent) {
|
|
2886
3545
|
const context = {
|
|
@@ -2901,7 +3560,82 @@ server.registerTool(
|
|
|
2901
3560
|
};
|
|
2902
3561
|
agentResult = await runRecipeWithCapture("hermes-agent", context);
|
|
2903
3562
|
}
|
|
2904
|
-
return asText({ ok: !agentResult || agentResult.success,
|
|
3563
|
+
return asText({ ...result, ok: result.ok && (!agentResult || agentResult.success), agentResult, guidance: parityGuidance() });
|
|
3564
|
+
} catch (err) {
|
|
3565
|
+
return { isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] };
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
);
|
|
3569
|
+
server.registerTool(
|
|
3570
|
+
"pjangler_project_init",
|
|
3571
|
+
{
|
|
3572
|
+
title: "Initialize a pjangler project",
|
|
3573
|
+
description: "Plan or apply a registry-backed CommonProject project init. Dry-run is the default; writes require apply=true and live actions require live=true.",
|
|
3574
|
+
inputSchema: {
|
|
3575
|
+
name: z.string(),
|
|
3576
|
+
description: z.string().optional(),
|
|
3577
|
+
targetDir: z.string().optional(),
|
|
3578
|
+
sourceSkill: z.string().optional(),
|
|
3579
|
+
primaryLanguage: z.string().optional(),
|
|
3580
|
+
provisionAgent: z.boolean().optional(),
|
|
3581
|
+
agentRole: z.string().optional(),
|
|
3582
|
+
apply: z.boolean().optional(),
|
|
3583
|
+
live: z.boolean().optional(),
|
|
3584
|
+
slug: z.string().optional(),
|
|
3585
|
+
identifier: z.string().optional(),
|
|
3586
|
+
registryPath: z.string().optional(),
|
|
3587
|
+
force: z.boolean().optional()
|
|
3588
|
+
}
|
|
3589
|
+
},
|
|
3590
|
+
async (input) => {
|
|
3591
|
+
try {
|
|
3592
|
+
const plan = planProjectInit({
|
|
3593
|
+
name: input.name,
|
|
3594
|
+
description: input.description,
|
|
3595
|
+
targetDir: input.targetDir,
|
|
3596
|
+
sourceSkill: input.sourceSkill,
|
|
3597
|
+
primaryLanguage: input.primaryLanguage,
|
|
3598
|
+
provisionAgent: input.provisionAgent ?? false,
|
|
3599
|
+
agentRole: input.agentRole,
|
|
3600
|
+
apply: input.apply ?? false,
|
|
3601
|
+
live: input.live ?? false,
|
|
3602
|
+
projectSlug: input.slug,
|
|
3603
|
+
projectIdentifier: input.identifier,
|
|
3604
|
+
registryPath: input.registryPath,
|
|
3605
|
+
force: input.force ?? false,
|
|
3606
|
+
overwrite: input.force ?? false
|
|
3607
|
+
});
|
|
3608
|
+
if (!input.apply) return asText(plan);
|
|
3609
|
+
return asText(executeProjectInitPlan(plan));
|
|
3610
|
+
} catch (err) {
|
|
3611
|
+
return { isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] };
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
);
|
|
3615
|
+
server.registerTool(
|
|
3616
|
+
"pjangler_project_list",
|
|
3617
|
+
{
|
|
3618
|
+
title: "List pjangler registry projects",
|
|
3619
|
+
description: "Return projects from the pjangler central registry.",
|
|
3620
|
+
inputSchema: {
|
|
3621
|
+
registryPath: z.string().optional()
|
|
3622
|
+
}
|
|
3623
|
+
},
|
|
3624
|
+
async ({ registryPath: registryPath2 }) => asText(loadProjectRegistry(registryPath2 ?? projectRegistryPath()))
|
|
3625
|
+
);
|
|
3626
|
+
server.registerTool(
|
|
3627
|
+
"pjangler_project_show",
|
|
3628
|
+
{
|
|
3629
|
+
title: "Show a pjangler registry project",
|
|
3630
|
+
description: "Return one project by slug from the pjangler central registry.",
|
|
3631
|
+
inputSchema: {
|
|
3632
|
+
slug: z.string(),
|
|
3633
|
+
registryPath: z.string().optional()
|
|
3634
|
+
}
|
|
3635
|
+
},
|
|
3636
|
+
async ({ slug, registryPath: registryPath2 }) => {
|
|
3637
|
+
try {
|
|
3638
|
+
return asText(getProject(loadProjectRegistry(registryPath2 ?? projectRegistryPath()), slug));
|
|
2905
3639
|
} catch (err) {
|
|
2906
3640
|
return { isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] };
|
|
2907
3641
|
}
|
|
@@ -3003,7 +3737,7 @@ server.registerTool(
|
|
|
3003
3737
|
targetDir: resolvedTarget,
|
|
3004
3738
|
yes: true,
|
|
3005
3739
|
local,
|
|
3006
|
-
targetRepo: input.targetRepo ??
|
|
3740
|
+
targetRepo: input.targetRepo ?? basename4(resolvedTarget),
|
|
3007
3741
|
role: input.role,
|
|
3008
3742
|
agentPurpose: input.agentPurpose,
|
|
3009
3743
|
soulTone: input.soulTone,
|