@delorenj/pjangler 1.2.19 → 1.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +642 -291
- package/dist/mcp-server.js +649 -298
- package/package.json +1 -1
- package/templates/commonproject/copier.yml +3 -4
- package/templates/commonproject/template/.agents/hooks/README.md +13 -26
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
- package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
- package/templates/commonproject/template/.agents/local.example.json +2 -8
- package/templates/commonproject/template/.agents/skills.json +6 -0
- package/templates/commonproject/template/mise.toml.jinja +9 -16
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +11 -0
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +21 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +34 -13
- package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
package/dist/mcp-server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/mcp-server.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { basename as basename4, dirname as dirname8, join as
|
|
4
|
+
import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
|
|
5
|
+
import { basename as basename4, dirname as dirname8, join as join15, resolve as resolve3 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -285,97 +285,284 @@ var AddMiseCodegraphScript = class extends Command {
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
const content = `#!/usr/bin/env bash
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
# Auto-generated by pjangler
|
|
289
|
+
|
|
290
|
+
REPO_ROOT="$(pwd)"
|
|
291
|
+
PROJECT_NAME="$(basename "$REPO_ROOT")"
|
|
292
|
+
CONTAINER_NAME="codegraph-mcp-$PROJECT_NAME"
|
|
293
|
+
CACHE_DIR="$REPO_ROOT/.codegraph"
|
|
294
|
+
|
|
295
|
+
# Deterministically generate a port based on the repository path
|
|
296
|
+
PORT=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}' | tr -d 'a-f' | cut -c1-4)
|
|
297
|
+
# Ensure port is > 1024
|
|
298
|
+
PORT=$(( (PORT % 60000) + 1025 ))
|
|
299
|
+
|
|
300
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
|
|
301
|
+
echo "\u{1F680} Starting CodeGraph MCP Server on port $PORT..."
|
|
302
|
+
|
|
303
|
+
# Ensure the container isn't lingering in a stopped state
|
|
304
|
+
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
305
|
+
|
|
306
|
+
# Run the true Colby McHenry CodeGraph Docker image
|
|
307
|
+
# which we built locally as colbymchenry-codegraph-mcp:latest
|
|
308
|
+
docker run -d \\
|
|
309
|
+
--name "$CONTAINER_NAME" \\
|
|
310
|
+
--restart unless-stopped \\
|
|
311
|
+
-p "$PORT:8045" \\
|
|
312
|
+
-v "$REPO_ROOT:/repo" \\
|
|
313
|
+
colbymchenry-codegraph-mcp:latest >/dev/null
|
|
314
|
+
|
|
315
|
+
echo "\u2705 CodeGraph MCP running in background. SSE URL: http://localhost:$PORT/sse"
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
# Run init inside the container to ensure the index is bootstrapped.
|
|
319
|
+
# We run this using the standard codegraph CLI inside the container
|
|
320
|
+
docker exec "$CONTAINER_NAME" codegraph init -i /repo >/dev/null 2>&1 || true
|
|
321
|
+
|
|
322
|
+
# Wire up the MCP server to local agents
|
|
323
|
+
WIRE_SCRIPT="$(dirname "$0")/codegraph-wire.sh"
|
|
324
|
+
if [ -x "$WIRE_SCRIPT" ]; then
|
|
325
|
+
"$WIRE_SCRIPT"
|
|
326
|
+
fi
|
|
327
|
+
`;
|
|
328
|
+
this.writeFile(filePath, content);
|
|
329
|
+
if (!this.context.dryRun) {
|
|
330
|
+
chmodSync(join2(this.context.targetDir, filePath), 493);
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
335
|
+
filePath
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/commands/AddMiseCodegraphWireScript.ts
|
|
341
|
+
import { chmodSync as chmodSync2 } from "fs";
|
|
342
|
+
import { join as join3 } from "path";
|
|
343
|
+
var AddMiseCodegraphWireScript = class extends Command {
|
|
344
|
+
async invoke() {
|
|
345
|
+
const filePath = ".mise/scripts/codegraph-wire.sh";
|
|
346
|
+
if (this.fileExists(filePath) && !this.context.force) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph-wire.sh already exists"),
|
|
350
|
+
filePath
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const content = `#!/usr/bin/env bash
|
|
354
|
+
# Mise enter hook: Auto-wire CodeGraph MCP Server to local agents.
|
|
355
|
+
# Detects Claude Code, Codex, Gemini, Kimi, and OpenCode and configures
|
|
356
|
+
# them to use the local SSE endpoint for the current project.
|
|
294
357
|
|
|
295
358
|
set -euo pipefail
|
|
296
359
|
|
|
297
360
|
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
298
|
-
PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
|
|
299
|
-
mkdir -p "$PROJECT_BIN_DIR"
|
|
300
361
|
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
|
|
362
|
+
# Compute the same port as the container script
|
|
363
|
+
PORT_HASH=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}')
|
|
364
|
+
PORT_DEC=$(printf "%d" "0x\${PORT_HASH:0:4}")
|
|
365
|
+
PORT=$(( 8045 + (PORT_DEC % 955) ))
|
|
306
366
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
367
|
+
SSE_URL="http://localhost:$PORT/sse"
|
|
368
|
+
|
|
369
|
+
inject_sse() {
|
|
370
|
+
local target="$1"
|
|
371
|
+
local agent="$2"
|
|
372
|
+
|
|
373
|
+
if [ ! -f "$target" ]; then
|
|
374
|
+
mkdir -p "$(dirname "$target")"
|
|
375
|
+
echo '{"mcpServers": {}}' > "$target"
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
# Ensure the file is valid JSON (fail gracefully if it's garbled)
|
|
379
|
+
if ! jq . "$target" >/dev/null 2>&1; then
|
|
380
|
+
echo "[mise] WARNING: $target is not valid JSON, skipping $agent wiring" >&2
|
|
381
|
+
return
|
|
312
382
|
fi
|
|
383
|
+
|
|
384
|
+
# Inject or update the codegraph server
|
|
385
|
+
jq --arg url "$SSE_URL" '.mcpServers.codegraph = {"type": "sse", "url": $url}' "$target" > "$target.tmp" && mv "$target.tmp" "$target"
|
|
386
|
+
echo "[mise] Wired CodeGraph SSE for $agent -> $target"
|
|
313
387
|
}
|
|
314
388
|
|
|
315
|
-
#
|
|
316
|
-
|
|
317
|
-
if command -v codegraph >/dev/null 2>&1; then
|
|
318
|
-
return 0
|
|
319
|
-
fi
|
|
389
|
+
# 1. Claude Code (Project-scoped)
|
|
390
|
+
inject_sse "$REPO_ROOT/.claude.json" "Claude Code"
|
|
320
391
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
324
|
-
return 0
|
|
325
|
-
fi
|
|
392
|
+
# 2. Codex (Global)
|
|
393
|
+
inject_sse "$HOME/.codex/mcp.json" "Codex"
|
|
326
394
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if [ -x "$d/codegraph" ]; then
|
|
330
|
-
export PATH="$d:$PATH"
|
|
331
|
-
return 0
|
|
332
|
-
fi
|
|
333
|
-
done
|
|
395
|
+
# 3. Gemini (Global)
|
|
396
|
+
inject_sse "$HOME/.gemini/config/mcp.json" "Gemini"
|
|
334
397
|
|
|
335
|
-
|
|
336
|
-
|
|
398
|
+
# 4. Kimi (Global)
|
|
399
|
+
inject_sse "$HOME/.kimi-code/mcp.json" "Kimi"
|
|
337
400
|
|
|
338
|
-
#
|
|
339
|
-
|
|
340
|
-
init_project() {
|
|
341
|
-
local err_file
|
|
342
|
-
err_file="$(mktemp)"
|
|
343
|
-
trap 'rm -f "$err_file"' RETURN
|
|
401
|
+
# 5. OpenCode (Global)
|
|
402
|
+
inject_sse "$HOME/.opencode/mcp.json" "OpenCode"
|
|
344
403
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
fi
|
|
404
|
+
# 6. Cursor (Global)
|
|
405
|
+
inject_sse "$HOME/.cursor/mcp.json" "Cursor"
|
|
348
406
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
ensure_codegraph
|
|
352
|
-
codegraph init -i "$REPO_ROOT"
|
|
353
|
-
return 0
|
|
354
|
-
fi
|
|
407
|
+
# 7. VSCode native MCP (Global)
|
|
408
|
+
inject_sse "$HOME/.vscode/mcp.json" "VSCode"
|
|
355
409
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
410
|
+
# 8. Cline (VSCode extension)
|
|
411
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" "Cline"
|
|
359
412
|
|
|
360
|
-
|
|
413
|
+
# 9. Roo (VSCode extension)
|
|
414
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json" "Roo Code"
|
|
361
415
|
`;
|
|
362
416
|
this.writeFile(filePath, content);
|
|
363
417
|
if (!this.context.dryRun) {
|
|
364
|
-
|
|
418
|
+
chmodSync2(join3(this.context.targetDir, filePath), 493);
|
|
365
419
|
}
|
|
366
420
|
return {
|
|
367
421
|
success: true,
|
|
368
|
-
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
422
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph-wire.sh" : "\u2705 Created .mise/scripts/codegraph-wire.sh"),
|
|
369
423
|
filePath
|
|
370
424
|
};
|
|
371
425
|
}
|
|
372
426
|
};
|
|
373
427
|
|
|
428
|
+
// src/commands/WireMiseOpInject.ts
|
|
429
|
+
import { join as join4 } from "node:path";
|
|
430
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2, chmodSync as chmodSync3 } from "node:fs";
|
|
431
|
+
var WireMiseOpInject = class _WireMiseOpInject extends Command {
|
|
432
|
+
static MARKER = "# pjangler:op-inject";
|
|
433
|
+
static CR = "{{config_root}}";
|
|
434
|
+
async invoke() {
|
|
435
|
+
const scriptPath = ".mise/scripts/inject-op-secrets.sh";
|
|
436
|
+
const fullScriptPath = join4(this.context.targetDir, scriptPath);
|
|
437
|
+
const scriptContent = `#!/usr/bin/env bash
|
|
438
|
+
# Resolve 1Password \`op://\` references in .env.op into .env.secrets, which mise
|
|
439
|
+
# loads via \`_.file\`.
|
|
440
|
+
#
|
|
441
|
+
# Runs from the mise enter hook: fail-open and non-interactive.
|
|
442
|
+
# No .env.op means no 1Password, so this exits before spending anything.
|
|
443
|
+
|
|
444
|
+
set -euo pipefail
|
|
445
|
+
|
|
446
|
+
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
447
|
+
SRC="$REPO_ROOT/.env.op"
|
|
448
|
+
OUT="$REPO_ROOT/.env.secrets"
|
|
449
|
+
|
|
450
|
+
TTL_HOURS="\${OP_INJECT_TTL_HOURS:-12}"
|
|
451
|
+
|
|
452
|
+
[[ -f "$SRC" ]] || exit 0
|
|
453
|
+
command -v op >/dev/null 2>&1 || exit 0
|
|
454
|
+
|
|
455
|
+
if [[ -f "$OUT" && "$SRC" -ot "$OUT" ]]; then
|
|
456
|
+
if [[ "$TTL_HOURS" != "0" ]] && [[ -z "$(find "$OUT" -mmin "+$((TTL_HOURS * 60))" 2>/dev/null)" ]]; then
|
|
457
|
+
exit 0
|
|
458
|
+
fi
|
|
459
|
+
fi
|
|
460
|
+
|
|
461
|
+
op account list >/dev/null 2>&1 || exit 0
|
|
462
|
+
|
|
463
|
+
tmp="$(mktemp "\${OUT}.XXXXXX")"
|
|
464
|
+
trap 'rm -f "$tmp"' EXIT
|
|
465
|
+
chmod 600 "$tmp"
|
|
466
|
+
|
|
467
|
+
if op inject -i "$SRC" -o "$tmp" --force >/dev/null 2>&1; then
|
|
468
|
+
mv "$tmp" "$OUT"
|
|
469
|
+
trap - EXIT
|
|
470
|
+
else
|
|
471
|
+
echo "mise: op inject from .env.op failed \u2014 $(basename "$OUT") unchanged." >&2
|
|
472
|
+
echo " Check 'op signin', or that .env.op's op:// items are readable." >&2
|
|
473
|
+
echo " To disable: rm .env.op" >&2
|
|
474
|
+
fi
|
|
475
|
+
`;
|
|
476
|
+
this.writeFile(scriptPath, scriptContent);
|
|
477
|
+
if (!this.context.dryRun) {
|
|
478
|
+
chmodSync3(fullScriptPath, 493);
|
|
479
|
+
}
|
|
480
|
+
const misePath = join4(this.context.targetDir, "mise.toml");
|
|
481
|
+
if (!existsSync2(misePath)) {
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj init mise` first, then re-run."
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
let content = readFileSync(misePath, "utf8");
|
|
488
|
+
if (content.includes(_WireMiseOpInject.MARKER)) {
|
|
489
|
+
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for op-inject") };
|
|
490
|
+
}
|
|
491
|
+
const cr = _WireMiseOpInject.CR;
|
|
492
|
+
const envRe = /\[env\]\s*\n([\s\S]*?(?=\n\[|$))/;
|
|
493
|
+
if (envRe.test(content)) {
|
|
494
|
+
content = content.replace(envRe, (m) => {
|
|
495
|
+
let block = m;
|
|
496
|
+
if (!block.includes("_.file")) {
|
|
497
|
+
block += `
|
|
498
|
+
_.file = ['.env', '.env.secrets']
|
|
499
|
+
`;
|
|
500
|
+
} else if (!block.includes(".env.secrets")) {
|
|
501
|
+
block = block.replace(/(_\.file\s*=\s*\[)([^\]]*?)(\])/, (m2, prefix, files, suffix) => {
|
|
502
|
+
const added = files.trim() ? `${files}, '.env.secrets'` : `'.env.secrets'`;
|
|
503
|
+
return `${prefix}${added}${suffix}`;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return block;
|
|
507
|
+
});
|
|
508
|
+
} else {
|
|
509
|
+
const envBlock = `[env]
|
|
510
|
+
_.file = ['.env', '.env.secrets']
|
|
511
|
+
|
|
512
|
+
`;
|
|
513
|
+
if (content.includes("[tools]")) {
|
|
514
|
+
content = content.replace(/(\[tools\][\s\S]*?(?=\n\[|$))/, `$1
|
|
515
|
+
${envBlock}`);
|
|
516
|
+
} else {
|
|
517
|
+
content = envBlock + content;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const enterAdds = ` '${cr}/.mise/scripts/inject-op-secrets.sh',`;
|
|
521
|
+
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
522
|
+
if (enterRe.test(content)) {
|
|
523
|
+
content = content.replace(enterRe, (_m, head, close) => {
|
|
524
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
525
|
+
return `${head}${sep}
|
|
526
|
+
${enterAdds}${close}`;
|
|
527
|
+
});
|
|
528
|
+
} else {
|
|
529
|
+
if (content.includes("[hooks]")) {
|
|
530
|
+
content = content.replace(/\[hooks\]/, `[hooks]
|
|
531
|
+
enter = [
|
|
532
|
+
${enterAdds}
|
|
533
|
+
]`);
|
|
534
|
+
} else {
|
|
535
|
+
content += `
|
|
536
|
+
[hooks]
|
|
537
|
+
enter = [
|
|
538
|
+
${enterAdds}
|
|
539
|
+
]
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const appended = [
|
|
544
|
+
"",
|
|
545
|
+
_WireMiseOpInject.MARKER,
|
|
546
|
+
"[tasks.secrets-inject]",
|
|
547
|
+
'description = "Re-resolve .env.op secrets from 1Password into .env.secrets"',
|
|
548
|
+
`run = "OP_INJECT_TTL_HOURS=0 .mise/scripts/inject-op-secrets.sh"`,
|
|
549
|
+
_WireMiseOpInject.MARKER + ":end",
|
|
550
|
+
""
|
|
551
|
+
].join("\n");
|
|
552
|
+
content = content.replace(/\n*$/, "\n") + appended;
|
|
553
|
+
if (!this.context.dryRun) writeFileSync2(misePath, content);
|
|
554
|
+
return {
|
|
555
|
+
success: true,
|
|
556
|
+
message: this.formatMessage("\u2705 Wired mise.toml for op-inject (_.file, [hooks] enter, tasks.secrets-inject)")
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
374
561
|
// src/recipes/MiseRecipe.ts
|
|
375
562
|
var MiseRecipe = class extends Recipe {
|
|
376
563
|
constructor(context) {
|
|
377
564
|
super(context);
|
|
378
|
-
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
|
|
565
|
+
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript).addIngredient(AddMiseCodegraphWireScript).addIngredient(WireMiseOpInject);
|
|
379
566
|
}
|
|
380
567
|
printNextSteps() {
|
|
381
568
|
console.log("\u{1F389} Mise subsystem initialized successfully!");
|
|
@@ -604,34 +791,34 @@ var NodeRecipe = class extends Recipe {
|
|
|
604
791
|
|
|
605
792
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
606
793
|
import { homedir, platform } from "node:os";
|
|
607
|
-
import { existsSync as
|
|
608
|
-
import { join as
|
|
794
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
795
|
+
import { join as join5, dirname as dirname2 } from "node:path";
|
|
609
796
|
function resolveTemplateConfigPath() {
|
|
610
797
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
611
798
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
612
799
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
613
|
-
const base = xdg && xdg.length ? xdg :
|
|
614
|
-
return
|
|
800
|
+
const base = xdg && xdg.length ? xdg : join5(homedir(), ".config");
|
|
801
|
+
return join5(base, "hermes-agent-template", "config.toml");
|
|
615
802
|
}
|
|
616
803
|
function detectHermesBin(home) {
|
|
617
804
|
const candidates = [
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
805
|
+
join5(home, "code", "hermes-agent", "venv", "bin", "hermes"),
|
|
806
|
+
join5(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
|
|
807
|
+
join5(home, ".local", "bin", "hermes")
|
|
621
808
|
];
|
|
622
809
|
for (const c of candidates) {
|
|
623
|
-
if (
|
|
810
|
+
if (existsSync3(c)) return c;
|
|
624
811
|
}
|
|
625
812
|
return candidates[0];
|
|
626
813
|
}
|
|
627
814
|
function renderHostConfig() {
|
|
628
815
|
const home = homedir();
|
|
629
816
|
const hermesBin = detectHermesBin(home);
|
|
630
|
-
const hermesRepo =
|
|
631
|
-
const scaffoldDir =
|
|
632
|
-
const skillsDir =
|
|
633
|
-
const pmExternalSkillGlobalDir =
|
|
634
|
-
const pmExternalSkillBmadDir =
|
|
817
|
+
const hermesRepo = join5(home, "code", "hermes-agent");
|
|
818
|
+
const scaffoldDir = join5(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
819
|
+
const skillsDir = join5(home, ".agents", "skills");
|
|
820
|
+
const pmExternalSkillGlobalDir = join5(home, "code", "skillex", "skill-sets", "global", ".system");
|
|
821
|
+
const pmExternalSkillBmadDir = join5(home, "code", "skillex", "packs", "bmad", "6.10.2");
|
|
635
822
|
return `# hermes-agent-template \u2014 host configuration
|
|
636
823
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
637
824
|
#
|
|
@@ -680,7 +867,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
680
867
|
const ctx = this.context;
|
|
681
868
|
const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
|
|
682
869
|
const path = resolveTemplateConfigPath();
|
|
683
|
-
const exists =
|
|
870
|
+
const exists = existsSync3(path);
|
|
684
871
|
if (exists && !force) {
|
|
685
872
|
console.log(`\u2713 Config present: ${path}`);
|
|
686
873
|
return { success: true, message: "" };
|
|
@@ -691,7 +878,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
691
878
|
}
|
|
692
879
|
try {
|
|
693
880
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
694
|
-
|
|
881
|
+
writeFileSync3(path, renderHostConfig());
|
|
695
882
|
} catch (err) {
|
|
696
883
|
const msg = err instanceof Error ? err.message : String(err);
|
|
697
884
|
return { success: false, message: `\u2717 Failed to write ${path}: ${msg}` };
|
|
@@ -703,8 +890,8 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
703
890
|
};
|
|
704
891
|
|
|
705
892
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
706
|
-
import { basename, join as
|
|
707
|
-
import { readFileSync } from "node:fs";
|
|
893
|
+
import { basename, join as join6 } from "node:path";
|
|
894
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
708
895
|
import * as p from "@clack/prompts";
|
|
709
896
|
|
|
710
897
|
// src/commands/hermes/types.ts
|
|
@@ -719,7 +906,7 @@ function deriveProfileName(repo, role) {
|
|
|
719
906
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
720
907
|
function detectTicketProvider(targetDir) {
|
|
721
908
|
try {
|
|
722
|
-
const t = JSON.parse(
|
|
909
|
+
const t = JSON.parse(readFileSync2(join6(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
723
910
|
return t === "plane" || t === "trello" ? t : void 0;
|
|
724
911
|
} catch {
|
|
725
912
|
return void 0;
|
|
@@ -777,8 +964,8 @@ var PromptForAgentConfig = class extends Command {
|
|
|
777
964
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
778
965
|
import { spawnSync } from "node:child_process";
|
|
779
966
|
import { homedir as homedir2 } from "node:os";
|
|
780
|
-
import { join as
|
|
781
|
-
import { existsSync as
|
|
967
|
+
import { join as join7, dirname as dirname3 } from "node:path";
|
|
968
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
782
969
|
import { fileURLToPath } from "node:url";
|
|
783
970
|
import * as p2 from "@clack/prompts";
|
|
784
971
|
function resolveVendoredTemplate(name) {
|
|
@@ -789,8 +976,8 @@ function resolveVendoredTemplate(name) {
|
|
|
789
976
|
return void 0;
|
|
790
977
|
}
|
|
791
978
|
for (let i = 0; i < 8; i++) {
|
|
792
|
-
const candidate =
|
|
793
|
-
if (
|
|
979
|
+
const candidate = join7(dir, "templates", name);
|
|
980
|
+
if (existsSync4(join7(candidate, "copier.yml"))) return candidate;
|
|
794
981
|
const parent = dirname3(dir);
|
|
795
982
|
if (parent === dir) break;
|
|
796
983
|
dir = parent;
|
|
@@ -809,7 +996,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
809
996
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
810
997
|
};
|
|
811
998
|
}
|
|
812
|
-
const roleDir =
|
|
999
|
+
const roleDir = join7(ctx.targetDir, "agents", "hermes", role);
|
|
813
1000
|
ctx.roleDir = roleDir;
|
|
814
1001
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
815
1002
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -819,7 +1006,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
819
1006
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
820
1007
|
};
|
|
821
1008
|
}
|
|
822
|
-
if (
|
|
1009
|
+
if (existsSync4(join7(roleDir, "role.yaml")) && !ctx.force) {
|
|
823
1010
|
if (ctx.yes) {
|
|
824
1011
|
ctx.force = true;
|
|
825
1012
|
} else {
|
|
@@ -846,9 +1033,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
846
1033
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
847
1034
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
848
1035
|
};
|
|
849
|
-
const LOCAL_TEMPLATE =
|
|
1036
|
+
const LOCAL_TEMPLATE = join7(homedir2(), "code", "hermes-agent-template");
|
|
850
1037
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
851
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (
|
|
1038
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join7(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
852
1039
|
const args = [
|
|
853
1040
|
"copy",
|
|
854
1041
|
templateSrc,
|
|
@@ -879,7 +1066,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
879
1066
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
880
1067
|
};
|
|
881
1068
|
}
|
|
882
|
-
mkdirSync3(
|
|
1069
|
+
mkdirSync3(join7(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
883
1070
|
const spinner4 = p2.spinner();
|
|
884
1071
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
885
1072
|
const result = spawnSync("copier", args, {
|
|
@@ -903,14 +1090,14 @@ var RunCopierTemplate = class extends Command {
|
|
|
903
1090
|
};
|
|
904
1091
|
|
|
905
1092
|
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
906
|
-
import { existsSync as
|
|
907
|
-
import { join as
|
|
1093
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4, readdirSync } from "fs";
|
|
1094
|
+
import { join as join8 } from "path";
|
|
908
1095
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
909
1096
|
var UntrackHermesRuntimes = class extends Command {
|
|
910
1097
|
async invoke() {
|
|
911
1098
|
const targetDir = this.context.targetDir;
|
|
912
|
-
const rolesDir =
|
|
913
|
-
if (!
|
|
1099
|
+
const rolesDir = join8(targetDir, "agents", "hermes");
|
|
1100
|
+
if (!existsSync5(rolesDir)) {
|
|
914
1101
|
return {
|
|
915
1102
|
success: true,
|
|
916
1103
|
message: "No Hermes agents found (no agents/hermes directory)."
|
|
@@ -926,9 +1113,9 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
926
1113
|
let modifiedAny = false;
|
|
927
1114
|
const details = [];
|
|
928
1115
|
for (const role of roles) {
|
|
929
|
-
const roleDir =
|
|
930
|
-
const runtimePath =
|
|
931
|
-
const gitignorePath =
|
|
1116
|
+
const roleDir = join8("agents", "hermes", role);
|
|
1117
|
+
const runtimePath = join8(roleDir, "runtime");
|
|
1118
|
+
const gitignorePath = join8(roleDir, ".gitignore");
|
|
932
1119
|
let isTracked = false;
|
|
933
1120
|
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
934
1121
|
cwd: targetDir,
|
|
@@ -938,9 +1125,9 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
938
1125
|
isTracked = true;
|
|
939
1126
|
}
|
|
940
1127
|
let isIgnored = false;
|
|
941
|
-
const fullGitignorePath =
|
|
942
|
-
if (
|
|
943
|
-
const content =
|
|
1128
|
+
const fullGitignorePath = join8(targetDir, gitignorePath);
|
|
1129
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1130
|
+
const content = readFileSync3(fullGitignorePath, "utf8");
|
|
944
1131
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
945
1132
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
946
1133
|
}
|
|
@@ -965,14 +1152,14 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
965
1152
|
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
966
1153
|
if (!this.context.dryRun) {
|
|
967
1154
|
let content = "";
|
|
968
|
-
if (
|
|
969
|
-
content =
|
|
1155
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1156
|
+
content = readFileSync3(fullGitignorePath, "utf8");
|
|
970
1157
|
}
|
|
971
1158
|
if (content && !content.endsWith("\n")) {
|
|
972
1159
|
content += "\n";
|
|
973
1160
|
}
|
|
974
1161
|
content += "runtime/\n";
|
|
975
|
-
|
|
1162
|
+
writeFileSync4(fullGitignorePath, content, "utf8");
|
|
976
1163
|
}
|
|
977
1164
|
}
|
|
978
1165
|
}
|
|
@@ -994,8 +1181,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
|
|
|
994
1181
|
|
|
995
1182
|
// src/commands/hermes/WireTelegram.ts
|
|
996
1183
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
997
|
-
import { join as
|
|
998
|
-
import { existsSync as
|
|
1184
|
+
import { join as join9 } from "node:path";
|
|
1185
|
+
import { existsSync as existsSync6, unlinkSync } from "node:fs";
|
|
999
1186
|
import * as p3 from "@clack/prompts";
|
|
1000
1187
|
var WireTelegram = class extends Command {
|
|
1001
1188
|
async invoke() {
|
|
@@ -1083,15 +1270,15 @@ var WireTelegram = class extends Command {
|
|
|
1083
1270
|
if (p3.isCancel(allowedAnswer)) {
|
|
1084
1271
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1085
1272
|
}
|
|
1086
|
-
const script =
|
|
1087
|
-
if (!
|
|
1273
|
+
const script = join9(roleDir, ".scripts", "30-telegram.sh");
|
|
1274
|
+
if (!existsSync6(script)) {
|
|
1088
1275
|
return {
|
|
1089
1276
|
success: false,
|
|
1090
1277
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1091
1278
|
};
|
|
1092
1279
|
}
|
|
1093
|
-
const marker =
|
|
1094
|
-
if (
|
|
1280
|
+
const marker = join9(roleDir, ".scripts", ".done-30-telegram");
|
|
1281
|
+
if (existsSync6(marker)) unlinkSync(marker);
|
|
1095
1282
|
const spinner4 = p3.spinner();
|
|
1096
1283
|
spinner4.start("Verifying token + wiring profile");
|
|
1097
1284
|
const result = spawnSync3("bash", [script], {
|
|
@@ -1118,8 +1305,8 @@ function cap(s) {
|
|
|
1118
1305
|
|
|
1119
1306
|
// src/commands/hermes/WireEmail.ts
|
|
1120
1307
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1121
|
-
import { join as
|
|
1122
|
-
import { existsSync as
|
|
1308
|
+
import { join as join10 } from "node:path";
|
|
1309
|
+
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1123
1310
|
import * as p4 from "@clack/prompts";
|
|
1124
1311
|
var WireEmail = class extends Command {
|
|
1125
1312
|
async invoke() {
|
|
@@ -1134,8 +1321,8 @@ var WireEmail = class extends Command {
|
|
|
1134
1321
|
if (!targetRepo || !role || !roleDir) {
|
|
1135
1322
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1136
1323
|
}
|
|
1137
|
-
const script =
|
|
1138
|
-
if (!
|
|
1324
|
+
const script = join10(roleDir, ".scripts", "50-email.sh");
|
|
1325
|
+
if (!existsSync7(script)) {
|
|
1139
1326
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1140
1327
|
}
|
|
1141
1328
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
@@ -1197,8 +1384,8 @@ var WireEmail = class extends Command {
|
|
|
1197
1384
|
}
|
|
1198
1385
|
}
|
|
1199
1386
|
}
|
|
1200
|
-
const marker =
|
|
1201
|
-
if (
|
|
1387
|
+
const marker = join10(roleDir, ".scripts", ".done-50-email");
|
|
1388
|
+
if (existsSync7(marker)) unlinkSync2(marker);
|
|
1202
1389
|
const spinner4 = p4.spinner();
|
|
1203
1390
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1204
1391
|
const result = spawnSync4("bash", [script], {
|
|
@@ -1285,15 +1472,15 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1285
1472
|
|
|
1286
1473
|
// src/commands/AgentHooksCommands.ts
|
|
1287
1474
|
import { homedir as homedir4 } from "node:os";
|
|
1288
|
-
import { join as
|
|
1289
|
-
import { existsSync as
|
|
1475
|
+
import { join as join12, dirname as dirname5 } from "node:path";
|
|
1476
|
+
import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1290
1477
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1291
1478
|
|
|
1292
1479
|
// src/project/index.ts
|
|
1293
1480
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1294
|
-
import { existsSync as
|
|
1481
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1295
1482
|
import { homedir as homedir3 } from "node:os";
|
|
1296
|
-
import { basename as basename2, delimiter, dirname as dirname4, join as
|
|
1483
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join11, resolve } from "node:path";
|
|
1297
1484
|
import YAML2 from "yaml";
|
|
1298
1485
|
|
|
1299
1486
|
// src/project/RegistryStore.ts
|
|
@@ -1528,18 +1715,18 @@ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
|
1528
1715
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1529
1716
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1530
1717
|
"/home/delorenj/code/skillex/all-skills",
|
|
1531
|
-
|
|
1532
|
-
|
|
1718
|
+
join11(homedir3(), ".agents", "skills"),
|
|
1719
|
+
join11(homedir3(), ".codex", "skills")
|
|
1533
1720
|
];
|
|
1534
1721
|
function projectRegistryPath(env2 = process.env) {
|
|
1535
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1722
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1536
1723
|
}
|
|
1537
1724
|
function emptyProjectRegistry() {
|
|
1538
1725
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1539
1726
|
}
|
|
1540
1727
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1541
|
-
if (!
|
|
1542
|
-
const raw = YAML2.parse(
|
|
1728
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
1729
|
+
const raw = YAML2.parse(readFileSync4(path, "utf8"));
|
|
1543
1730
|
if (raw == null) return emptyProjectRegistry();
|
|
1544
1731
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1545
1732
|
const registry = raw;
|
|
@@ -1554,7 +1741,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1554
1741
|
validateProjectRegistry(registry);
|
|
1555
1742
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1556
1743
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1557
|
-
|
|
1744
|
+
writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1558
1745
|
renameSync(temp, path);
|
|
1559
1746
|
}
|
|
1560
1747
|
function validateProjectRegistry(registry) {
|
|
@@ -1636,7 +1823,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1636
1823
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1637
1824
|
if (override === "0" || override === "false") return false;
|
|
1638
1825
|
if (override === "1" || override === "true") return true;
|
|
1639
|
-
return !
|
|
1826
|
+
return !existsSync8(join11(homedir3(), ".agents", "hooks"));
|
|
1640
1827
|
}
|
|
1641
1828
|
function jsonStable(value) {
|
|
1642
1829
|
return JSON.stringify(value);
|
|
@@ -1667,12 +1854,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1667
1854
|
if (!sourceSkill) return void 0;
|
|
1668
1855
|
const expanded = expandHome(sourceSkill);
|
|
1669
1856
|
const direct = resolve(expanded);
|
|
1670
|
-
if (
|
|
1857
|
+
if (existsSync8(direct)) return direct;
|
|
1671
1858
|
const name = basename2(sourceSkill);
|
|
1672
1859
|
const roots = sourceSkillRoots(env2);
|
|
1673
1860
|
for (const root of roots) {
|
|
1674
|
-
const candidate =
|
|
1675
|
-
if (
|
|
1861
|
+
const candidate = join11(root, name);
|
|
1862
|
+
if (existsSync8(candidate)) return candidate;
|
|
1676
1863
|
}
|
|
1677
1864
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1678
1865
|
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
@@ -1753,7 +1940,7 @@ function planProjectInit(input) {
|
|
|
1753
1940
|
}));
|
|
1754
1941
|
}
|
|
1755
1942
|
actions.push(
|
|
1756
|
-
{ kind: "project.write-manifest", path:
|
|
1943
|
+
{ kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
|
|
1757
1944
|
{
|
|
1758
1945
|
kind: "ticket-provider.create-or-link",
|
|
1759
1946
|
enabled: live,
|
|
@@ -1806,7 +1993,7 @@ async function executeProjectInitPlan(plan) {
|
|
|
1806
1993
|
}
|
|
1807
1994
|
if (result.status !== 0) {
|
|
1808
1995
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1809
|
-
if (
|
|
1996
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1810
1997
|
break;
|
|
1811
1998
|
}
|
|
1812
1999
|
changedFiles.push(action.targetDir);
|
|
@@ -1814,9 +2001,9 @@ async function executeProjectInitPlan(plan) {
|
|
|
1814
2001
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1815
2002
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1816
2003
|
`;
|
|
1817
|
-
const current =
|
|
2004
|
+
const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
|
|
1818
2005
|
if (current !== next) {
|
|
1819
|
-
|
|
2006
|
+
writeFileSync5(action.path, next, "utf8");
|
|
1820
2007
|
changedFiles.push(action.path);
|
|
1821
2008
|
}
|
|
1822
2009
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1879,7 +2066,7 @@ function getProject(registry, slug) {
|
|
|
1879
2066
|
return project;
|
|
1880
2067
|
}
|
|
1881
2068
|
function buildCommonProjectCopierAction(input) {
|
|
1882
|
-
const templateDir =
|
|
2069
|
+
const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
|
|
1883
2070
|
const data = {
|
|
1884
2071
|
project_name: input.projectName,
|
|
1885
2072
|
project_description: input.projectDescription ?? "",
|
|
@@ -1908,7 +2095,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1908
2095
|
function resolvePjanglerRoot() {
|
|
1909
2096
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1910
2097
|
while (dir !== dirname4(dir)) {
|
|
1911
|
-
if (
|
|
2098
|
+
if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1912
2099
|
dir = dirname4(dir);
|
|
1913
2100
|
}
|
|
1914
2101
|
return resolve(process.cwd());
|
|
@@ -1940,7 +2127,7 @@ function validateProjectRecord(project, key) {
|
|
|
1940
2127
|
}
|
|
1941
2128
|
function expandHome(path) {
|
|
1942
2129
|
if (path === "~") return homedir3();
|
|
1943
|
-
if (path.startsWith("~/")) return
|
|
2130
|
+
if (path.startsWith("~/")) return join11(homedir3(), path.slice(2));
|
|
1944
2131
|
return path;
|
|
1945
2132
|
}
|
|
1946
2133
|
function isRecord(value) {
|
|
@@ -1957,16 +2144,16 @@ function resolveTemplateRoot() {
|
|
|
1957
2144
|
try {
|
|
1958
2145
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1959
2146
|
for (let i = 0; i < 8; i++) {
|
|
1960
|
-
candidates.push(
|
|
2147
|
+
candidates.push(join12(dir, "templates", "commonproject", "template"));
|
|
1961
2148
|
const parent = dirname5(dir);
|
|
1962
2149
|
if (parent === dir) break;
|
|
1963
2150
|
dir = parent;
|
|
1964
2151
|
}
|
|
1965
2152
|
} catch {
|
|
1966
2153
|
}
|
|
1967
|
-
candidates.push(
|
|
2154
|
+
candidates.push(join12(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1968
2155
|
for (const c of candidates) {
|
|
1969
|
-
if (
|
|
2156
|
+
if (existsSync9(join12(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1970
2157
|
}
|
|
1971
2158
|
throw new Error(
|
|
1972
2159
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1985,18 +2172,17 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1985
2172
|
}
|
|
1986
2173
|
const items = [
|
|
1987
2174
|
{ rel: ".agents/hooks", dir: true },
|
|
2175
|
+
{ rel: ".agents/skills.json", dir: false },
|
|
1988
2176
|
{ rel: ".agents/local.example.json", dir: false },
|
|
1989
|
-
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
1990
|
-
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
1991
2177
|
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
1992
2178
|
];
|
|
1993
2179
|
const created = [];
|
|
1994
2180
|
const skipped = [];
|
|
1995
2181
|
for (const { rel, dir } of items) {
|
|
1996
|
-
const src =
|
|
1997
|
-
const dest =
|
|
1998
|
-
if (!
|
|
1999
|
-
if (
|
|
2182
|
+
const src = join12(templateRoot, rel);
|
|
2183
|
+
const dest = join12(this.context.targetDir, rel);
|
|
2184
|
+
if (!existsSync9(src)) continue;
|
|
2185
|
+
if (existsSync9(dest) && !this.context.force) {
|
|
2000
2186
|
skipped.push(rel);
|
|
2001
2187
|
continue;
|
|
2002
2188
|
}
|
|
@@ -2022,25 +2208,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2022
2208
|
if (!resolveAgentHooksLayer()) {
|
|
2023
2209
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
2024
2210
|
}
|
|
2025
|
-
const misePath =
|
|
2026
|
-
if (!
|
|
2211
|
+
const misePath = join12(this.context.targetDir, "mise.toml");
|
|
2212
|
+
if (!existsSync9(misePath)) {
|
|
2027
2213
|
return {
|
|
2028
2214
|
success: false,
|
|
2029
2215
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
2030
2216
|
};
|
|
2031
2217
|
}
|
|
2032
|
-
let content =
|
|
2218
|
+
let content = readFileSync5(misePath, "utf8");
|
|
2033
2219
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
2034
2220
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
2035
2221
|
}
|
|
2036
2222
|
const cr = _WireMiseAgentHooks.CR;
|
|
2037
|
-
const enterAdds = [
|
|
2038
|
-
` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
|
|
2039
|
-
` "${cr}/.agents/hooks/sync.py --install --quiet",`
|
|
2040
|
-
].join("\n");
|
|
2223
|
+
const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
|
|
2041
2224
|
const leaveBlock = [
|
|
2042
2225
|
"leave = [",
|
|
2043
|
-
` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
|
|
2044
2226
|
` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
|
|
2045
2227
|
"]"
|
|
2046
2228
|
].join("\n");
|
|
@@ -2057,7 +2239,6 @@ ${enterAdds}${close}`;
|
|
|
2057
2239
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
2058
2240
|
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2059
2241
|
return `${head}${sep}
|
|
2060
|
-
"${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
|
|
2061
2242
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
2062
2243
|
});
|
|
2063
2244
|
} else {
|
|
@@ -2070,6 +2251,14 @@ ${leaveBlock}`);
|
|
|
2070
2251
|
"",
|
|
2071
2252
|
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
2072
2253
|
"[[watch_files]]",
|
|
2254
|
+
'patterns = [".agents/skills.json"]',
|
|
2255
|
+
'task = "skills-sync"',
|
|
2256
|
+
"",
|
|
2257
|
+
"[tasks.skills-sync]",
|
|
2258
|
+
'description = "Sync skills from manifest to local CLI dirs"',
|
|
2259
|
+
'run = "sync-skills.py --scope project"',
|
|
2260
|
+
"",
|
|
2261
|
+
"[[watch_files]]",
|
|
2073
2262
|
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
2074
2263
|
'task = "hooks-sync"',
|
|
2075
2264
|
"",
|
|
@@ -2085,18 +2274,6 @@ ${leaveBlock}`);
|
|
|
2085
2274
|
'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
|
|
2086
2275
|
`run = "${cr}/.agents/hooks/sync.py --uninstall"`,
|
|
2087
2276
|
"",
|
|
2088
|
-
"[tasks.link-project-skills-to-clis]",
|
|
2089
|
-
'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
|
|
2090
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
2091
|
-
"",
|
|
2092
|
-
"[tasks.unlink-project-skills-from-clis]",
|
|
2093
|
-
'description = "Remove project skill symlinks from shared per-CLI dirs"',
|
|
2094
|
-
`run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
|
|
2095
|
-
"",
|
|
2096
|
-
"[tasks.skills-relink]",
|
|
2097
|
-
'description = "Re-fan the project skill set to all CLIs"',
|
|
2098
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
2099
|
-
"",
|
|
2100
2277
|
"[tasks.hindsight-setup]",
|
|
2101
2278
|
`description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
|
|
2102
2279
|
`run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
|
|
@@ -2105,7 +2282,7 @@ ${leaveBlock}`);
|
|
|
2105
2282
|
""
|
|
2106
2283
|
].join("\n");
|
|
2107
2284
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
2108
|
-
if (!this.context.dryRun)
|
|
2285
|
+
if (!this.context.dryRun) writeFileSync6(misePath, content);
|
|
2109
2286
|
if (wiredHooks) {
|
|
2110
2287
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
2111
2288
|
}
|
|
@@ -2114,8 +2291,8 @@ ${leaveBlock}`);
|
|
|
2114
2291
|
message: this.formatMessage(
|
|
2115
2292
|
`\u2705 Added agent-hooks tasks to mise.toml.
|
|
2116
2293
|
\u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
|
|
2117
|
-
enter += "
|
|
2118
|
-
leave += "${cr}/.
|
|
2294
|
+
enter += "sync-skills.py --scope project", "${cr}/.agents/hooks/sync.py --install --quiet"
|
|
2295
|
+
leave += "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
|
|
2119
2296
|
)
|
|
2120
2297
|
};
|
|
2121
2298
|
}
|
|
@@ -2130,10 +2307,25 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
2130
2307
|
printNextSteps() {
|
|
2131
2308
|
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
2132
2309
|
console.log(" Next steps:");
|
|
2133
|
-
console.log(" 1. mise run
|
|
2134
|
-
console.log(" 2.
|
|
2135
|
-
console.log(" 3.
|
|
2136
|
-
console.log(
|
|
2310
|
+
console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
|
|
2311
|
+
console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
2312
|
+
console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
|
|
2313
|
+
console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
2314
|
+
console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
// src/recipes/MiseOpInjectRecipe.ts
|
|
2319
|
+
var MiseOpInjectRecipe = class extends Recipe {
|
|
2320
|
+
constructor(context) {
|
|
2321
|
+
super(context);
|
|
2322
|
+
this.addIngredient(WireMiseOpInject);
|
|
2323
|
+
}
|
|
2324
|
+
printNextSteps() {
|
|
2325
|
+
console.log("\u{1F389} Wired up .env.op 1Password resolution via mise!");
|
|
2326
|
+
console.log(" Next steps:");
|
|
2327
|
+
console.log(" 1. Create .env.op with your op:// secret references");
|
|
2328
|
+
console.log(" 2. Run `mise run secrets-inject` or simply cd out and back in to trigger the hook");
|
|
2137
2329
|
}
|
|
2138
2330
|
};
|
|
2139
2331
|
|
|
@@ -2176,6 +2368,12 @@ var RECIPE_REGISTRY = {
|
|
|
2176
2368
|
description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
|
|
2177
2369
|
class: AgentHooksRecipe,
|
|
2178
2370
|
commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
|
|
2371
|
+
},
|
|
2372
|
+
"mise-op-inject": {
|
|
2373
|
+
name: "mise-op-inject",
|
|
2374
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2375
|
+
class: MiseOpInjectRecipe,
|
|
2376
|
+
commands: ["WireMiseOpInject"]
|
|
2179
2377
|
}
|
|
2180
2378
|
};
|
|
2181
2379
|
var COMMAND_REGISTRY = {
|
|
@@ -2244,6 +2442,12 @@ var COMMAND_REGISTRY = {
|
|
|
2244
2442
|
description: "Create .env.example file",
|
|
2245
2443
|
group: "environment",
|
|
2246
2444
|
class: AddDotenv
|
|
2445
|
+
},
|
|
2446
|
+
WireMiseOpInject: {
|
|
2447
|
+
name: "WireMiseOpInject",
|
|
2448
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2449
|
+
group: "mise",
|
|
2450
|
+
class: WireMiseOpInject
|
|
2247
2451
|
}
|
|
2248
2452
|
};
|
|
2249
2453
|
function getRecipeNames() {
|
|
@@ -2259,15 +2463,15 @@ function createRecipe(name, context) {
|
|
|
2259
2463
|
}
|
|
2260
2464
|
|
|
2261
2465
|
// src/utils/version.ts
|
|
2262
|
-
import { readFileSync as
|
|
2263
|
-
import { dirname as dirname6, join as
|
|
2466
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2467
|
+
import { dirname as dirname6, join as join13 } from "node:path";
|
|
2264
2468
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2265
2469
|
var PJANGLER_VERSION = (() => {
|
|
2266
2470
|
try {
|
|
2267
2471
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2268
2472
|
for (let i = 0; i < 4; i++) {
|
|
2269
2473
|
try {
|
|
2270
|
-
const raw =
|
|
2474
|
+
const raw = readFileSync6(join13(dir, "package.json"), "utf8");
|
|
2271
2475
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
2272
2476
|
} catch {
|
|
2273
2477
|
const parent = dirname6(dir);
|
|
@@ -2281,29 +2485,39 @@ var PJANGLER_VERSION = (() => {
|
|
|
2281
2485
|
})();
|
|
2282
2486
|
|
|
2283
2487
|
// src/parity/index.ts
|
|
2284
|
-
import { existsSync as
|
|
2285
|
-
import { basename as basename3, dirname as dirname7, join as
|
|
2488
|
+
import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync7, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync4, copyFileSync, rmSync } from "node:fs";
|
|
2489
|
+
import { basename as basename3, dirname as dirname7, join as join14, relative, resolve as resolve2 } from "node:path";
|
|
2286
2490
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2287
2491
|
import { homedir as homedir5 } from "node:os";
|
|
2288
2492
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2289
2493
|
import YAML3 from "yaml";
|
|
2290
2494
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2291
2495
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
2496
|
+
var SYNC_SKILLS_SCRIPT = "sync-skills.py --scope project";
|
|
2292
2497
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
2498
|
+
var SKILLS_REGISTRY_URL = "https://github.com/delorenj/skillex.git";
|
|
2293
2499
|
var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
|
|
2294
2500
|
# agent files to the main AGENTS.md file.
|
|
2295
2501
|
#
|
|
2296
2502
|
# TODO: Ensure this works for all levels of nesting.
|
|
2297
2503
|
# i.e. All linked agent files MUST be siblings at
|
|
2298
2504
|
# any given level of nesting.`;
|
|
2299
|
-
var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
|
|
2505
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT, SYNC_SKILLS_SCRIPT];
|
|
2300
2506
|
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
2301
2507
|
patterns = ["AGENTS.md"]
|
|
2302
2508
|
task = "link-agentfiles"
|
|
2303
2509
|
|
|
2510
|
+
[[watch_files]]
|
|
2511
|
+
patterns = [".agents/skills.json"]
|
|
2512
|
+
task = "skills-sync"
|
|
2513
|
+
|
|
2304
2514
|
[tasks.link-agentfiles]
|
|
2305
2515
|
description = "Symlink all agent files to AGENTS.md"
|
|
2306
|
-
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2516
|
+
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2517
|
+
|
|
2518
|
+
[tasks.skills-sync]
|
|
2519
|
+
description = "Sync skills from manifest to local CLI dirs"
|
|
2520
|
+
run = "sync-skills.py --scope project"`;
|
|
2307
2521
|
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
2308
2522
|
[tasks."version"]
|
|
2309
2523
|
description = "Print the current version (vX.Y.Z)"
|
|
@@ -2333,7 +2547,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
|
|
|
2333
2547
|
function resolvePjanglerRoot2() {
|
|
2334
2548
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2335
2549
|
while (dir !== dirname7(dir)) {
|
|
2336
|
-
if (
|
|
2550
|
+
if (existsSync10(join14(dir, "package.json")) && existsSync10(join14(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2337
2551
|
return dir;
|
|
2338
2552
|
}
|
|
2339
2553
|
dir = dirname7(dir);
|
|
@@ -2344,17 +2558,17 @@ function normalizeNewlines(value) {
|
|
|
2344
2558
|
return value.replace(/\r\n/g, "\n");
|
|
2345
2559
|
}
|
|
2346
2560
|
function readText(path) {
|
|
2347
|
-
return normalizeNewlines(
|
|
2561
|
+
return normalizeNewlines(readFileSync7(path, "utf8"));
|
|
2348
2562
|
}
|
|
2349
2563
|
function safeReadText(path) {
|
|
2350
|
-
return
|
|
2564
|
+
return existsSync10(path) ? readText(path) : null;
|
|
2351
2565
|
}
|
|
2352
2566
|
function ensureParent(path) {
|
|
2353
2567
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2354
2568
|
}
|
|
2355
2569
|
function writeText(path, content) {
|
|
2356
2570
|
ensureParent(path);
|
|
2357
|
-
|
|
2571
|
+
writeFileSync7(path, content);
|
|
2358
2572
|
}
|
|
2359
2573
|
function tryParseJson(text2) {
|
|
2360
2574
|
if (!text2) return null;
|
|
@@ -2371,7 +2585,7 @@ function titleCaseSlug(slug) {
|
|
|
2371
2585
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2372
2586
|
}
|
|
2373
2587
|
function readSymlinkTarget(path) {
|
|
2374
|
-
if (!
|
|
2588
|
+
if (!existsSync10(path)) return null;
|
|
2375
2589
|
try {
|
|
2376
2590
|
return readlinkSync(path);
|
|
2377
2591
|
} catch {
|
|
@@ -2379,7 +2593,7 @@ function readSymlinkTarget(path) {
|
|
|
2379
2593
|
}
|
|
2380
2594
|
}
|
|
2381
2595
|
function ensureSymlink(path, target, dryRun) {
|
|
2382
|
-
if (
|
|
2596
|
+
if (existsSync10(path)) {
|
|
2383
2597
|
const stat = lstatSync(path);
|
|
2384
2598
|
if (stat.isSymbolicLink()) {
|
|
2385
2599
|
const current = readSymlinkTarget(path);
|
|
@@ -2396,11 +2610,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2396
2610
|
return { changed: true };
|
|
2397
2611
|
}
|
|
2398
2612
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2399
|
-
const agentsPath =
|
|
2400
|
-
if (
|
|
2613
|
+
const agentsPath = join14(repoRoot, "AGENTS.md");
|
|
2614
|
+
if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
|
|
2401
2615
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2402
|
-
const source =
|
|
2403
|
-
if (!
|
|
2616
|
+
const source = join14(repoRoot, file);
|
|
2617
|
+
if (!existsSync10(source)) continue;
|
|
2404
2618
|
const stat = lstatSync(source);
|
|
2405
2619
|
if (stat.isSymbolicLink()) continue;
|
|
2406
2620
|
if (stat.isFile()) {
|
|
@@ -2409,8 +2623,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2409
2623
|
}
|
|
2410
2624
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2411
2625
|
}
|
|
2412
|
-
const readmePath =
|
|
2413
|
-
if (
|
|
2626
|
+
const readmePath = join14(repoRoot, "README.md");
|
|
2627
|
+
if (existsSync10(readmePath)) {
|
|
2414
2628
|
const stat = lstatSync(readmePath);
|
|
2415
2629
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2416
2630
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2449,12 +2663,12 @@ function yamlGet(text2, keyPath) {
|
|
|
2449
2663
|
return "";
|
|
2450
2664
|
}
|
|
2451
2665
|
function discoverRoles(repoRoot) {
|
|
2452
|
-
const rolesDir =
|
|
2453
|
-
if (!
|
|
2666
|
+
const rolesDir = join14(repoRoot, "agents", "hermes");
|
|
2667
|
+
if (!existsSync10(rolesDir)) return [];
|
|
2454
2668
|
return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2455
|
-
const roleDir =
|
|
2456
|
-
const roleYamlPath =
|
|
2457
|
-
if (!
|
|
2669
|
+
const roleDir = join14(rolesDir, entry.name);
|
|
2670
|
+
const roleYamlPath = join14(roleDir, "role.yaml");
|
|
2671
|
+
if (!existsSync10(roleYamlPath)) return null;
|
|
2458
2672
|
const text2 = readText(roleYamlPath);
|
|
2459
2673
|
const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
|
|
2460
2674
|
return {
|
|
@@ -2482,7 +2696,7 @@ function discoverRoles(repoRoot) {
|
|
|
2482
2696
|
}).filter((value) => Boolean(value));
|
|
2483
2697
|
}
|
|
2484
2698
|
function registryPath(homeDir) {
|
|
2485
|
-
return
|
|
2699
|
+
return join14(homeDir, ".hermes", "agents-registry.yaml");
|
|
2486
2700
|
}
|
|
2487
2701
|
function systemctlUser(args) {
|
|
2488
2702
|
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -2493,8 +2707,8 @@ function systemctlUser(args) {
|
|
|
2493
2707
|
};
|
|
2494
2708
|
}
|
|
2495
2709
|
function templateScript(ctx, name) {
|
|
2496
|
-
const source =
|
|
2497
|
-
return
|
|
2710
|
+
const source = join14(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2711
|
+
return existsSync10(source) ? readText(source) : void 0;
|
|
2498
2712
|
}
|
|
2499
2713
|
function templateVersioningScript(ctx) {
|
|
2500
2714
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2506,8 +2720,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2506
2720
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2507
2721
|
if (override === "0" || override === "false") return false;
|
|
2508
2722
|
if (override === "1" || override === "true") return true;
|
|
2509
|
-
if (
|
|
2510
|
-
return !
|
|
2723
|
+
if (existsSync10(join14(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2724
|
+
return !existsSync10(join14(ctx.homeDir, ".agents", "hooks"));
|
|
2511
2725
|
}
|
|
2512
2726
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2513
2727
|
const out = [];
|
|
@@ -2537,19 +2751,36 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
2537
2751
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2538
2752
|
}
|
|
2539
2753
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2540
|
-
const targetPath =
|
|
2541
|
-
if (
|
|
2542
|
-
const sourcePath =
|
|
2543
|
-
if (!
|
|
2754
|
+
const targetPath = join14(ctx.repoRoot, "mise.toml");
|
|
2755
|
+
if (existsSync10(targetPath)) return false;
|
|
2756
|
+
const sourcePath = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2757
|
+
if (!existsSync10(sourcePath)) return false;
|
|
2544
2758
|
changedFiles.push(targetPath);
|
|
2545
2759
|
if (!ctx.dryRun) {
|
|
2546
2760
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
2547
2761
|
}
|
|
2548
2762
|
return true;
|
|
2549
2763
|
}
|
|
2764
|
+
function templateCommonProjectText(ctx, rel) {
|
|
2765
|
+
const path = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
2766
|
+
return existsSync10(path) ? readText(path) : void 0;
|
|
2767
|
+
}
|
|
2768
|
+
function canonicalSkillsManifest() {
|
|
2769
|
+
return `${JSON.stringify(
|
|
2770
|
+
{
|
|
2771
|
+
$schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
|
|
2772
|
+
inherit_global: true,
|
|
2773
|
+
registry: SKILLS_REGISTRY_URL,
|
|
2774
|
+
skills: []
|
|
2775
|
+
},
|
|
2776
|
+
null,
|
|
2777
|
+
2
|
|
2778
|
+
)}
|
|
2779
|
+
`;
|
|
2780
|
+
}
|
|
2550
2781
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2551
|
-
const packageJson =
|
|
2552
|
-
return
|
|
2782
|
+
const packageJson = join14(repoRoot, "package.json");
|
|
2783
|
+
return existsSync10(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
2553
2784
|
}
|
|
2554
2785
|
function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
|
|
2555
2786
|
if (startMarker.test(text2)) {
|
|
@@ -2573,7 +2804,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2573
2804
|
function requiredMisePathEntries(ctx) {
|
|
2574
2805
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2575
2806
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2576
|
-
if (
|
|
2807
|
+
if (existsSync10(join14(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2577
2808
|
}
|
|
2578
2809
|
return required;
|
|
2579
2810
|
}
|
|
@@ -2674,6 +2905,9 @@ function stripTomlStringsAndComments(line) {
|
|
|
2674
2905
|
function isManagedHookEntry(value) {
|
|
2675
2906
|
const trimmed = value.trim();
|
|
2676
2907
|
if (trimmed === OP_INJECT_SCRIPT) return true;
|
|
2908
|
+
if (trimmed === SYNC_SKILLS_SCRIPT) return true;
|
|
2909
|
+
if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2910
|
+
if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2677
2911
|
return /link-agentfiles\.sh'?\s*$/.test(trimmed);
|
|
2678
2912
|
}
|
|
2679
2913
|
function normalizeHookScript(script) {
|
|
@@ -2776,12 +3010,17 @@ function upsertLinkAgentfilesHooks(text2) {
|
|
|
2776
3010
|
function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
2777
3011
|
const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
|
|
2778
3012
|
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
3013
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
|
|
3014
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3015
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3016
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
|
|
2779
3017
|
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
3018
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
|
|
2780
3019
|
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
2781
3020
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2782
3021
|
}
|
|
2783
3022
|
function readProjectJson(ctx) {
|
|
2784
|
-
return tryParseJson(safeReadText(
|
|
3023
|
+
return tryParseJson(safeReadText(join14(ctx.repoRoot, ".project.json")));
|
|
2785
3024
|
}
|
|
2786
3025
|
function boolSetting(value, fallback) {
|
|
2787
3026
|
if (typeof value === "boolean") return value;
|
|
@@ -2856,12 +3095,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2856
3095
|
};
|
|
2857
3096
|
}
|
|
2858
3097
|
function projectJsonFinding(ctx) {
|
|
2859
|
-
const projectPath =
|
|
2860
|
-
const planeJsonPath =
|
|
3098
|
+
const projectPath = join14(ctx.repoRoot, ".project.json");
|
|
3099
|
+
const planeJsonPath = join14(ctx.repoRoot, ".plane.json");
|
|
2861
3100
|
const details = [];
|
|
2862
3101
|
const data = readProjectJson(ctx);
|
|
2863
3102
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2864
|
-
if (!
|
|
3103
|
+
if (!existsSync10(projectPath)) {
|
|
2865
3104
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2866
3105
|
}
|
|
2867
3106
|
if (!data) {
|
|
@@ -2896,7 +3135,7 @@ function projectJsonFinding(ctx) {
|
|
|
2896
3135
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2897
3136
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2898
3137
|
}
|
|
2899
|
-
if (
|
|
3138
|
+
if (existsSync10(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2900
3139
|
return {
|
|
2901
3140
|
id: "sot.project-json",
|
|
2902
3141
|
title: "Canonical .project.json",
|
|
@@ -2977,17 +3216,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2977
3216
|
`.replace(/\u0010/g, "$");
|
|
2978
3217
|
}
|
|
2979
3218
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2980
|
-
if (!
|
|
3219
|
+
if (!existsSync10(sourceDir)) return;
|
|
2981
3220
|
mkdirSync6(targetDir, { recursive: true });
|
|
2982
3221
|
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2983
|
-
const sourcePath =
|
|
3222
|
+
const sourcePath = join14(sourceDir, entry.name);
|
|
2984
3223
|
if (skip?.(sourcePath)) continue;
|
|
2985
|
-
const targetPath =
|
|
3224
|
+
const targetPath = join14(targetDir, entry.name);
|
|
2986
3225
|
if (entry.isDirectory()) {
|
|
2987
3226
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2988
3227
|
continue;
|
|
2989
3228
|
}
|
|
2990
|
-
if (
|
|
3229
|
+
if (existsSync10(targetPath)) continue;
|
|
2991
3230
|
changedFiles.push(targetPath);
|
|
2992
3231
|
if (!dryRun) {
|
|
2993
3232
|
ensureParent(targetPath);
|
|
@@ -2996,7 +3235,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2996
3235
|
}
|
|
2997
3236
|
}
|
|
2998
3237
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2999
|
-
const gitmodulesPath =
|
|
3238
|
+
const gitmodulesPath = join14(repoRoot, ".gitmodules");
|
|
3000
3239
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
3001
3240
|
const owner = role.runtimeOwner || "delorenj";
|
|
3002
3241
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -3163,7 +3402,7 @@ function runBmadInstall(repoRoot) {
|
|
|
3163
3402
|
return { ok: true };
|
|
3164
3403
|
}
|
|
3165
3404
|
function readInstalledBmadVersion(repoRoot) {
|
|
3166
|
-
const raw = safeReadText(
|
|
3405
|
+
const raw = safeReadText(join14(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
3167
3406
|
if (!raw) return void 0;
|
|
3168
3407
|
try {
|
|
3169
3408
|
const parsed = YAML3.parse(raw);
|
|
@@ -3174,8 +3413,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
3174
3413
|
}
|
|
3175
3414
|
}
|
|
3176
3415
|
function bmadCachePath(homeDir) {
|
|
3177
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
3178
|
-
return
|
|
3416
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join14(homeDir, ".cache");
|
|
3417
|
+
return join14(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
3179
3418
|
}
|
|
3180
3419
|
function readBmadDistTagsCache(homeDir) {
|
|
3181
3420
|
const raw = safeReadText(bmadCachePath(homeDir));
|
|
@@ -3218,7 +3457,7 @@ function resolveBmadDistTags(homeDir) {
|
|
|
3218
3457
|
try {
|
|
3219
3458
|
const path = bmadCachePath(homeDir);
|
|
3220
3459
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
3221
|
-
|
|
3460
|
+
writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
|
|
3222
3461
|
} catch {
|
|
3223
3462
|
}
|
|
3224
3463
|
return { distTags: fetched, stale: false };
|
|
@@ -3263,14 +3502,14 @@ var RULES = [
|
|
|
3263
3502
|
id: "mise.config-root",
|
|
3264
3503
|
title: "mise config_root + AGENTS link hooks",
|
|
3265
3504
|
audit: (ctx) => {
|
|
3266
|
-
const misePath =
|
|
3267
|
-
if (!
|
|
3505
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3506
|
+
if (!existsSync10(misePath)) {
|
|
3268
3507
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
3269
3508
|
}
|
|
3270
3509
|
const text2 = readText(misePath);
|
|
3271
3510
|
const details = [];
|
|
3272
|
-
const linkAgentfilesPath =
|
|
3273
|
-
if (!
|
|
3511
|
+
const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3512
|
+
if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
3274
3513
|
const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
3275
3514
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
3276
3515
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -3288,10 +3527,10 @@ var RULES = [
|
|
|
3288
3527
|
};
|
|
3289
3528
|
},
|
|
3290
3529
|
migrate: (ctx, finding) => {
|
|
3291
|
-
const path =
|
|
3530
|
+
const path = join14(ctx.repoRoot, "mise.toml");
|
|
3292
3531
|
const changedFiles = [];
|
|
3293
3532
|
const details = [];
|
|
3294
|
-
if (!
|
|
3533
|
+
if (!existsSync10(path)) {
|
|
3295
3534
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3296
3535
|
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: [] };
|
|
3297
3536
|
}
|
|
@@ -3307,7 +3546,7 @@ var RULES = [
|
|
|
3307
3546
|
if (!ctx.dryRun) writeText(path, next);
|
|
3308
3547
|
text2 = next;
|
|
3309
3548
|
}
|
|
3310
|
-
const linkAgentfilesPath =
|
|
3549
|
+
const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3311
3550
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
3312
3551
|
if (expectedScript === void 0) {
|
|
3313
3552
|
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: [] };
|
|
@@ -3316,7 +3555,7 @@ var RULES = [
|
|
|
3316
3555
|
changedFiles.push(linkAgentfilesPath);
|
|
3317
3556
|
if (!ctx.dryRun) {
|
|
3318
3557
|
writeText(linkAgentfilesPath, expectedScript);
|
|
3319
|
-
|
|
3558
|
+
chmodSync4(linkAgentfilesPath, 493);
|
|
3320
3559
|
}
|
|
3321
3560
|
}
|
|
3322
3561
|
return {
|
|
@@ -3334,13 +3573,13 @@ var RULES = [
|
|
|
3334
3573
|
title: "managed mise versioning block",
|
|
3335
3574
|
audit: (ctx) => {
|
|
3336
3575
|
const details = [];
|
|
3337
|
-
const misePath =
|
|
3338
|
-
const versioningPath =
|
|
3339
|
-
const manifestPath =
|
|
3576
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3577
|
+
const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3578
|
+
const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3340
3579
|
const text2 = safeReadText(misePath);
|
|
3341
3580
|
if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
3342
|
-
if (!
|
|
3343
|
-
if (!
|
|
3581
|
+
if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
3582
|
+
if (!existsSync10(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
3344
3583
|
return {
|
|
3345
3584
|
id: "mise.versioning",
|
|
3346
3585
|
title: "managed mise versioning block",
|
|
@@ -3353,8 +3592,8 @@ var RULES = [
|
|
|
3353
3592
|
migrate: (ctx, finding) => {
|
|
3354
3593
|
const changedFiles = [];
|
|
3355
3594
|
const details = [];
|
|
3356
|
-
const misePath =
|
|
3357
|
-
if (!
|
|
3595
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3596
|
+
if (!existsSync10(misePath)) {
|
|
3358
3597
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3359
3598
|
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: [] };
|
|
3360
3599
|
}
|
|
@@ -3378,7 +3617,7 @@ var RULES = [
|
|
|
3378
3617
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3379
3618
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3380
3619
|
}
|
|
3381
|
-
const versioningPath =
|
|
3620
|
+
const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3382
3621
|
const expectedScript = templateVersioningScript(ctx);
|
|
3383
3622
|
if (expectedScript === void 0) {
|
|
3384
3623
|
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: [] };
|
|
@@ -3387,10 +3626,10 @@ var RULES = [
|
|
|
3387
3626
|
changedFiles.push(versioningPath);
|
|
3388
3627
|
if (!ctx.dryRun) {
|
|
3389
3628
|
writeText(versioningPath, expectedScript);
|
|
3390
|
-
|
|
3629
|
+
chmodSync4(versioningPath, 493);
|
|
3391
3630
|
}
|
|
3392
3631
|
}
|
|
3393
|
-
const manifestPath =
|
|
3632
|
+
const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3394
3633
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
3395
3634
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3396
3635
|
changedFiles.push(manifestPath);
|
|
@@ -3406,13 +3645,125 @@ var RULES = [
|
|
|
3406
3645
|
};
|
|
3407
3646
|
}
|
|
3408
3647
|
},
|
|
3648
|
+
{
|
|
3649
|
+
id: "skills.project-manifest",
|
|
3650
|
+
title: "Skillex project skills manifest",
|
|
3651
|
+
audit: (ctx) => {
|
|
3652
|
+
const details = [];
|
|
3653
|
+
const manifestPath = join14(ctx.repoRoot, ".agents", "skills.json");
|
|
3654
|
+
const legacyDir = join14(ctx.repoRoot, ".agents", "skills");
|
|
3655
|
+
const localExamplePath = join14(ctx.repoRoot, ".agents", "local.example.json");
|
|
3656
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3657
|
+
let fixable = true;
|
|
3658
|
+
const manifest = tryParseJson(safeReadText(manifestPath));
|
|
3659
|
+
if (!manifest) {
|
|
3660
|
+
details.push(".agents/skills.json missing or invalid JSON");
|
|
3661
|
+
} else {
|
|
3662
|
+
if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
|
|
3663
|
+
if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
|
|
3664
|
+
if (!Array.isArray(manifest.skills)) details.push(".agents/skills.json should define a skills array");
|
|
3665
|
+
}
|
|
3666
|
+
if (existsSync10(legacyDir)) {
|
|
3667
|
+
const entries = readdirSync2(legacyDir);
|
|
3668
|
+
if (entries.length > 0) {
|
|
3669
|
+
details.push(".agents/skills/ still contains legacy committed skills; map them into .agents/skills.json before migrating");
|
|
3670
|
+
fixable = false;
|
|
3671
|
+
} else {
|
|
3672
|
+
details.push(".agents/skills/ legacy directory should be removed");
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3676
|
+
if (existsSync10(join14(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
|
|
3677
|
+
}
|
|
3678
|
+
const localExample = tryParseJson(safeReadText(localExamplePath));
|
|
3679
|
+
if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
|
|
3680
|
+
details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
|
|
3681
|
+
}
|
|
3682
|
+
const mise = safeReadText(misePath);
|
|
3683
|
+
if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run sync-skills.py --scope project on enter");
|
|
3684
|
+
if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
|
|
3685
|
+
if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
|
|
3686
|
+
if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
|
|
3687
|
+
details.push("mise.toml still contains legacy skill-link wiring");
|
|
3688
|
+
}
|
|
3689
|
+
return {
|
|
3690
|
+
id: "skills.project-manifest",
|
|
3691
|
+
title: "Skillex project skills manifest",
|
|
3692
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3693
|
+
summary: details.length === 0 ? "Skillex skills manifest parity verified" : `${details.length} Skillex migration issue(s) detected`,
|
|
3694
|
+
details,
|
|
3695
|
+
fixable
|
|
3696
|
+
};
|
|
3697
|
+
},
|
|
3698
|
+
migrate: (ctx, finding) => {
|
|
3699
|
+
const changedFiles = [];
|
|
3700
|
+
const details = [];
|
|
3701
|
+
const manifestPath = join14(ctx.repoRoot, ".agents", "skills.json");
|
|
3702
|
+
const legacyDir = join14(ctx.repoRoot, ".agents", "skills");
|
|
3703
|
+
const localExamplePath = join14(ctx.repoRoot, ".agents", "local.example.json");
|
|
3704
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3705
|
+
if (existsSync10(legacyDir)) {
|
|
3706
|
+
const entries = readdirSync2(legacyDir);
|
|
3707
|
+
if (entries.length > 0) {
|
|
3708
|
+
return {
|
|
3709
|
+
id: finding.id,
|
|
3710
|
+
title: finding.title,
|
|
3711
|
+
status: "blocked",
|
|
3712
|
+
summary: "Legacy .agents/skills/ still contains committed skills; migrate them into .agents/skills.json manually first",
|
|
3713
|
+
changedFiles,
|
|
3714
|
+
details: entries.map((entry) => `.agents/skills/${entry}`)
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
changedFiles.push(legacyDir);
|
|
3718
|
+
if (!ctx.dryRun) rmSync(legacyDir, { recursive: true, force: true });
|
|
3719
|
+
}
|
|
3720
|
+
const expectedManifest = canonicalSkillsManifest();
|
|
3721
|
+
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3722
|
+
changedFiles.push(manifestPath);
|
|
3723
|
+
if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
|
|
3724
|
+
details.push("Wrote canonical .agents/skills.json manifest");
|
|
3725
|
+
}
|
|
3726
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3727
|
+
const path = join14(ctx.repoRoot, rel);
|
|
3728
|
+
if (existsSync10(path)) {
|
|
3729
|
+
changedFiles.push(path);
|
|
3730
|
+
if (!ctx.dryRun) unlinkSync3(path);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
const templateLocalExample = templateCommonProjectText(ctx, ".agents/local.example.json");
|
|
3734
|
+
const currentLocalExample = safeReadText(localExamplePath);
|
|
3735
|
+
if (templateLocalExample && currentLocalExample && currentLocalExample !== templateLocalExample) {
|
|
3736
|
+
changedFiles.push(localExamplePath);
|
|
3737
|
+
if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
|
|
3738
|
+
}
|
|
3739
|
+
if (!existsSync10(misePath)) {
|
|
3740
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3741
|
+
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 };
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
const currentMise = readText(misePath);
|
|
3745
|
+
const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
|
|
3746
|
+
if (nextMise !== currentMise) {
|
|
3747
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3748
|
+
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3749
|
+
}
|
|
3750
|
+
return {
|
|
3751
|
+
id: finding.id,
|
|
3752
|
+
title: finding.title,
|
|
3753
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3754
|
+
summary: changedFiles.length ? "Skillex skills manifest contract normalized" : "No changes required",
|
|
3755
|
+
changedFiles,
|
|
3756
|
+
details
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
},
|
|
3409
3760
|
{
|
|
3410
3761
|
id: "sot.agent-symlinks",
|
|
3411
3762
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
3412
3763
|
audit: (ctx) => {
|
|
3413
|
-
const agentsPath =
|
|
3414
|
-
if (!
|
|
3415
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
3764
|
+
const agentsPath = join14(ctx.repoRoot, "AGENTS.md");
|
|
3765
|
+
if (!existsSync10(agentsPath)) {
|
|
3766
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join14(ctx.repoRoot, file)));
|
|
3416
3767
|
if (fallbackSources.length === 0) {
|
|
3417
3768
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
3418
3769
|
}
|
|
@@ -3427,7 +3778,7 @@ var RULES = [
|
|
|
3427
3778
|
}
|
|
3428
3779
|
const details = [];
|
|
3429
3780
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3430
|
-
const full =
|
|
3781
|
+
const full = join14(ctx.repoRoot, file);
|
|
3431
3782
|
const target = readSymlinkTarget(full);
|
|
3432
3783
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
3433
3784
|
}
|
|
@@ -3451,7 +3802,7 @@ var RULES = [
|
|
|
3451
3802
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
3452
3803
|
}
|
|
3453
3804
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3454
|
-
const full =
|
|
3805
|
+
const full = join14(ctx.repoRoot, file);
|
|
3455
3806
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
3456
3807
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
3457
3808
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3473,7 +3824,7 @@ var RULES = [
|
|
|
3473
3824
|
migrate: (ctx, finding) => {
|
|
3474
3825
|
const changedFiles = [];
|
|
3475
3826
|
const details = [];
|
|
3476
|
-
const path =
|
|
3827
|
+
const path = join14(ctx.repoRoot, ".project.json");
|
|
3477
3828
|
const existing = readProjectJson(ctx) ?? {};
|
|
3478
3829
|
const canonical = canonicalProjectJson(ctx);
|
|
3479
3830
|
const merged = { ...existing, ...canonical };
|
|
@@ -3483,10 +3834,10 @@ var RULES = [
|
|
|
3483
3834
|
changedFiles.push(path);
|
|
3484
3835
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3485
3836
|
}
|
|
3486
|
-
const planeJson =
|
|
3487
|
-
if (
|
|
3837
|
+
const planeJson = join14(ctx.repoRoot, ".plane.json");
|
|
3838
|
+
if (existsSync10(planeJson)) {
|
|
3488
3839
|
const backup = `${planeJson}.migrated-backup`;
|
|
3489
|
-
if (
|
|
3840
|
+
if (existsSync10(backup)) {
|
|
3490
3841
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
3491
3842
|
} else {
|
|
3492
3843
|
changedFiles.push(backup);
|
|
@@ -3508,8 +3859,8 @@ var RULES = [
|
|
|
3508
3859
|
title: ".env.op + gitignore secrets contract",
|
|
3509
3860
|
audit: (ctx) => {
|
|
3510
3861
|
const details = [];
|
|
3511
|
-
const envOp = safeReadText(
|
|
3512
|
-
const gitignore = safeReadText(
|
|
3862
|
+
const envOp = safeReadText(join14(ctx.repoRoot, ".env.op"));
|
|
3863
|
+
const gitignore = safeReadText(join14(ctx.repoRoot, ".gitignore"));
|
|
3513
3864
|
if (!envOp) {
|
|
3514
3865
|
details.push(".env.op missing");
|
|
3515
3866
|
} else {
|
|
@@ -3535,12 +3886,12 @@ var RULES = [
|
|
|
3535
3886
|
migrate: (ctx, finding) => {
|
|
3536
3887
|
const changedFiles = [];
|
|
3537
3888
|
const details = [];
|
|
3538
|
-
const envOpPath =
|
|
3539
|
-
if (!
|
|
3889
|
+
const envOpPath = join14(ctx.repoRoot, ".env.op");
|
|
3890
|
+
if (!existsSync10(envOpPath)) {
|
|
3540
3891
|
changedFiles.push(envOpPath);
|
|
3541
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3892
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join14(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
3542
3893
|
}
|
|
3543
|
-
const gitignorePath =
|
|
3894
|
+
const gitignorePath = join14(ctx.repoRoot, ".gitignore");
|
|
3544
3895
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3545
3896
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3546
3897
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3567,7 +3918,7 @@ var RULES = [
|
|
|
3567
3918
|
title: ".copier-answers.yml provenance + drift report",
|
|
3568
3919
|
audit: (ctx) => {
|
|
3569
3920
|
const details = [];
|
|
3570
|
-
const path =
|
|
3921
|
+
const path = join14(ctx.repoRoot, ".copier-answers.yml");
|
|
3571
3922
|
const text2 = safeReadText(path);
|
|
3572
3923
|
const project = readProjectJson(ctx);
|
|
3573
3924
|
if (!text2) {
|
|
@@ -3598,12 +3949,12 @@ var RULES = [
|
|
|
3598
3949
|
const changedFiles = [];
|
|
3599
3950
|
const project = canonicalProjectJson(ctx);
|
|
3600
3951
|
const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3601
|
-
_src_path: ${
|
|
3952
|
+
_src_path: ${join14(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3602
3953
|
project_description: ${String(project.project_description)}
|
|
3603
3954
|
project_name: ${String(project.project_name)}
|
|
3604
3955
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3605
3956
|
`;
|
|
3606
|
-
const path =
|
|
3957
|
+
const path = join14(ctx.repoRoot, ".copier-answers.yml");
|
|
3607
3958
|
if (safeReadText(path) !== text2) {
|
|
3608
3959
|
changedFiles.push(path);
|
|
3609
3960
|
if (!ctx.dryRun) writeText(path, text2);
|
|
@@ -3622,14 +3973,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3622
3973
|
id: "bmad.scaffold",
|
|
3623
3974
|
title: "BMAD modules/docs scaffold",
|
|
3624
3975
|
audit: (ctx) => {
|
|
3625
|
-
const targetRoot =
|
|
3976
|
+
const targetRoot = join14(ctx.repoRoot, "_bmad");
|
|
3626
3977
|
const sentinels = [
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3978
|
+
join14("core", "config.yaml"),
|
|
3979
|
+
join14("config.toml"),
|
|
3980
|
+
join14("_config", "manifest.yaml"),
|
|
3981
|
+
join14("bmm", "config.yaml")
|
|
3631
3982
|
];
|
|
3632
|
-
const missing = sentinels.filter((file) => !
|
|
3983
|
+
const missing = sentinels.filter((file) => !existsSync10(join14(targetRoot, file)));
|
|
3633
3984
|
return {
|
|
3634
3985
|
id: "bmad.scaffold",
|
|
3635
3986
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3643,7 +3994,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3643
3994
|
const changedFiles = [];
|
|
3644
3995
|
if (ctx.dryRun) {
|
|
3645
3996
|
for (const detail of finding.details) {
|
|
3646
|
-
changedFiles.push(
|
|
3997
|
+
changedFiles.push(join14(ctx.repoRoot, detail));
|
|
3647
3998
|
}
|
|
3648
3999
|
return {
|
|
3649
4000
|
id: finding.id,
|
|
@@ -3668,8 +4019,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3668
4019
|
};
|
|
3669
4020
|
}
|
|
3670
4021
|
for (const detail of finding.details) {
|
|
3671
|
-
if (
|
|
3672
|
-
changedFiles.push(
|
|
4022
|
+
if (existsSync10(join14(ctx.repoRoot, detail))) {
|
|
4023
|
+
changedFiles.push(join14(ctx.repoRoot, detail));
|
|
3673
4024
|
}
|
|
3674
4025
|
}
|
|
3675
4026
|
return {
|
|
@@ -3692,7 +4043,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3692
4043
|
id: "bmad.version",
|
|
3693
4044
|
title: "BMAD version currency",
|
|
3694
4045
|
status: "skip",
|
|
3695
|
-
summary:
|
|
4046
|
+
summary: existsSync10(join14(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
3696
4047
|
details: [],
|
|
3697
4048
|
fixable: false
|
|
3698
4049
|
};
|
|
@@ -3747,7 +4098,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3747
4098
|
}
|
|
3748
4099
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3749
4100
|
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3750
|
-
const manifestPath =
|
|
4101
|
+
const manifestPath = join14(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
3751
4102
|
if (ctx.dryRun) {
|
|
3752
4103
|
return {
|
|
3753
4104
|
id: finding.id,
|
|
@@ -3794,11 +4145,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3794
4145
|
}
|
|
3795
4146
|
const details = [];
|
|
3796
4147
|
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"]) {
|
|
3797
|
-
if (!
|
|
4148
|
+
if (!existsSync10(join14(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join14(role.roleDir, rel))}`);
|
|
3798
4149
|
}
|
|
3799
|
-
const gitmodules = safeReadText(
|
|
4150
|
+
const gitmodules = safeReadText(join14(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3800
4151
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3801
|
-
if (!profileMetaInheritsDefault(
|
|
4152
|
+
if (!profileMetaInheritsDefault(join14(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3802
4153
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3803
4154
|
}
|
|
3804
4155
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3819,21 +4170,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3819
4170
|
if (!role) {
|
|
3820
4171
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3821
4172
|
}
|
|
3822
|
-
const templateRoleDir =
|
|
3823
|
-
writeIfDifferent(
|
|
3824
|
-
writeIfDifferent(
|
|
3825
|
-
writeIfDifferent(
|
|
3826
|
-
copyMissingRecursive(
|
|
3827
|
-
copyMissingRecursive(
|
|
3828
|
-
copyMissingRecursive(
|
|
3829
|
-
const promptSource =
|
|
3830
|
-
const promptTarget =
|
|
3831
|
-
if (
|
|
4173
|
+
const templateRoleDir = join14(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
4174
|
+
writeIfDifferent(join14(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
4175
|
+
writeIfDifferent(join14(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
4176
|
+
writeIfDifferent(join14(role.roleDir, ".gitignore"), readText(join14(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
4177
|
+
copyMissingRecursive(join14(templateRoleDir, ".runtime-scaffold"), join14(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
4178
|
+
copyMissingRecursive(join14(templateRoleDir, ".runtime-scaffold"), join14(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
4179
|
+
copyMissingRecursive(join14(templateRoleDir, ".scripts"), join14(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
4180
|
+
const promptSource = join14(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
4181
|
+
const promptTarget = join14(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
4182
|
+
if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
|
|
3832
4183
|
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);
|
|
3833
4184
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3834
4185
|
}
|
|
3835
4186
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3836
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
4187
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join14(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3837
4188
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3838
4189
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3839
4190
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3865,7 +4216,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3865
4216
|
const details = [];
|
|
3866
4217
|
for (const role of roles) {
|
|
3867
4218
|
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3868
|
-
const runtimeRelPath =
|
|
4219
|
+
const runtimeRelPath = join14(roleRelDir, "runtime");
|
|
3869
4220
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3870
4221
|
cwd: ctx.repoRoot,
|
|
3871
4222
|
encoding: "utf8"
|
|
@@ -3873,8 +4224,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3873
4224
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3874
4225
|
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3875
4226
|
}
|
|
3876
|
-
const gitignorePath =
|
|
3877
|
-
if (
|
|
4227
|
+
const gitignorePath = join14(role.roleDir, ".gitignore");
|
|
4228
|
+
if (existsSync10(gitignorePath)) {
|
|
3878
4229
|
const content = safeReadText(gitignorePath) ?? "";
|
|
3879
4230
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3880
4231
|
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
@@ -3899,7 +4250,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3899
4250
|
const details = [];
|
|
3900
4251
|
for (const role of roles) {
|
|
3901
4252
|
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3902
|
-
const runtimeRelPath =
|
|
4253
|
+
const runtimeRelPath = join14(roleRelDir, "runtime");
|
|
3903
4254
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3904
4255
|
cwd: ctx.repoRoot,
|
|
3905
4256
|
encoding: "utf8"
|
|
@@ -3914,10 +4265,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3914
4265
|
});
|
|
3915
4266
|
}
|
|
3916
4267
|
}
|
|
3917
|
-
const gitignorePath =
|
|
4268
|
+
const gitignorePath = join14(role.roleDir, ".gitignore");
|
|
3918
4269
|
let content = "";
|
|
3919
4270
|
let isIgnored = false;
|
|
3920
|
-
if (
|
|
4271
|
+
if (existsSync10(gitignorePath)) {
|
|
3921
4272
|
content = safeReadText(gitignorePath) ?? "";
|
|
3922
4273
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3923
4274
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
@@ -3984,9 +4335,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3984
4335
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3985
4336
|
}
|
|
3986
4337
|
for (const role of roles) {
|
|
3987
|
-
const sysDir =
|
|
4338
|
+
const sysDir = join14(ctx.homeDir, ".config", "systemd", "user");
|
|
3988
4339
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3989
|
-
const allUnitsPresent = units.every((unit) =>
|
|
4340
|
+
const allUnitsPresent = units.every((unit) => existsSync10(join14(sysDir, unit)));
|
|
3990
4341
|
if (allUnitsPresent) {
|
|
3991
4342
|
if (ctx.dryRun) {
|
|
3992
4343
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3998,8 +4349,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3998
4349
|
}
|
|
3999
4350
|
continue;
|
|
4000
4351
|
}
|
|
4001
|
-
for (const script of [
|
|
4002
|
-
if (!script || !
|
|
4352
|
+
for (const script of [join14(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
4353
|
+
if (!script || !existsSync10(script)) continue;
|
|
4003
4354
|
if (ctx.dryRun) {
|
|
4004
4355
|
details.push(`would run: bash ${script}`);
|
|
4005
4356
|
} else {
|
|
@@ -4026,7 +4377,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
4026
4377
|
changedFiles.push(path);
|
|
4027
4378
|
if (!dryRun) {
|
|
4028
4379
|
writeText(path, normalized);
|
|
4029
|
-
if (mode)
|
|
4380
|
+
if (mode) chmodSync4(path, mode);
|
|
4030
4381
|
}
|
|
4031
4382
|
}
|
|
4032
4383
|
function getParityRuleIds() {
|
|
@@ -4123,7 +4474,7 @@ var server = new McpServer({
|
|
|
4123
4474
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
4124
4475
|
function resolveTargetDir(targetDir) {
|
|
4125
4476
|
const dir = resolve3(targetDir ?? process.cwd());
|
|
4126
|
-
if (!
|
|
4477
|
+
if (!existsSync11(dir)) {
|
|
4127
4478
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
4128
4479
|
}
|
|
4129
4480
|
if (!statSync2(dir).isDirectory()) {
|
|
@@ -4134,7 +4485,7 @@ function resolveTargetDir(targetDir) {
|
|
|
4134
4485
|
function resolvePjanglerRoot3() {
|
|
4135
4486
|
let dir = dirname8(fileURLToPath5(import.meta.url));
|
|
4136
4487
|
while (dir !== dirname8(dir)) {
|
|
4137
|
-
if (
|
|
4488
|
+
if (existsSync11(join15(dir, "package.json")) && existsSync11(join15(dir, "templates", "commonproject", "copier.yml"))) {
|
|
4138
4489
|
return dir;
|
|
4139
4490
|
}
|
|
4140
4491
|
dir = dirname8(dir);
|
|
@@ -4336,8 +4687,8 @@ server.registerTool(
|
|
|
4336
4687
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
4337
4688
|
const projectSlug = input.projectSlug ?? slugify(input.projectName);
|
|
4338
4689
|
const parentDir = resolve3(input.parentDir ?? process.cwd());
|
|
4339
|
-
if (!
|
|
4340
|
-
const targetDir = resolve3(input.targetDir ??
|
|
4690
|
+
if (!existsSync11(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
4691
|
+
const targetDir = resolve3(input.targetDir ?? join15(parentDir, projectSlug));
|
|
4341
4692
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
4342
4693
|
const dryRun = input.dryRun ?? true;
|
|
4343
4694
|
const local = input.local ?? true;
|
|
@@ -4347,7 +4698,7 @@ server.registerTool(
|
|
|
4347
4698
|
if (!skipPlane && ticketProvider === "plane" && !boardId) {
|
|
4348
4699
|
throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
|
|
4349
4700
|
}
|
|
4350
|
-
if (!dryRun &&
|
|
4701
|
+
if (!dryRun && existsSync11(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
4351
4702
|
const plan = planProjectInit({
|
|
4352
4703
|
name: input.projectName,
|
|
4353
4704
|
description: input.projectDescription,
|