@delorenj/pjangler 1.2.19 → 1.2.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2353 -518
- package/dist/mcp-server.js +2331 -494
- package/package.json +14 -3
- package/templates/commonproject/copier.yml +6 -4
- package/templates/commonproject/template/.agents/hooks/README.md +13 -26
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
- package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
- package/templates/commonproject/template/.agents/local.example.json +2 -8
- package/templates/commonproject/template/.agents/skills.json +6 -0
- package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
- package/templates/commonproject/template/mise.toml.jinja +16 -16
- package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
- package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
- package/templates/hermes-agent/README.md +70 -29
- package/templates/hermes-agent/copier.yml +9 -6
- package/templates/hermes-agent/docs/architecture.md +75 -51
- package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
- package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
- package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
- package/templates/hermes-agent/docs/operations.md +179 -81
- package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
- package/templates/hermes-agent/docs/sentinel/README.md +10 -10
- package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
- package/templates/hermes-agent/docs/sentinel/development.md +12 -15
- package/templates/hermes-agent/docs/sentinel.md +1 -1
- package/templates/hermes-agent/install-local.sh +41 -4
- package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
- package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
- package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
- package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
- package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
- package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
- package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
- package/templates/hermes-agent/template/.gitignore.jinja +5 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
- package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
- package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
- package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
- package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
- package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
- package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +12 -1
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +21 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +36 -15
- package/templates/hermes-agent/template/hermes.jinja +6 -3
- package/templates/hermes-agent/template/role.yaml.jinja +24 -13
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
- package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
- package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
- package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
- package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
- package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
- package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
- package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
- package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
- package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
- package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
- package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
- package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +0 -181
package/dist/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
|
|
4
|
+
import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
|
|
5
|
+
import { basename as basename5, dirname as dirname8, join as join16, resolve as resolve4 } 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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
361
|
+
|
|
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) ))
|
|
366
|
+
|
|
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"
|
|
312
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
|
|
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 sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
525
|
+
return `${head}${sep2}
|
|
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,166 @@ 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 join6, dirname as dirname2 } from "node:path";
|
|
796
|
+
|
|
797
|
+
// src/parity/bmadPack.ts
|
|
798
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, readdirSync } from "node:fs";
|
|
799
|
+
import { createHash } from "node:crypto";
|
|
800
|
+
import { basename, join as join5, relative, resolve, sep } from "node:path";
|
|
801
|
+
var BMAD_PACK_VERSION = "6.10.1-next.31";
|
|
802
|
+
var BMAD_PACK_CHECKSUMS_SHA256 = "a8bc005612ac60e3ec775fff5a11eafe38be6acdae96efa3d770b48322cb3224";
|
|
803
|
+
var BMAD_PACK_SKILL_COUNT = 76;
|
|
804
|
+
var BMAD_PACK_PAYLOAD_FILES = 1072;
|
|
805
|
+
function sha256(content) {
|
|
806
|
+
return createHash("sha256").update(content).digest("hex");
|
|
807
|
+
}
|
|
808
|
+
function readRegularFile(path) {
|
|
809
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
810
|
+
try {
|
|
811
|
+
if (!fstatSync(fd).isFile()) throw new Error(`BMAD pack entry is not a regular file: ${path}`);
|
|
812
|
+
return readFileSync2(fd);
|
|
813
|
+
} finally {
|
|
814
|
+
closeSync(fd);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function safeRelativePath(value) {
|
|
818
|
+
if (!value || value.includes("\\") || value.startsWith("/") || value.split("/").some((part) => part === "" || part === "." || part === "..")) {
|
|
819
|
+
throw new Error(`Unsafe BMAD checksum path: ${JSON.stringify(value)}`);
|
|
820
|
+
}
|
|
821
|
+
return value;
|
|
822
|
+
}
|
|
823
|
+
function parsePackToml(content) {
|
|
824
|
+
const required = [
|
|
825
|
+
/^name = "bmad"$/m,
|
|
826
|
+
new RegExp(`^version = "${BMAD_PACK_VERSION.replaceAll(".", "\\.")}"$`, "m"),
|
|
827
|
+
/^upstream = "bmad-method"$/m,
|
|
828
|
+
new RegExp(`^upstream_version = "${BMAD_PACK_VERSION.replaceAll(".", "\\.")}"$`, "m"),
|
|
829
|
+
/^rendered_from = "\.agent\/skills"$/m,
|
|
830
|
+
/^immutable = true$/m,
|
|
831
|
+
/^project_projection = "symlink"$/m
|
|
832
|
+
];
|
|
833
|
+
if (required.some((pattern) => !pattern.test(content))) {
|
|
834
|
+
throw new Error(`BMAD pack.toml does not declare the trusted ${BMAD_PACK_VERSION} contract`);
|
|
835
|
+
}
|
|
836
|
+
const payloadMatch = content.match(/^payload_files = (\d+)$/m);
|
|
837
|
+
const skillsMatch = content.match(/^skills = \[\n([\s\S]*?)^\]$/m);
|
|
838
|
+
if (!payloadMatch || !skillsMatch) throw new Error("BMAD pack.toml is missing payload inventory metadata");
|
|
839
|
+
const skillNames = [...skillsMatch[1].matchAll(/^\s+"([^"]+)",$/gm)].map((match) => match[1]);
|
|
840
|
+
const payloadFiles = Number.parseInt(payloadMatch[1], 10);
|
|
841
|
+
if (skillNames.length !== BMAD_PACK_SKILL_COUNT || new Set(skillNames).size !== skillNames.length) {
|
|
842
|
+
throw new Error(`BMAD pack.toml must declare exactly ${BMAD_PACK_SKILL_COUNT} unique skills`);
|
|
843
|
+
}
|
|
844
|
+
if (payloadFiles !== BMAD_PACK_PAYLOAD_FILES) {
|
|
845
|
+
throw new Error(`BMAD pack.toml must declare exactly ${BMAD_PACK_PAYLOAD_FILES} payload files`);
|
|
846
|
+
}
|
|
847
|
+
for (const name of skillNames) {
|
|
848
|
+
if (!name.startsWith("bmad-") || basename(name) !== name) throw new Error(`Unsafe BMAD skill identity: ${name}`);
|
|
849
|
+
}
|
|
850
|
+
return { skillNames, payloadFiles };
|
|
851
|
+
}
|
|
852
|
+
function walkRegularTree(root) {
|
|
853
|
+
const files = /* @__PURE__ */ new Map();
|
|
854
|
+
const directories = /* @__PURE__ */ new Set();
|
|
855
|
+
const visit = (directory) => {
|
|
856
|
+
for (const name of readdirSync(directory).sort()) {
|
|
857
|
+
const path = join5(directory, name);
|
|
858
|
+
const stat = lstatSync(path);
|
|
859
|
+
if (stat.isSymbolicLink()) throw new Error(`BMAD pack may not contain symlinks: ${path}`);
|
|
860
|
+
if (stat.isDirectory()) {
|
|
861
|
+
directories.add(relative(root, path).split(sep).join("/"));
|
|
862
|
+
visit(path);
|
|
863
|
+
} else if (stat.isFile()) files.set(relative(root, path).split(sep).join("/"), readRegularFile(path));
|
|
864
|
+
else throw new Error(`BMAD pack may contain only regular files/directories: ${path}`);
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
visit(root);
|
|
868
|
+
return { files, directories };
|
|
869
|
+
}
|
|
870
|
+
function validateTrustedBmadPack(packRoot) {
|
|
871
|
+
const root = resolve(packRoot);
|
|
872
|
+
const rootStat = lstatSync(root);
|
|
873
|
+
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
874
|
+
throw new Error(`BMAD pack root must be a real directory: ${root}`);
|
|
875
|
+
}
|
|
876
|
+
const checksumsContent = readRegularFile(join5(root, "SHA256SUMS"));
|
|
877
|
+
if (sha256(checksumsContent) !== BMAD_PACK_CHECKSUMS_SHA256) {
|
|
878
|
+
throw new Error(`BMAD pack checksum manifest is not the trusted ${BMAD_PACK_VERSION} manifest`);
|
|
879
|
+
}
|
|
880
|
+
const expected = /* @__PURE__ */ new Map();
|
|
881
|
+
for (const line of checksumsContent.toString("utf8").split("\n")) {
|
|
882
|
+
if (!line) continue;
|
|
883
|
+
const match = line.match(/^([0-9a-f]{64}) (.+)$/);
|
|
884
|
+
if (!match) throw new Error(`Invalid BMAD SHA256SUMS entry: ${line}`);
|
|
885
|
+
const path = safeRelativePath(match[2]);
|
|
886
|
+
if (expected.has(path)) throw new Error(`Duplicate BMAD SHA256SUMS entry: ${path}`);
|
|
887
|
+
expected.set(path, match[1]);
|
|
888
|
+
}
|
|
889
|
+
const walked = walkRegularTree(root);
|
|
890
|
+
const actual = walked.files;
|
|
891
|
+
actual.delete("SHA256SUMS");
|
|
892
|
+
const missing = [...expected.keys()].filter((path) => !actual.has(path));
|
|
893
|
+
const extra = [...actual.keys()].filter((path) => !expected.has(path));
|
|
894
|
+
if (missing.length || extra.length) {
|
|
895
|
+
throw new Error(`BMAD checksum coverage mismatch; missing=${JSON.stringify(missing)} extra=${JSON.stringify(extra)}`);
|
|
896
|
+
}
|
|
897
|
+
for (const [path, digest] of expected) {
|
|
898
|
+
const content = actual.get(path);
|
|
899
|
+
if (!content || sha256(content) !== digest) throw new Error(`BMAD pack digest mismatch: ${path}`);
|
|
900
|
+
}
|
|
901
|
+
for (const directory of walked.directories) {
|
|
902
|
+
if (![...actual.keys()].some((path) => path.startsWith(`${directory}/`))) {
|
|
903
|
+
throw new Error(`BMAD pack contains an unauthenticated empty directory: ${directory}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const packToml = actual.get("pack.toml");
|
|
907
|
+
if (!packToml) throw new Error("BMAD pack.toml is missing");
|
|
908
|
+
const metadata = parsePackToml(packToml.toString("utf8"));
|
|
909
|
+
const skillSet = new Set(metadata.skillNames);
|
|
910
|
+
const topLevelDirectories = readdirSync(root).filter((name) => lstatSync(join5(root, name)).isDirectory()).sort();
|
|
911
|
+
if (topLevelDirectories.length !== skillSet.size || topLevelDirectories.some((name) => !skillSet.has(name))) {
|
|
912
|
+
throw new Error("BMAD pack directory inventory differs from authenticated pack.toml skills");
|
|
913
|
+
}
|
|
914
|
+
for (const name of metadata.skillNames) {
|
|
915
|
+
const skillMd = join5(root, name, "SKILL.md");
|
|
916
|
+
if (!lstatSync(skillMd).isFile() || lstatSync(skillMd).isSymbolicLink()) {
|
|
917
|
+
throw new Error(`BMAD skill is missing a regular SKILL.md: ${name}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const payloadCount = [...actual.keys()].filter((path) => skillSet.has(path.split("/", 1)[0])).length;
|
|
921
|
+
if (payloadCount !== metadata.payloadFiles) {
|
|
922
|
+
throw new Error(`BMAD payload inventory mismatch: ${payloadCount} != ${metadata.payloadFiles}`);
|
|
923
|
+
}
|
|
924
|
+
return { root, skillNames: metadata.skillNames, payloadFiles: metadata.payloadFiles };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
609
928
|
function resolveTemplateConfigPath() {
|
|
610
929
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
611
930
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
612
931
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
613
|
-
const base = xdg && xdg.length ? xdg :
|
|
614
|
-
return
|
|
932
|
+
const base = xdg && xdg.length ? xdg : join6(homedir(), ".config");
|
|
933
|
+
return join6(base, "hermes-agent-template", "config.toml");
|
|
615
934
|
}
|
|
616
935
|
function detectHermesBin(home) {
|
|
617
936
|
const candidates = [
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
937
|
+
join6(home, "code", "hermes-agent", "venv", "bin", "hermes"),
|
|
938
|
+
join6(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
|
|
939
|
+
join6(home, ".local", "bin", "hermes")
|
|
621
940
|
];
|
|
622
941
|
for (const c of candidates) {
|
|
623
|
-
if (
|
|
942
|
+
if (existsSync3(c)) return c;
|
|
624
943
|
}
|
|
625
944
|
return candidates[0];
|
|
626
945
|
}
|
|
627
946
|
function renderHostConfig() {
|
|
628
947
|
const home = homedir();
|
|
629
948
|
const hermesBin = detectHermesBin(home);
|
|
630
|
-
const hermesRepo =
|
|
631
|
-
const scaffoldDir =
|
|
632
|
-
const skillsDir =
|
|
633
|
-
const pmExternalSkillGlobalDir =
|
|
634
|
-
const pmExternalSkillBmadDir =
|
|
949
|
+
const hermesRepo = join6(home, "code", "hermes-agent");
|
|
950
|
+
const scaffoldDir = join6(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
951
|
+
const skillsDir = join6(home, ".agents", "skills");
|
|
952
|
+
const pmExternalSkillGlobalDir = join6(home, "code", "skillex", "skill-sets", "global", ".system");
|
|
953
|
+
const pmExternalSkillBmadDir = join6(home, "code", "skillex", "packs", "bmad", BMAD_PACK_VERSION);
|
|
635
954
|
return `# hermes-agent-template \u2014 host configuration
|
|
636
955
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
637
956
|
#
|
|
@@ -680,7 +999,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
680
999
|
const ctx = this.context;
|
|
681
1000
|
const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
|
|
682
1001
|
const path = resolveTemplateConfigPath();
|
|
683
|
-
const exists =
|
|
1002
|
+
const exists = existsSync3(path);
|
|
684
1003
|
if (exists && !force) {
|
|
685
1004
|
console.log(`\u2713 Config present: ${path}`);
|
|
686
1005
|
return { success: true, message: "" };
|
|
@@ -691,7 +1010,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
691
1010
|
}
|
|
692
1011
|
try {
|
|
693
1012
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
694
|
-
|
|
1013
|
+
writeFileSync3(path, renderHostConfig());
|
|
695
1014
|
} catch (err) {
|
|
696
1015
|
const msg = err instanceof Error ? err.message : String(err);
|
|
697
1016
|
return { success: false, message: `\u2717 Failed to write ${path}: ${msg}` };
|
|
@@ -703,8 +1022,8 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
703
1022
|
};
|
|
704
1023
|
|
|
705
1024
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
706
|
-
import { basename, join as
|
|
707
|
-
import { readFileSync } from "node:fs";
|
|
1025
|
+
import { basename as basename2, join as join7 } from "node:path";
|
|
1026
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
708
1027
|
import * as p from "@clack/prompts";
|
|
709
1028
|
|
|
710
1029
|
// src/commands/hermes/types.ts
|
|
@@ -719,7 +1038,7 @@ function deriveProfileName(repo, role) {
|
|
|
719
1038
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
720
1039
|
function detectTicketProvider(targetDir) {
|
|
721
1040
|
try {
|
|
722
|
-
const t = JSON.parse(
|
|
1041
|
+
const t = JSON.parse(readFileSync3(join7(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
723
1042
|
return t === "plane" || t === "trello" ? t : void 0;
|
|
724
1043
|
} catch {
|
|
725
1044
|
return void 0;
|
|
@@ -728,7 +1047,7 @@ function detectTicketProvider(targetDir) {
|
|
|
728
1047
|
var PromptForAgentConfig = class extends Command {
|
|
729
1048
|
async invoke() {
|
|
730
1049
|
const ctx = this.context;
|
|
731
|
-
const defaultRepo =
|
|
1050
|
+
const defaultRepo = basename2(ctx.targetDir).toLowerCase();
|
|
732
1051
|
ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
|
|
733
1052
|
ctx.role ??= "pm";
|
|
734
1053
|
ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
|
|
@@ -777,8 +1096,8 @@ var PromptForAgentConfig = class extends Command {
|
|
|
777
1096
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
778
1097
|
import { spawnSync } from "node:child_process";
|
|
779
1098
|
import { homedir as homedir2 } from "node:os";
|
|
780
|
-
import { join as
|
|
781
|
-
import { existsSync as
|
|
1099
|
+
import { join as join8, dirname as dirname3 } from "node:path";
|
|
1100
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
782
1101
|
import { fileURLToPath } from "node:url";
|
|
783
1102
|
import * as p2 from "@clack/prompts";
|
|
784
1103
|
function resolveVendoredTemplate(name) {
|
|
@@ -789,8 +1108,8 @@ function resolveVendoredTemplate(name) {
|
|
|
789
1108
|
return void 0;
|
|
790
1109
|
}
|
|
791
1110
|
for (let i = 0; i < 8; i++) {
|
|
792
|
-
const candidate =
|
|
793
|
-
if (
|
|
1111
|
+
const candidate = join8(dir, "templates", name);
|
|
1112
|
+
if (existsSync4(join8(candidate, "copier.yml"))) return candidate;
|
|
794
1113
|
const parent = dirname3(dir);
|
|
795
1114
|
if (parent === dir) break;
|
|
796
1115
|
dir = parent;
|
|
@@ -809,7 +1128,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
809
1128
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
810
1129
|
};
|
|
811
1130
|
}
|
|
812
|
-
const roleDir =
|
|
1131
|
+
const roleDir = join8(ctx.targetDir, "agents", "hermes", role);
|
|
813
1132
|
ctx.roleDir = roleDir;
|
|
814
1133
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
815
1134
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -819,7 +1138,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
819
1138
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
820
1139
|
};
|
|
821
1140
|
}
|
|
822
|
-
if (
|
|
1141
|
+
if (existsSync4(join8(roleDir, "role.yaml")) && !ctx.force) {
|
|
823
1142
|
if (ctx.yes) {
|
|
824
1143
|
ctx.force = true;
|
|
825
1144
|
} else {
|
|
@@ -840,15 +1159,16 @@ var RunCopierTemplate = class extends Command {
|
|
|
840
1159
|
...process.env,
|
|
841
1160
|
SKIP_TELEGRAM: "1",
|
|
842
1161
|
SKIP_EMAIL: "1",
|
|
843
|
-
//
|
|
1162
|
+
// Bloodbank is a fleet-shared Hermes gateway. Never provision the legacy
|
|
1163
|
+
// per-profile file consumer, even when an older template still exposes it.
|
|
844
1164
|
SKIP_RUNTIME_REPO: ctx.skipRuntimeRepo ? "1" : "0",
|
|
845
1165
|
SKIP_PLANE: ctx.skipPlane ? "1" : "0",
|
|
846
|
-
SKIP_BLOODBANK:
|
|
1166
|
+
SKIP_BLOODBANK: "1",
|
|
847
1167
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
848
1168
|
};
|
|
849
|
-
const LOCAL_TEMPLATE =
|
|
1169
|
+
const LOCAL_TEMPLATE = join8(homedir2(), "code", "hermes-agent-template");
|
|
850
1170
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
851
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (
|
|
1171
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join8(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
852
1172
|
const args = [
|
|
853
1173
|
"copy",
|
|
854
1174
|
templateSrc,
|
|
@@ -879,7 +1199,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
879
1199
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
880
1200
|
};
|
|
881
1201
|
}
|
|
882
|
-
mkdirSync3(
|
|
1202
|
+
mkdirSync3(join8(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
883
1203
|
const spinner4 = p2.spinner();
|
|
884
1204
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
885
1205
|
const result = spawnSync("copier", args, {
|
|
@@ -903,20 +1223,26 @@ var RunCopierTemplate = class extends Command {
|
|
|
903
1223
|
};
|
|
904
1224
|
|
|
905
1225
|
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
906
|
-
import { existsSync as
|
|
907
|
-
import { join as
|
|
1226
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
1227
|
+
import { join as join9 } from "path";
|
|
908
1228
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1229
|
+
function sectionHasPath(section, targetPath) {
|
|
1230
|
+
return section.split(/\r?\n/).some((line) => /^\s*path\s*=/.test(line) && line.replace(/^\s*path\s*=\s*/, "").trim() === targetPath);
|
|
1231
|
+
}
|
|
1232
|
+
function removeSubmodulePath(content, targetPath) {
|
|
1233
|
+
return content.replace(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm, (section) => sectionHasPath(section, targetPath) ? "" : section).replace(/\n{3,}/g, "\n\n").trim();
|
|
1234
|
+
}
|
|
909
1235
|
var UntrackHermesRuntimes = class extends Command {
|
|
910
1236
|
async invoke() {
|
|
911
1237
|
const targetDir = this.context.targetDir;
|
|
912
|
-
const rolesDir =
|
|
913
|
-
if (!
|
|
1238
|
+
const rolesDir = join9(targetDir, "agents", "hermes");
|
|
1239
|
+
if (!existsSync5(rolesDir)) {
|
|
914
1240
|
return {
|
|
915
1241
|
success: true,
|
|
916
1242
|
message: "No Hermes agents found (no agents/hermes directory)."
|
|
917
1243
|
};
|
|
918
1244
|
}
|
|
919
|
-
const roles =
|
|
1245
|
+
const roles = readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
920
1246
|
if (roles.length === 0) {
|
|
921
1247
|
return {
|
|
922
1248
|
success: true,
|
|
@@ -926,53 +1252,85 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
926
1252
|
let modifiedAny = false;
|
|
927
1253
|
const details = [];
|
|
928
1254
|
for (const role of roles) {
|
|
929
|
-
const roleDir =
|
|
930
|
-
const runtimePath =
|
|
931
|
-
const gitignorePath =
|
|
1255
|
+
const roleDir = join9("agents", "hermes", role);
|
|
1256
|
+
const runtimePath = join9(roleDir, "runtime");
|
|
1257
|
+
const gitignorePath = join9(roleDir, ".gitignore");
|
|
1258
|
+
const gitmodulesPath = join9(targetDir, ".gitmodules");
|
|
932
1259
|
let isTracked = false;
|
|
933
|
-
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
1260
|
+
const lsResult = spawnSync2("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
934
1261
|
cwd: targetDir,
|
|
935
1262
|
encoding: "utf8"
|
|
936
1263
|
});
|
|
1264
|
+
if (lsResult.status !== 0) {
|
|
1265
|
+
return {
|
|
1266
|
+
success: false,
|
|
1267
|
+
message: `\u2717 Failed to inspect runtime index at ${runtimePath}: ${lsResult.stderr.trim() || `exit ${lsResult.status}`}`
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
937
1270
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
938
1271
|
isTracked = true;
|
|
939
1272
|
}
|
|
1273
|
+
let hasStaleMapping = false;
|
|
1274
|
+
let gitmodulesContent = "";
|
|
1275
|
+
if (existsSync5(gitmodulesPath)) {
|
|
1276
|
+
gitmodulesContent = readFileSync4(gitmodulesPath, "utf8");
|
|
1277
|
+
const sections = gitmodulesContent.match(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm) ?? [];
|
|
1278
|
+
hasStaleMapping = sections.some((section) => sectionHasPath(section, runtimePath));
|
|
1279
|
+
}
|
|
940
1280
|
let isIgnored = false;
|
|
941
|
-
const fullGitignorePath =
|
|
942
|
-
if (
|
|
943
|
-
const content =
|
|
1281
|
+
const fullGitignorePath = join9(targetDir, gitignorePath);
|
|
1282
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1283
|
+
const content = readFileSync4(fullGitignorePath, "utf8");
|
|
944
1284
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
945
1285
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
946
1286
|
}
|
|
947
|
-
if (isTracked || !isIgnored) {
|
|
1287
|
+
if (isTracked || hasStaleMapping || !isIgnored) {
|
|
948
1288
|
modifiedAny = true;
|
|
949
1289
|
if (isTracked) {
|
|
950
1290
|
details.push(`untrack agents/hermes/${role}/runtime`);
|
|
951
1291
|
if (!this.context.dryRun) {
|
|
952
|
-
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", runtimePath], {
|
|
1292
|
+
const rmResult = spawnSync2("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
|
|
953
1293
|
cwd: targetDir,
|
|
954
1294
|
encoding: "utf8"
|
|
955
1295
|
});
|
|
956
1296
|
if (rmResult.status !== 0) {
|
|
957
1297
|
return {
|
|
958
1298
|
success: false,
|
|
959
|
-
message:
|
|
1299
|
+
message: `\u2717 Failed to untrack ${runtimePath}: ${rmResult.stderr.trim() || `exit ${rmResult.status}`}`
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
const verifyResult = spawnSync2("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
1303
|
+
cwd: targetDir,
|
|
1304
|
+
encoding: "utf8"
|
|
1305
|
+
});
|
|
1306
|
+
if (verifyResult.status !== 0 || verifyResult.stdout.trim()) {
|
|
1307
|
+
return {
|
|
1308
|
+
success: false,
|
|
1309
|
+
message: verifyResult.status !== 0 ? `\u2717 Failed to verify untracked runtime ${runtimePath}: ${verifyResult.stderr.trim() || `exit ${verifyResult.status}`}` : `\u2717 Runtime remains tracked after index-only removal: ${runtimePath}`
|
|
960
1310
|
};
|
|
961
1311
|
}
|
|
962
1312
|
}
|
|
963
1313
|
}
|
|
1314
|
+
if (hasStaleMapping) {
|
|
1315
|
+
details.push(`remove stale .gitmodules mapping for ${runtimePath}`);
|
|
1316
|
+
if (!this.context.dryRun) {
|
|
1317
|
+
const next = removeSubmodulePath(gitmodulesContent, runtimePath);
|
|
1318
|
+
writeFileSync4(gitmodulesPath, next ? `${next}
|
|
1319
|
+
` : "", "utf8");
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
964
1322
|
if (!isIgnored) {
|
|
965
1323
|
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
966
1324
|
if (!this.context.dryRun) {
|
|
967
1325
|
let content = "";
|
|
968
|
-
if (
|
|
969
|
-
content =
|
|
1326
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1327
|
+
content = readFileSync4(fullGitignorePath, "utf8");
|
|
970
1328
|
}
|
|
971
1329
|
if (content && !content.endsWith("\n")) {
|
|
972
1330
|
content += "\n";
|
|
973
1331
|
}
|
|
974
1332
|
content += "runtime/\n";
|
|
975
|
-
|
|
1333
|
+
writeFileSync4(fullGitignorePath, content, "utf8");
|
|
976
1334
|
}
|
|
977
1335
|
}
|
|
978
1336
|
}
|
|
@@ -994,8 +1352,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
|
|
|
994
1352
|
|
|
995
1353
|
// src/commands/hermes/WireTelegram.ts
|
|
996
1354
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
997
|
-
import { join as
|
|
998
|
-
import { existsSync as
|
|
1355
|
+
import { join as join10 } from "node:path";
|
|
1356
|
+
import { existsSync as existsSync6, unlinkSync } from "node:fs";
|
|
999
1357
|
import * as p3 from "@clack/prompts";
|
|
1000
1358
|
var WireTelegram = class extends Command {
|
|
1001
1359
|
async invoke() {
|
|
@@ -1083,15 +1441,15 @@ var WireTelegram = class extends Command {
|
|
|
1083
1441
|
if (p3.isCancel(allowedAnswer)) {
|
|
1084
1442
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1085
1443
|
}
|
|
1086
|
-
const script =
|
|
1087
|
-
if (!
|
|
1444
|
+
const script = join10(roleDir, ".scripts", "30-telegram.sh");
|
|
1445
|
+
if (!existsSync6(script)) {
|
|
1088
1446
|
return {
|
|
1089
1447
|
success: false,
|
|
1090
1448
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1091
1449
|
};
|
|
1092
1450
|
}
|
|
1093
|
-
const marker =
|
|
1094
|
-
if (
|
|
1451
|
+
const marker = join10(roleDir, ".scripts", ".done-30-telegram");
|
|
1452
|
+
if (existsSync6(marker)) unlinkSync(marker);
|
|
1095
1453
|
const spinner4 = p3.spinner();
|
|
1096
1454
|
spinner4.start("Verifying token + wiring profile");
|
|
1097
1455
|
const result = spawnSync3("bash", [script], {
|
|
@@ -1118,8 +1476,8 @@ function cap(s) {
|
|
|
1118
1476
|
|
|
1119
1477
|
// src/commands/hermes/WireEmail.ts
|
|
1120
1478
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1121
|
-
import { join as
|
|
1122
|
-
import { existsSync as
|
|
1479
|
+
import { join as join11 } from "node:path";
|
|
1480
|
+
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1123
1481
|
import * as p4 from "@clack/prompts";
|
|
1124
1482
|
var WireEmail = class extends Command {
|
|
1125
1483
|
async invoke() {
|
|
@@ -1134,8 +1492,8 @@ var WireEmail = class extends Command {
|
|
|
1134
1492
|
if (!targetRepo || !role || !roleDir) {
|
|
1135
1493
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1136
1494
|
}
|
|
1137
|
-
const script =
|
|
1138
|
-
if (!
|
|
1495
|
+
const script = join11(roleDir, ".scripts", "50-email.sh");
|
|
1496
|
+
if (!existsSync7(script)) {
|
|
1139
1497
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1140
1498
|
}
|
|
1141
1499
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
@@ -1197,8 +1555,8 @@ var WireEmail = class extends Command {
|
|
|
1197
1555
|
}
|
|
1198
1556
|
}
|
|
1199
1557
|
}
|
|
1200
|
-
const marker =
|
|
1201
|
-
if (
|
|
1558
|
+
const marker = join11(roleDir, ".scripts", ".done-50-email");
|
|
1559
|
+
if (existsSync7(marker)) unlinkSync2(marker);
|
|
1202
1560
|
const spinner4 = p4.spinner();
|
|
1203
1561
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1204
1562
|
const result = spawnSync4("bash", [script], {
|
|
@@ -1226,7 +1584,6 @@ var PrintHermesSummary = class extends Command {
|
|
|
1226
1584
|
const botHandle = `${targetRepo?.toLowerCase().replace(/-/g, "_")}_${role?.toLowerCase()}_bot`;
|
|
1227
1585
|
const email = `${targetRepo}-${role}@delo.sh`;
|
|
1228
1586
|
const gw = `hermes-${agentId}-gateway.service`;
|
|
1229
|
-
const csm = `hermes-${agentId}-consumer.service`;
|
|
1230
1587
|
const hb = `hermes-${agentId}-heartbeat.timer`;
|
|
1231
1588
|
const lines = [];
|
|
1232
1589
|
lines.push(`agent_id ${agentId}`);
|
|
@@ -1236,13 +1593,13 @@ var PrintHermesSummary = class extends Command {
|
|
|
1236
1593
|
if (!skipEmail) lines.push(`email ${email}`);
|
|
1237
1594
|
lines.push("");
|
|
1238
1595
|
lines.push("Start daemons:");
|
|
1239
|
-
lines.push(` systemctl --user start ${csm}`);
|
|
1240
1596
|
lines.push(` systemctl --user start ${hb}`);
|
|
1241
1597
|
if (!skipTelegram) {
|
|
1242
1598
|
lines.push(` systemctl --user start ${gw}`);
|
|
1243
1599
|
} else {
|
|
1244
1600
|
lines.push(` # gateway needs Telegram wired first (re-run with --skip-telegram=0)`);
|
|
1245
1601
|
}
|
|
1602
|
+
lines.push(" # Bloodbank commands arrive through the fleet-shared Hermes gateway");
|
|
1246
1603
|
lines.push("");
|
|
1247
1604
|
lines.push("Talk locally:");
|
|
1248
1605
|
lines.push(` ${ctx.roleDir}/hermes chat "status"`);
|
|
@@ -1285,15 +1642,15 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1285
1642
|
|
|
1286
1643
|
// src/commands/AgentHooksCommands.ts
|
|
1287
1644
|
import { homedir as homedir4 } from "node:os";
|
|
1288
|
-
import { join as
|
|
1289
|
-
import { existsSync as
|
|
1645
|
+
import { join as join13, dirname as dirname5 } from "node:path";
|
|
1646
|
+
import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1290
1647
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1291
1648
|
|
|
1292
1649
|
// src/project/index.ts
|
|
1293
1650
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1294
|
-
import { existsSync as
|
|
1651
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync5, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1295
1652
|
import { homedir as homedir3 } from "node:os";
|
|
1296
|
-
import { basename as
|
|
1653
|
+
import { basename as basename3, delimiter, dirname as dirname4, join as join12, resolve as resolve2 } from "node:path";
|
|
1297
1654
|
import YAML2 from "yaml";
|
|
1298
1655
|
|
|
1299
1656
|
// src/project/RegistryStore.ts
|
|
@@ -1528,18 +1885,18 @@ var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
|
1528
1885
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1529
1886
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1530
1887
|
"/home/delorenj/code/skillex/all-skills",
|
|
1531
|
-
|
|
1532
|
-
|
|
1888
|
+
join12(homedir3(), ".agents", "skills"),
|
|
1889
|
+
join12(homedir3(), ".codex", "skills")
|
|
1533
1890
|
];
|
|
1534
1891
|
function projectRegistryPath(env2 = process.env) {
|
|
1535
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1892
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join12(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1536
1893
|
}
|
|
1537
1894
|
function emptyProjectRegistry() {
|
|
1538
1895
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1539
1896
|
}
|
|
1540
1897
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1541
|
-
if (!
|
|
1542
|
-
const raw = YAML2.parse(
|
|
1898
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
1899
|
+
const raw = YAML2.parse(readFileSync5(path, "utf8"));
|
|
1543
1900
|
if (raw == null) return emptyProjectRegistry();
|
|
1544
1901
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1545
1902
|
const registry = raw;
|
|
@@ -1554,7 +1911,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1554
1911
|
validateProjectRegistry(registry);
|
|
1555
1912
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1556
1913
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1557
|
-
|
|
1914
|
+
writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1558
1915
|
renameSync(temp, path);
|
|
1559
1916
|
}
|
|
1560
1917
|
function validateProjectRegistry(registry) {
|
|
@@ -1569,7 +1926,7 @@ function validateProjectRegistry(registry) {
|
|
|
1569
1926
|
validateProjectRecord(project, slug);
|
|
1570
1927
|
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
1571
1928
|
slugs.add(project.slug);
|
|
1572
|
-
const repoKey =
|
|
1929
|
+
const repoKey = resolve2(project.repo_path);
|
|
1573
1930
|
const existingRepoSlug = repoPaths.get(repoKey);
|
|
1574
1931
|
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
1575
1932
|
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
@@ -1636,7 +1993,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1636
1993
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1637
1994
|
if (override === "0" || override === "false") return false;
|
|
1638
1995
|
if (override === "1" || override === "true") return true;
|
|
1639
|
-
return !
|
|
1996
|
+
return !existsSync8(join12(homedir3(), ".agents", "hooks"));
|
|
1640
1997
|
}
|
|
1641
1998
|
function jsonStable(value) {
|
|
1642
1999
|
return JSON.stringify(value);
|
|
@@ -1649,14 +2006,14 @@ function projectRecordEquivalent(a, b) {
|
|
|
1649
2006
|
}
|
|
1650
2007
|
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
1651
2008
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
1652
|
-
return
|
|
2009
|
+
return resolve2(dirname4(resolve2(cwd)), compactName);
|
|
1653
2010
|
}
|
|
1654
2011
|
function sourceSkillRoots(env2 = process.env) {
|
|
1655
2012
|
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter).map((root) => root.trim()).filter(Boolean);
|
|
1656
2013
|
const seen = /* @__PURE__ */ new Set();
|
|
1657
2014
|
const roots = [];
|
|
1658
2015
|
for (const root of [...DEFAULT_SOURCE_SKILL_ROOTS, ...configuredRoots]) {
|
|
1659
|
-
const normalized =
|
|
2016
|
+
const normalized = resolve2(expandHome(root));
|
|
1660
2017
|
if (seen.has(normalized)) continue;
|
|
1661
2018
|
seen.add(normalized);
|
|
1662
2019
|
roots.push(normalized);
|
|
@@ -1666,13 +2023,13 @@ function sourceSkillRoots(env2 = process.env) {
|
|
|
1666
2023
|
function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
1667
2024
|
if (!sourceSkill) return void 0;
|
|
1668
2025
|
const expanded = expandHome(sourceSkill);
|
|
1669
|
-
const direct =
|
|
1670
|
-
if (
|
|
1671
|
-
const name =
|
|
2026
|
+
const direct = resolve2(expanded);
|
|
2027
|
+
if (existsSync8(direct)) return direct;
|
|
2028
|
+
const name = basename3(sourceSkill);
|
|
1672
2029
|
const roots = sourceSkillRoots(env2);
|
|
1673
2030
|
for (const root of roots) {
|
|
1674
|
-
const candidate =
|
|
1675
|
-
if (
|
|
2031
|
+
const candidate = join12(root, name);
|
|
2032
|
+
if (existsSync8(candidate)) return candidate;
|
|
1676
2033
|
}
|
|
1677
2034
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1678
2035
|
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
@@ -1680,11 +2037,11 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1680
2037
|
}
|
|
1681
2038
|
function planProjectInit(input) {
|
|
1682
2039
|
if (!input.name.trim()) throw new Error("Project name is required");
|
|
1683
|
-
const registryPath2 =
|
|
2040
|
+
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
1684
2041
|
const registry = loadProjectRegistry(registryPath2);
|
|
1685
2042
|
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1686
2043
|
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
1687
|
-
const targetDir =
|
|
2044
|
+
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
1688
2045
|
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
1689
2046
|
const existing = registry.projects[slug];
|
|
1690
2047
|
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
@@ -1727,7 +2084,7 @@ function planProjectInit(input) {
|
|
|
1727
2084
|
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
1728
2085
|
};
|
|
1729
2086
|
validateNoDuplicateProject(registry, project, overwrite);
|
|
1730
|
-
const pjanglerRoot =
|
|
2087
|
+
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot());
|
|
1731
2088
|
const manifest = projectManifestFromRegistryProject(project);
|
|
1732
2089
|
const apply = input.apply ?? false;
|
|
1733
2090
|
const live = input.live ?? false;
|
|
@@ -1753,7 +2110,7 @@ function planProjectInit(input) {
|
|
|
1753
2110
|
}));
|
|
1754
2111
|
}
|
|
1755
2112
|
actions.push(
|
|
1756
|
-
{ kind: "project.write-manifest", path:
|
|
2113
|
+
{ kind: "project.write-manifest", path: join12(targetDir, ".project.json"), manifest },
|
|
1757
2114
|
{
|
|
1758
2115
|
kind: "ticket-provider.create-or-link",
|
|
1759
2116
|
enabled: live,
|
|
@@ -1806,7 +2163,7 @@ async function executeProjectInitPlan(plan) {
|
|
|
1806
2163
|
}
|
|
1807
2164
|
if (result.status !== 0) {
|
|
1808
2165
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1809
|
-
if (
|
|
2166
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1810
2167
|
break;
|
|
1811
2168
|
}
|
|
1812
2169
|
changedFiles.push(action.targetDir);
|
|
@@ -1814,9 +2171,9 @@ async function executeProjectInitPlan(plan) {
|
|
|
1814
2171
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1815
2172
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1816
2173
|
`;
|
|
1817
|
-
const current =
|
|
2174
|
+
const current = existsSync8(action.path) ? readFileSync5(action.path, "utf8") : void 0;
|
|
1818
2175
|
if (current !== next) {
|
|
1819
|
-
|
|
2176
|
+
writeFileSync5(action.path, next, "utf8");
|
|
1820
2177
|
changedFiles.push(action.path);
|
|
1821
2178
|
}
|
|
1822
2179
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1879,7 +2236,7 @@ function getProject(registry, slug) {
|
|
|
1879
2236
|
return project;
|
|
1880
2237
|
}
|
|
1881
2238
|
function buildCommonProjectCopierAction(input) {
|
|
1882
|
-
const templateDir =
|
|
2239
|
+
const templateDir = join12(input.pjanglerRoot, "templates", "commonproject");
|
|
1883
2240
|
const data = {
|
|
1884
2241
|
project_name: input.projectName,
|
|
1885
2242
|
project_description: input.projectDescription ?? "",
|
|
@@ -1908,19 +2265,19 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1908
2265
|
function resolvePjanglerRoot() {
|
|
1909
2266
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1910
2267
|
while (dir !== dirname4(dir)) {
|
|
1911
|
-
if (
|
|
2268
|
+
if (existsSync8(join12(dir, "package.json")) && existsSync8(join12(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1912
2269
|
dir = dirname4(dir);
|
|
1913
2270
|
}
|
|
1914
|
-
return
|
|
2271
|
+
return resolve2(process.cwd());
|
|
1915
2272
|
}
|
|
1916
2273
|
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
1917
2274
|
const existingSameSlug = registry.projects[project.slug];
|
|
1918
|
-
if (existingSameSlug && !overwrite &&
|
|
2275
|
+
if (existingSameSlug && !overwrite && resolve2(existingSameSlug.repo_path) !== resolve2(project.repo_path)) {
|
|
1919
2276
|
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
1920
2277
|
}
|
|
1921
2278
|
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
1922
2279
|
if (slug === project.slug) continue;
|
|
1923
|
-
if (
|
|
2280
|
+
if (resolve2(existing.repo_path) === resolve2(project.repo_path)) {
|
|
1924
2281
|
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
1925
2282
|
}
|
|
1926
2283
|
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
@@ -1940,7 +2297,7 @@ function validateProjectRecord(project, key) {
|
|
|
1940
2297
|
}
|
|
1941
2298
|
function expandHome(path) {
|
|
1942
2299
|
if (path === "~") return homedir3();
|
|
1943
|
-
if (path.startsWith("~/")) return
|
|
2300
|
+
if (path.startsWith("~/")) return join12(homedir3(), path.slice(2));
|
|
1944
2301
|
return path;
|
|
1945
2302
|
}
|
|
1946
2303
|
function isRecord(value) {
|
|
@@ -1957,16 +2314,16 @@ function resolveTemplateRoot() {
|
|
|
1957
2314
|
try {
|
|
1958
2315
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1959
2316
|
for (let i = 0; i < 8; i++) {
|
|
1960
|
-
candidates.push(
|
|
2317
|
+
candidates.push(join13(dir, "templates", "commonproject", "template"));
|
|
1961
2318
|
const parent = dirname5(dir);
|
|
1962
2319
|
if (parent === dir) break;
|
|
1963
2320
|
dir = parent;
|
|
1964
2321
|
}
|
|
1965
2322
|
} catch {
|
|
1966
2323
|
}
|
|
1967
|
-
candidates.push(
|
|
2324
|
+
candidates.push(join13(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1968
2325
|
for (const c of candidates) {
|
|
1969
|
-
if (
|
|
2326
|
+
if (existsSync9(join13(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1970
2327
|
}
|
|
1971
2328
|
throw new Error(
|
|
1972
2329
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1985,18 +2342,17 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1985
2342
|
}
|
|
1986
2343
|
const items = [
|
|
1987
2344
|
{ rel: ".agents/hooks", dir: true },
|
|
2345
|
+
{ rel: ".agents/skills.json", dir: false },
|
|
1988
2346
|
{ 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
2347
|
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
1992
2348
|
];
|
|
1993
2349
|
const created = [];
|
|
1994
2350
|
const skipped = [];
|
|
1995
2351
|
for (const { rel, dir } of items) {
|
|
1996
|
-
const src =
|
|
1997
|
-
const dest =
|
|
1998
|
-
if (!
|
|
1999
|
-
if (
|
|
2352
|
+
const src = join13(templateRoot, rel);
|
|
2353
|
+
const dest = join13(this.context.targetDir, rel);
|
|
2354
|
+
if (!existsSync9(src)) continue;
|
|
2355
|
+
if (existsSync9(dest) && !this.context.force) {
|
|
2000
2356
|
skipped.push(rel);
|
|
2001
2357
|
continue;
|
|
2002
2358
|
}
|
|
@@ -2022,25 +2378,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2022
2378
|
if (!resolveAgentHooksLayer()) {
|
|
2023
2379
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
2024
2380
|
}
|
|
2025
|
-
const misePath =
|
|
2026
|
-
if (!
|
|
2381
|
+
const misePath = join13(this.context.targetDir, "mise.toml");
|
|
2382
|
+
if (!existsSync9(misePath)) {
|
|
2027
2383
|
return {
|
|
2028
2384
|
success: false,
|
|
2029
2385
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
2030
2386
|
};
|
|
2031
2387
|
}
|
|
2032
|
-
let content =
|
|
2388
|
+
let content = readFileSync6(misePath, "utf8");
|
|
2033
2389
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
2034
2390
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
2035
2391
|
}
|
|
2036
2392
|
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");
|
|
2393
|
+
const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
|
|
2041
2394
|
const leaveBlock = [
|
|
2042
2395
|
"leave = [",
|
|
2043
|
-
` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
|
|
2044
2396
|
` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
|
|
2045
2397
|
"]"
|
|
2046
2398
|
].join("\n");
|
|
@@ -2048,16 +2400,15 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
2048
2400
|
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
2049
2401
|
if (enterRe.test(content)) {
|
|
2050
2402
|
content = content.replace(enterRe, (_m, head, close) => {
|
|
2051
|
-
const
|
|
2052
|
-
return `${head}${
|
|
2403
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2404
|
+
return `${head}${sep2}
|
|
2053
2405
|
${enterAdds}${close}`;
|
|
2054
2406
|
});
|
|
2055
2407
|
const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
2056
2408
|
if (leaveRe.test(content)) {
|
|
2057
2409
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
2058
|
-
const
|
|
2059
|
-
return `${head}${
|
|
2060
|
-
"${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
|
|
2410
|
+
const sep2 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
2411
|
+
return `${head}${sep2}
|
|
2061
2412
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
2062
2413
|
});
|
|
2063
2414
|
} else {
|
|
@@ -2070,6 +2421,14 @@ ${leaveBlock}`);
|
|
|
2070
2421
|
"",
|
|
2071
2422
|
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
2072
2423
|
"[[watch_files]]",
|
|
2424
|
+
'patterns = [".agents/skills.json"]',
|
|
2425
|
+
'task = "skills-sync"',
|
|
2426
|
+
"",
|
|
2427
|
+
"[tasks.skills-sync]",
|
|
2428
|
+
'description = "Sync skills from manifest to local CLI dirs"',
|
|
2429
|
+
'run = "sync-skills.py --scope project"',
|
|
2430
|
+
"",
|
|
2431
|
+
"[[watch_files]]",
|
|
2073
2432
|
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
2074
2433
|
'task = "hooks-sync"',
|
|
2075
2434
|
"",
|
|
@@ -2085,18 +2444,6 @@ ${leaveBlock}`);
|
|
|
2085
2444
|
'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
|
|
2086
2445
|
`run = "${cr}/.agents/hooks/sync.py --uninstall"`,
|
|
2087
2446
|
"",
|
|
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
2447
|
"[tasks.hindsight-setup]",
|
|
2101
2448
|
`description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
|
|
2102
2449
|
`run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
|
|
@@ -2105,7 +2452,7 @@ ${leaveBlock}`);
|
|
|
2105
2452
|
""
|
|
2106
2453
|
].join("\n");
|
|
2107
2454
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
2108
|
-
if (!this.context.dryRun)
|
|
2455
|
+
if (!this.context.dryRun) writeFileSync6(misePath, content);
|
|
2109
2456
|
if (wiredHooks) {
|
|
2110
2457
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
2111
2458
|
}
|
|
@@ -2114,8 +2461,8 @@ ${leaveBlock}`);
|
|
|
2114
2461
|
message: this.formatMessage(
|
|
2115
2462
|
`\u2705 Added agent-hooks tasks to mise.toml.
|
|
2116
2463
|
\u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
|
|
2117
|
-
enter += "
|
|
2118
|
-
leave += "${cr}/.
|
|
2464
|
+
enter += "sync-skills.py --scope project", "${cr}/.agents/hooks/sync.py --install --quiet"
|
|
2465
|
+
leave += "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
|
|
2119
2466
|
)
|
|
2120
2467
|
};
|
|
2121
2468
|
}
|
|
@@ -2130,10 +2477,25 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
2130
2477
|
printNextSteps() {
|
|
2131
2478
|
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
2132
2479
|
console.log(" Next steps:");
|
|
2133
|
-
console.log(" 1. mise run
|
|
2134
|
-
console.log(" 2.
|
|
2135
|
-
console.log(" 3.
|
|
2136
|
-
console.log(
|
|
2480
|
+
console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
|
|
2481
|
+
console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
2482
|
+
console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
|
|
2483
|
+
console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
2484
|
+
console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
|
|
2488
|
+
// src/recipes/MiseOpInjectRecipe.ts
|
|
2489
|
+
var MiseOpInjectRecipe = class extends Recipe {
|
|
2490
|
+
constructor(context) {
|
|
2491
|
+
super(context);
|
|
2492
|
+
this.addIngredient(WireMiseOpInject);
|
|
2493
|
+
}
|
|
2494
|
+
printNextSteps() {
|
|
2495
|
+
console.log("\u{1F389} Wired up .env.op 1Password resolution via mise!");
|
|
2496
|
+
console.log(" Next steps:");
|
|
2497
|
+
console.log(" 1. Create .env.op with your op:// secret references");
|
|
2498
|
+
console.log(" 2. Run `mise run secrets-inject` or simply cd out and back in to trigger the hook");
|
|
2137
2499
|
}
|
|
2138
2500
|
};
|
|
2139
2501
|
|
|
@@ -2176,6 +2538,12 @@ var RECIPE_REGISTRY = {
|
|
|
2176
2538
|
description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
|
|
2177
2539
|
class: AgentHooksRecipe,
|
|
2178
2540
|
commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
|
|
2541
|
+
},
|
|
2542
|
+
"mise-op-inject": {
|
|
2543
|
+
name: "mise-op-inject",
|
|
2544
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2545
|
+
class: MiseOpInjectRecipe,
|
|
2546
|
+
commands: ["WireMiseOpInject"]
|
|
2179
2547
|
}
|
|
2180
2548
|
};
|
|
2181
2549
|
var COMMAND_REGISTRY = {
|
|
@@ -2244,6 +2612,12 @@ var COMMAND_REGISTRY = {
|
|
|
2244
2612
|
description: "Create .env.example file",
|
|
2245
2613
|
group: "environment",
|
|
2246
2614
|
class: AddDotenv
|
|
2615
|
+
},
|
|
2616
|
+
WireMiseOpInject: {
|
|
2617
|
+
name: "WireMiseOpInject",
|
|
2618
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2619
|
+
group: "mise",
|
|
2620
|
+
class: WireMiseOpInject
|
|
2247
2621
|
}
|
|
2248
2622
|
};
|
|
2249
2623
|
function getRecipeNames() {
|
|
@@ -2259,15 +2633,15 @@ function createRecipe(name, context) {
|
|
|
2259
2633
|
}
|
|
2260
2634
|
|
|
2261
2635
|
// src/utils/version.ts
|
|
2262
|
-
import { readFileSync as
|
|
2263
|
-
import { dirname as dirname6, join as
|
|
2636
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2637
|
+
import { dirname as dirname6, join as join14 } from "node:path";
|
|
2264
2638
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2265
2639
|
var PJANGLER_VERSION = (() => {
|
|
2266
2640
|
try {
|
|
2267
2641
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2268
2642
|
for (let i = 0; i < 4; i++) {
|
|
2269
2643
|
try {
|
|
2270
|
-
const raw =
|
|
2644
|
+
const raw = readFileSync7(join14(dir, "package.json"), "utf8");
|
|
2271
2645
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
2272
2646
|
} catch {
|
|
2273
2647
|
const parent = dirname6(dir);
|
|
@@ -2281,29 +2655,61 @@ var PJANGLER_VERSION = (() => {
|
|
|
2281
2655
|
})();
|
|
2282
2656
|
|
|
2283
2657
|
// src/parity/index.ts
|
|
2284
|
-
import { existsSync as
|
|
2285
|
-
import { basename as
|
|
2286
|
-
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2658
|
+
import { existsSync as existsSync10, lstatSync as lstatSync2, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync8, readlinkSync, readdirSync as readdirSync3, realpathSync, renameSync as renameSync2, rmdirSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync4, copyFileSync, rmSync } from "node:fs";
|
|
2659
|
+
import { basename as basename4, dirname as dirname7, join as join15, relative as relative2, resolve as resolve3 } from "node:path";
|
|
2660
|
+
import { fileURLToPath as fileURLToPath4, pathToFileURL } from "node:url";
|
|
2287
2661
|
import { homedir as homedir5 } from "node:os";
|
|
2288
2662
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2289
2663
|
import YAML3 from "yaml";
|
|
2290
2664
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2291
2665
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
2666
|
+
var PROVISION_BMAD_SKILLS_SCRIPT = "python3 '{{config_root}}/.mise/scripts/provision-bmad-skills.py'";
|
|
2667
|
+
var SYNC_SKILLS_SCRIPT = "python3 '{{config_root}}/.mise/scripts/sync-skills.py' --scope project";
|
|
2292
2668
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
2669
|
+
var SKILLS_REGISTRY_URL = "https://github.com/delorenj/skillex.git";
|
|
2670
|
+
var PROJECT_CLI_SKILL_DIRS = [
|
|
2671
|
+
".gemini/skills",
|
|
2672
|
+
".codex/skills",
|
|
2673
|
+
".kimi/skills",
|
|
2674
|
+
".augment/skills",
|
|
2675
|
+
".config/opencode/skills",
|
|
2676
|
+
".hermes/skills",
|
|
2677
|
+
".claude/skills",
|
|
2678
|
+
".openclaw/skills"
|
|
2679
|
+
];
|
|
2680
|
+
var CANONICAL_CLAUDE_SKILLS_ALIAS = "../.agents/skills";
|
|
2293
2681
|
var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
|
|
2294
2682
|
# agent files to the main AGENTS.md file.
|
|
2295
2683
|
#
|
|
2296
2684
|
# TODO: Ensure this works for all levels of nesting.
|
|
2297
2685
|
# i.e. All linked agent files MUST be siblings at
|
|
2298
2686
|
# any given level of nesting.`;
|
|
2299
|
-
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
2687
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
2688
|
+
LINK_AGENTFILES_SCRIPT,
|
|
2689
|
+
OP_INJECT_SCRIPT,
|
|
2690
|
+
PROVISION_BMAD_SKILLS_SCRIPT,
|
|
2691
|
+
SYNC_SKILLS_SCRIPT
|
|
2692
|
+
];
|
|
2300
2693
|
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
2301
2694
|
patterns = ["AGENTS.md"]
|
|
2302
2695
|
task = "link-agentfiles"
|
|
2303
2696
|
|
|
2697
|
+
[[watch_files]]
|
|
2698
|
+
patterns = [".agents/skills.json"]
|
|
2699
|
+
task = "skills-sync"
|
|
2700
|
+
|
|
2304
2701
|
[tasks.link-agentfiles]
|
|
2305
2702
|
description = "Symlink all agent files to AGENTS.md"
|
|
2306
|
-
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2703
|
+
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2704
|
+
|
|
2705
|
+
[tasks.skills-sync]
|
|
2706
|
+
description = "Sync skills from manifest to local CLI dirs"
|
|
2707
|
+
depends = ["skills-provision-bmad"]
|
|
2708
|
+
run = "python3 '{{config_root}}/.mise/scripts/sync-skills.py' --scope project"
|
|
2709
|
+
|
|
2710
|
+
[tasks.skills-provision-bmad]
|
|
2711
|
+
description = "Provision pinned BMAD skills from the Skillex pack"
|
|
2712
|
+
run = "python3 '{{config_root}}/.mise/scripts/provision-bmad-skills.py'"`;
|
|
2307
2713
|
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
2308
2714
|
[tasks."version"]
|
|
2309
2715
|
description = "Print the current version (vX.Y.Z)"
|
|
@@ -2333,7 +2739,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
|
|
|
2333
2739
|
function resolvePjanglerRoot2() {
|
|
2334
2740
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2335
2741
|
while (dir !== dirname7(dir)) {
|
|
2336
|
-
if (
|
|
2742
|
+
if (existsSync10(join15(dir, "package.json")) && existsSync10(join15(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2337
2743
|
return dir;
|
|
2338
2744
|
}
|
|
2339
2745
|
dir = dirname7(dir);
|
|
@@ -2344,17 +2750,17 @@ function normalizeNewlines(value) {
|
|
|
2344
2750
|
return value.replace(/\r\n/g, "\n");
|
|
2345
2751
|
}
|
|
2346
2752
|
function readText(path) {
|
|
2347
|
-
return normalizeNewlines(
|
|
2753
|
+
return normalizeNewlines(readFileSync8(path, "utf8"));
|
|
2348
2754
|
}
|
|
2349
2755
|
function safeReadText(path) {
|
|
2350
|
-
return
|
|
2756
|
+
return existsSync10(path) ? readText(path) : null;
|
|
2351
2757
|
}
|
|
2352
2758
|
function ensureParent(path) {
|
|
2353
2759
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2354
2760
|
}
|
|
2355
2761
|
function writeText(path, content) {
|
|
2356
2762
|
ensureParent(path);
|
|
2357
|
-
|
|
2763
|
+
writeFileSync7(path, content);
|
|
2358
2764
|
}
|
|
2359
2765
|
function tryParseJson(text2) {
|
|
2360
2766
|
if (!text2) return null;
|
|
@@ -2371,7 +2777,7 @@ function titleCaseSlug(slug) {
|
|
|
2371
2777
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2372
2778
|
}
|
|
2373
2779
|
function readSymlinkTarget(path) {
|
|
2374
|
-
if (!
|
|
2780
|
+
if (!existsSync10(path)) return null;
|
|
2375
2781
|
try {
|
|
2376
2782
|
return readlinkSync(path);
|
|
2377
2783
|
} catch {
|
|
@@ -2379,8 +2785,8 @@ function readSymlinkTarget(path) {
|
|
|
2379
2785
|
}
|
|
2380
2786
|
}
|
|
2381
2787
|
function ensureSymlink(path, target, dryRun) {
|
|
2382
|
-
if (
|
|
2383
|
-
const stat =
|
|
2788
|
+
if (existsSync10(path)) {
|
|
2789
|
+
const stat = lstatSync2(path);
|
|
2384
2790
|
if (stat.isSymbolicLink()) {
|
|
2385
2791
|
const current = readSymlinkTarget(path);
|
|
2386
2792
|
if (current === target) return { changed: false };
|
|
@@ -2390,18 +2796,18 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2390
2796
|
}
|
|
2391
2797
|
return { changed: true };
|
|
2392
2798
|
}
|
|
2393
|
-
return { changed: false, blocked: `${
|
|
2799
|
+
return { changed: false, blocked: `${relative2(process.cwd(), path) || path} exists and is not a symlink` };
|
|
2394
2800
|
}
|
|
2395
2801
|
if (!dryRun) symlinkSync(target, path);
|
|
2396
2802
|
return { changed: true };
|
|
2397
2803
|
}
|
|
2398
2804
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2399
|
-
const agentsPath =
|
|
2400
|
-
if (
|
|
2805
|
+
const agentsPath = join15(repoRoot, "AGENTS.md");
|
|
2806
|
+
if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
|
|
2401
2807
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2402
|
-
const source =
|
|
2403
|
-
if (!
|
|
2404
|
-
const stat =
|
|
2808
|
+
const source = join15(repoRoot, file);
|
|
2809
|
+
if (!existsSync10(source)) continue;
|
|
2810
|
+
const stat = lstatSync2(source);
|
|
2405
2811
|
if (stat.isSymbolicLink()) continue;
|
|
2406
2812
|
if (stat.isFile()) {
|
|
2407
2813
|
if (!dryRun) renameSync2(source, agentsPath);
|
|
@@ -2409,9 +2815,9 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2409
2815
|
}
|
|
2410
2816
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2411
2817
|
}
|
|
2412
|
-
const readmePath =
|
|
2413
|
-
if (
|
|
2414
|
-
const stat =
|
|
2818
|
+
const readmePath = join15(repoRoot, "README.md");
|
|
2819
|
+
if (existsSync10(readmePath)) {
|
|
2820
|
+
const stat = lstatSync2(readmePath);
|
|
2415
2821
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2416
2822
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
2417
2823
|
return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
|
|
@@ -2449,12 +2855,12 @@ function yamlGet(text2, keyPath) {
|
|
|
2449
2855
|
return "";
|
|
2450
2856
|
}
|
|
2451
2857
|
function discoverRoles(repoRoot) {
|
|
2452
|
-
const rolesDir =
|
|
2453
|
-
if (!
|
|
2454
|
-
return
|
|
2455
|
-
const roleDir =
|
|
2456
|
-
const roleYamlPath =
|
|
2457
|
-
if (!
|
|
2858
|
+
const rolesDir = join15(repoRoot, "agents", "hermes");
|
|
2859
|
+
if (!existsSync10(rolesDir)) return [];
|
|
2860
|
+
return readdirSync3(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2861
|
+
const roleDir = join15(rolesDir, entry.name);
|
|
2862
|
+
const roleYamlPath = join15(roleDir, "role.yaml");
|
|
2863
|
+
if (!existsSync10(roleYamlPath)) return null;
|
|
2458
2864
|
const text2 = readText(roleYamlPath);
|
|
2459
2865
|
const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
|
|
2460
2866
|
return {
|
|
@@ -2482,7 +2888,7 @@ function discoverRoles(repoRoot) {
|
|
|
2482
2888
|
}).filter((value) => Boolean(value));
|
|
2483
2889
|
}
|
|
2484
2890
|
function registryPath(homeDir) {
|
|
2485
|
-
return
|
|
2891
|
+
return join15(homeDir, ".hermes", "agents-registry.yaml");
|
|
2486
2892
|
}
|
|
2487
2893
|
function systemctlUser(args) {
|
|
2488
2894
|
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -2493,8 +2899,8 @@ function systemctlUser(args) {
|
|
|
2493
2899
|
};
|
|
2494
2900
|
}
|
|
2495
2901
|
function templateScript(ctx, name) {
|
|
2496
|
-
const source =
|
|
2497
|
-
return
|
|
2902
|
+
const source = join15(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2903
|
+
return existsSync10(source) ? readText(source) : void 0;
|
|
2498
2904
|
}
|
|
2499
2905
|
function templateVersioningScript(ctx) {
|
|
2500
2906
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2506,8 +2912,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2506
2912
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2507
2913
|
if (override === "0" || override === "false") return false;
|
|
2508
2914
|
if (override === "1" || override === "true") return true;
|
|
2509
|
-
if (
|
|
2510
|
-
return !
|
|
2915
|
+
if (existsSync10(join15(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2916
|
+
return !existsSync10(join15(ctx.homeDir, ".agents", "hooks"));
|
|
2511
2917
|
}
|
|
2512
2918
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2513
2919
|
const out = [];
|
|
@@ -2533,102 +2939,521 @@ function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
|
2533
2939
|
}
|
|
2534
2940
|
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
2535
2941
|
const project = readProjectJson(ctx);
|
|
2536
|
-
const projectName = String(project?.project_name ??
|
|
2942
|
+
const projectName = String(project?.project_name ?? basename4(ctx.repoRoot) ?? "project");
|
|
2537
2943
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2538
2944
|
}
|
|
2539
2945
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2540
|
-
const targetPath =
|
|
2541
|
-
if (
|
|
2542
|
-
const sourcePath =
|
|
2543
|
-
if (!
|
|
2946
|
+
const targetPath = join15(ctx.repoRoot, "mise.toml");
|
|
2947
|
+
if (existsSync10(targetPath)) return false;
|
|
2948
|
+
const sourcePath = join15(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2949
|
+
if (!existsSync10(sourcePath)) return false;
|
|
2544
2950
|
changedFiles.push(targetPath);
|
|
2545
2951
|
if (!ctx.dryRun) {
|
|
2546
2952
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
2547
2953
|
}
|
|
2548
2954
|
return true;
|
|
2549
2955
|
}
|
|
2550
|
-
function
|
|
2551
|
-
const
|
|
2552
|
-
return
|
|
2956
|
+
function templateCommonProjectText(ctx, rel) {
|
|
2957
|
+
const path = join15(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
2958
|
+
return existsSync10(path) ? readText(path) : void 0;
|
|
2553
2959
|
}
|
|
2554
|
-
function
|
|
2555
|
-
if (
|
|
2556
|
-
|
|
2557
|
-
}
|
|
2558
|
-
if (beforePattern) {
|
|
2559
|
-
const match = text2.match(beforePattern);
|
|
2560
|
-
if (match && typeof match.index === "number") {
|
|
2561
|
-
return `${text2.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
2562
|
-
|
|
2563
|
-
${text2.slice(match.index)}`;
|
|
2564
|
-
}
|
|
2960
|
+
function validateSkillName(name) {
|
|
2961
|
+
if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\") || basename4(name) !== name) {
|
|
2962
|
+
throw new Error(`Unsafe skill name: ${JSON.stringify(name)}`);
|
|
2565
2963
|
}
|
|
2566
|
-
return
|
|
2567
|
-
|
|
2568
|
-
${block}
|
|
2569
|
-
`;
|
|
2964
|
+
return name;
|
|
2570
2965
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2966
|
+
function lstatIfPresent(path) {
|
|
2967
|
+
try {
|
|
2968
|
+
return lstatSync2(path);
|
|
2969
|
+
} catch (error) {
|
|
2970
|
+
if (error.code === "ENOENT") return void 0;
|
|
2971
|
+
throw error;
|
|
2577
2972
|
}
|
|
2578
|
-
return required;
|
|
2579
2973
|
}
|
|
2580
|
-
function
|
|
2581
|
-
const
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
if (!pathLine) {
|
|
2594
|
-
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
2974
|
+
function isContainedBy(root, target) {
|
|
2975
|
+
const rel = relative2(root, target);
|
|
2976
|
+
return rel === "" || rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\");
|
|
2977
|
+
}
|
|
2978
|
+
function prepareSafeProjectSkillsDirs(ctx) {
|
|
2979
|
+
const projectRoot = realpathSync(ctx.repoRoot);
|
|
2980
|
+
const agentsDir = join15(projectRoot, ".agents");
|
|
2981
|
+
const skillsDir = join15(agentsDir, "skills");
|
|
2982
|
+
for (const path of [agentsDir, skillsDir]) {
|
|
2983
|
+
if (!isContainedBy(projectRoot, path)) throw new Error(`Project skills path escapes repository: ${path}`);
|
|
2984
|
+
const stat = lstatIfPresent(path);
|
|
2985
|
+
if (stat?.isSymbolicLink()) throw new Error(`Refusing symlinked project skills directory: ${path}`);
|
|
2986
|
+
if (stat && !stat.isDirectory()) throw new Error(`Project skills path is not a directory: ${path}`);
|
|
2595
2987
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2988
|
+
if (!ctx.dryRun) {
|
|
2989
|
+
if (!existsSync10(agentsDir)) mkdirSync6(agentsDir, { recursive: false });
|
|
2990
|
+
if (!existsSync10(skillsDir)) mkdirSync6(skillsDir, { recursive: false });
|
|
2991
|
+
for (const path of [agentsDir, skillsDir]) {
|
|
2992
|
+
if (lstatSync2(path).isSymbolicLink() || !lstatSync2(path).isDirectory()) {
|
|
2993
|
+
throw new Error(`Unsafe project skills directory after creation: ${path}`);
|
|
2994
|
+
}
|
|
2995
|
+
if (!isContainedBy(projectRoot, realpathSync(path))) {
|
|
2996
|
+
throw new Error(`Resolved project skills directory escapes repository: ${path}`);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2600
2999
|
}
|
|
2601
|
-
|
|
2602
|
-
if (pathLine[0] === nextLine) return text2;
|
|
2603
|
-
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
3000
|
+
return { agentsDir, skillsDir };
|
|
2604
3001
|
}
|
|
2605
|
-
function
|
|
2606
|
-
const
|
|
2607
|
-
let
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
3002
|
+
function projectSkillTopologyIssues(repoRoot) {
|
|
3003
|
+
const issues = [];
|
|
3004
|
+
let projectRoot;
|
|
3005
|
+
try {
|
|
3006
|
+
projectRoot = realpathSync(repoRoot);
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
return [`Project root is not a readable real directory: ${error instanceof Error ? error.message : String(error)}`];
|
|
3009
|
+
}
|
|
3010
|
+
const managedSkills = join15(projectRoot, ".agents", "skills");
|
|
3011
|
+
for (const rel of PROJECT_CLI_SKILL_DIRS) {
|
|
3012
|
+
const cliDir = join15(projectRoot, rel);
|
|
3013
|
+
const parent = dirname7(cliDir);
|
|
3014
|
+
const parentStat = lstatIfPresent(parent);
|
|
3015
|
+
if (!parentStat) continue;
|
|
3016
|
+
if (parentStat.isSymbolicLink() || !parentStat.isDirectory()) {
|
|
3017
|
+
issues.push(`${rel} has an unsafe symlinked/non-directory parent`);
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
3020
|
+
if (!isContainedBy(projectRoot, realOrSelf(parent))) {
|
|
3021
|
+
issues.push(`${rel} parent resolves outside the project`);
|
|
3022
|
+
continue;
|
|
3023
|
+
}
|
|
3024
|
+
const stat = lstatIfPresent(cliDir);
|
|
3025
|
+
if (!stat) continue;
|
|
3026
|
+
if (stat.isSymbolicLink()) {
|
|
3027
|
+
let rawTarget = "";
|
|
3028
|
+
try {
|
|
3029
|
+
rawTarget = readlinkSync(cliDir);
|
|
3030
|
+
} catch {
|
|
3031
|
+
issues.push(`${rel} is an unreadable skills directory symlink`);
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
if (rel !== ".claude/skills" || rawTarget !== CANONICAL_CLAUDE_SKILLS_ALIAS) {
|
|
3035
|
+
issues.push(`${rel} is an unsupported skills directory symlink`);
|
|
3036
|
+
continue;
|
|
3037
|
+
}
|
|
3038
|
+
const managedStat = lstatIfPresent(managedSkills);
|
|
3039
|
+
if (!managedStat || managedStat.isSymbolicLink() || !managedStat.isDirectory()) {
|
|
3040
|
+
issues.push(`${rel} canonical alias target .agents/skills is missing or unsafe`);
|
|
3041
|
+
continue;
|
|
3042
|
+
}
|
|
3043
|
+
try {
|
|
3044
|
+
if (realpathSync(cliDir) !== realpathSync(managedSkills)) {
|
|
3045
|
+
issues.push(`${rel} canonical alias resolves outside .agents/skills`);
|
|
2617
3046
|
}
|
|
3047
|
+
} catch {
|
|
3048
|
+
issues.push(`${rel} canonical alias is broken`);
|
|
2618
3049
|
}
|
|
2619
|
-
|
|
3050
|
+
continue;
|
|
2620
3051
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
}
|
|
3052
|
+
if (!stat.isDirectory()) {
|
|
3053
|
+
issues.push(`${rel} is not a directory`);
|
|
3054
|
+
continue;
|
|
3055
|
+
}
|
|
3056
|
+
if (!isContainedBy(projectRoot, realOrSelf(cliDir))) {
|
|
3057
|
+
issues.push(`${rel} resolves outside the project`);
|
|
2627
3058
|
}
|
|
2628
|
-
if (end === -1) end = lines.length;
|
|
2629
|
-
break;
|
|
2630
3059
|
}
|
|
2631
|
-
|
|
3060
|
+
return issues;
|
|
3061
|
+
}
|
|
3062
|
+
function bmadPackRoot(ctx) {
|
|
3063
|
+
return resolve3(
|
|
3064
|
+
process.env.PJ_BMAD_PACK_ROOT?.trim() || join15(ctx.homeDir, "code", "skillex", "packs", "bmad", BMAD_PACK_VERSION)
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
function canonicalBmadSkillEntries(ctx) {
|
|
3068
|
+
const root = bmadPackRoot(ctx);
|
|
3069
|
+
const trusted = validateTrustedBmadPack(root);
|
|
3070
|
+
return trusted.skillNames.map((name) => ({ name: validateSkillName(name), source: pathToFileURL(join15(root, name)).href }));
|
|
3071
|
+
}
|
|
3072
|
+
function skillManifestEntryName(entry) {
|
|
3073
|
+
if (typeof entry === "string") return entry;
|
|
3074
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
3075
|
+
const name = entry.name;
|
|
3076
|
+
return typeof name === "string" ? name : void 0;
|
|
3077
|
+
}
|
|
3078
|
+
function isPackManagedManifestEntry(entry, expectedNames, packRoot) {
|
|
3079
|
+
const name = skillManifestEntryName(entry);
|
|
3080
|
+
if (!name) return false;
|
|
3081
|
+
if (expectedNames.has(name)) return true;
|
|
3082
|
+
if (!entry || typeof entry !== "object") return false;
|
|
3083
|
+
const source = entry.source;
|
|
3084
|
+
if (typeof source !== "string" || !source.startsWith("file:")) return false;
|
|
3085
|
+
try {
|
|
3086
|
+
const sourcePath = resolve3(fileURLToPath4(source));
|
|
3087
|
+
return basename4(sourcePath) === name && isContainedBy(packRoot, sourcePath);
|
|
3088
|
+
} catch {
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
function canonicalSkillsManifest(ctx, current, packSkills = canonicalBmadSkillEntries(ctx)) {
|
|
3093
|
+
const existing = Array.isArray(current?.skills) ? current.skills : [];
|
|
3094
|
+
const expectedNames = new Set(packSkills.map((entry) => entry.name));
|
|
3095
|
+
const packRoot = bmadPackRoot(ctx);
|
|
3096
|
+
return `${JSON.stringify(
|
|
3097
|
+
{
|
|
3098
|
+
...current ?? {},
|
|
3099
|
+
$schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
|
|
3100
|
+
inherit_global: true,
|
|
3101
|
+
registry: SKILLS_REGISTRY_URL,
|
|
3102
|
+
skills: [
|
|
3103
|
+
...existing.filter((entry) => !isPackManagedManifestEntry(entry, expectedNames, packRoot)),
|
|
3104
|
+
...packSkills
|
|
3105
|
+
]
|
|
3106
|
+
},
|
|
3107
|
+
null,
|
|
3108
|
+
2
|
|
3109
|
+
)}
|
|
3110
|
+
`;
|
|
3111
|
+
}
|
|
3112
|
+
function removeProjectEntry(path) {
|
|
3113
|
+
const stat = lstatIfPresent(path);
|
|
3114
|
+
if (!stat) return;
|
|
3115
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
3116
|
+
rmSync(path, { recursive: true, force: true });
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
try {
|
|
3120
|
+
unlinkSync3(path);
|
|
3121
|
+
} catch (error) {
|
|
3122
|
+
if (error.code !== "ENOENT") throw error;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
function normalizeExecutableTemplate(ctx, target, expected, changedFiles) {
|
|
3126
|
+
const stat = lstatIfPresent(target);
|
|
3127
|
+
if (stat && (!stat.isFile() || stat.isSymbolicLink())) {
|
|
3128
|
+
throw new Error(`Refusing non-regular managed executable target: ${target}`);
|
|
3129
|
+
}
|
|
3130
|
+
const contentChanged = !stat || safeReadText(target) !== expected;
|
|
3131
|
+
const modeChanged = !stat || (Number(stat.mode) & 73) === 0;
|
|
3132
|
+
if (!contentChanged && !modeChanged) return;
|
|
3133
|
+
if (!changedFiles.includes(target)) changedFiles.push(target);
|
|
3134
|
+
if (ctx.dryRun) return;
|
|
3135
|
+
if (contentChanged) {
|
|
3136
|
+
writeText(target, expected);
|
|
3137
|
+
}
|
|
3138
|
+
const beforeChmod = lstatIfPresent(target);
|
|
3139
|
+
if (!beforeChmod?.isFile() || beforeChmod.isSymbolicLink()) {
|
|
3140
|
+
throw new Error(`Refusing changed managed executable target: ${target}`);
|
|
3141
|
+
}
|
|
3142
|
+
chmodSync4(target, 493);
|
|
3143
|
+
}
|
|
3144
|
+
function atomicWriteBuffer(path, content, mode, temporary) {
|
|
3145
|
+
writeFileSync7(temporary, content, { flag: "wx" });
|
|
3146
|
+
chmodSync4(temporary, mode);
|
|
3147
|
+
renameSync2(temporary, path);
|
|
3148
|
+
}
|
|
3149
|
+
function provisionBmadSkills(ctx, preservedManifest, hooks = {}) {
|
|
3150
|
+
const packRoot = bmadPackRoot(ctx);
|
|
3151
|
+
let packSkills;
|
|
3152
|
+
try {
|
|
3153
|
+
packSkills = canonicalBmadSkillEntries(ctx);
|
|
3154
|
+
} catch (error) {
|
|
3155
|
+
return {
|
|
3156
|
+
ok: false,
|
|
3157
|
+
changedFiles: [],
|
|
3158
|
+
error: `BMAD Skillex pack ${BMAD_PACK_VERSION} is not trusted at ${packRoot}: ${error instanceof Error ? error.message : String(error)}`
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
hooks.afterPreflight?.();
|
|
3162
|
+
const projectRoot = realpathSync(ctx.repoRoot);
|
|
3163
|
+
const agentsPath = join15(projectRoot, ".agents");
|
|
3164
|
+
const skillsPath = join15(agentsPath, "skills");
|
|
3165
|
+
const agentsExisted = Boolean(lstatIfPresent(agentsPath));
|
|
3166
|
+
const skillsExisted = Boolean(lstatIfPresent(skillsPath));
|
|
3167
|
+
let preflightDirs;
|
|
3168
|
+
try {
|
|
3169
|
+
preflightDirs = prepareSafeProjectSkillsDirs({ ...ctx, dryRun: true });
|
|
3170
|
+
} catch (error) {
|
|
3171
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3172
|
+
}
|
|
3173
|
+
const manifestPath = join15(preflightDirs.agentsDir, "skills.json");
|
|
3174
|
+
const manifestStat = lstatIfPresent(manifestPath);
|
|
3175
|
+
if (manifestStat?.isSymbolicLink() || manifestStat && !manifestStat.isFile()) {
|
|
3176
|
+
return { ok: false, changedFiles: [], error: `Refusing unsafe skills manifest: ${manifestPath}` };
|
|
3177
|
+
}
|
|
3178
|
+
const manifestBytes = manifestStat ? readFileSync8(manifestPath) : null;
|
|
3179
|
+
const manifestMode = manifestStat ? Number(manifestStat.mode) & 511 : 420;
|
|
3180
|
+
let currentManifest = {};
|
|
3181
|
+
if (manifestBytes !== null) {
|
|
3182
|
+
try {
|
|
3183
|
+
const parsed = JSON.parse(manifestBytes.toString("utf8"));
|
|
3184
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("must contain a JSON object");
|
|
3185
|
+
currentManifest = parsed;
|
|
3186
|
+
if (currentManifest.skills !== void 0 && !Array.isArray(currentManifest.skills)) throw new Error("skills must be an array");
|
|
3187
|
+
} catch (error) {
|
|
3188
|
+
return { ok: false, changedFiles: [], error: `Invalid existing skills manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}` };
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
let safeDirs;
|
|
3192
|
+
try {
|
|
3193
|
+
safeDirs = prepareSafeProjectSkillsDirs(ctx);
|
|
3194
|
+
} catch (error) {
|
|
3195
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3196
|
+
}
|
|
3197
|
+
const nextManifest = canonicalSkillsManifest(ctx, preservedManifest ?? currentManifest, packSkills);
|
|
3198
|
+
const skillsDir = safeDirs.skillsDir;
|
|
3199
|
+
const resolvedSkillsDir = ctx.dryRun && !existsSync10(skillsDir) ? skillsDir : realpathSync(skillsDir);
|
|
3200
|
+
const expected = new Map(packSkills.map((entry) => [entry.name, fileURLToPath4(entry.source)]));
|
|
3201
|
+
const expectedNames = new Set(expected.keys());
|
|
3202
|
+
const ownershipManifest = preservedManifest ?? currentManifest;
|
|
3203
|
+
const managedManifestNames = new Set(
|
|
3204
|
+
(Array.isArray(ownershipManifest.skills) ? ownershipManifest.skills : []).filter((entry) => isPackManagedManifestEntry(entry, expectedNames, packRoot)).map(skillManifestEntryName).filter((name) => Boolean(name))
|
|
3205
|
+
);
|
|
3206
|
+
const affected = /* @__PURE__ */ new Set();
|
|
3207
|
+
const staleManagedNames = /* @__PURE__ */ new Set();
|
|
3208
|
+
const originalCorrectLinks = /* @__PURE__ */ new Map();
|
|
3209
|
+
if (existsSync10(skillsDir)) {
|
|
3210
|
+
for (const name of readdirSync3(skillsDir)) {
|
|
3211
|
+
validateSkillName(name);
|
|
3212
|
+
if (dirname7(join15(resolvedSkillsDir, name)) !== resolvedSkillsDir) {
|
|
3213
|
+
return { ok: false, changedFiles: [], error: `BMAD skill path escapes project skills directory: ${name}` };
|
|
3214
|
+
}
|
|
3215
|
+
const entryPath = join15(skillsDir, name);
|
|
3216
|
+
let linkTargetsPack = false;
|
|
3217
|
+
try {
|
|
3218
|
+
linkTargetsPack = lstatSync2(entryPath).isSymbolicLink() && isContainedBy(packRoot, resolve3(dirname7(entryPath), readlinkSync(entryPath)));
|
|
3219
|
+
} catch {
|
|
3220
|
+
linkTargetsPack = false;
|
|
3221
|
+
}
|
|
3222
|
+
if (!expected.has(name) && !managedManifestNames.has(name) && !linkTargetsPack) continue;
|
|
3223
|
+
const target = expected.get(name);
|
|
3224
|
+
let correct = false;
|
|
3225
|
+
try {
|
|
3226
|
+
correct = Boolean(target) && lstatSync2(entryPath).isSymbolicLink() && resolve3(dirname7(entryPath), readlinkSync(entryPath)) === target;
|
|
3227
|
+
} catch {
|
|
3228
|
+
correct = false;
|
|
3229
|
+
}
|
|
3230
|
+
if (correct) originalCorrectLinks.set(name, readlinkSync(join15(skillsDir, name)));
|
|
3231
|
+
else {
|
|
3232
|
+
affected.add(name);
|
|
3233
|
+
if (!target) staleManagedNames.add(name);
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
for (const [name, target] of expected) {
|
|
3238
|
+
const link = join15(resolvedSkillsDir, validateSkillName(name));
|
|
3239
|
+
if (dirname7(link) !== resolvedSkillsDir) {
|
|
3240
|
+
return { ok: false, changedFiles: [], error: `BMAD skill path escapes project skills directory: ${name}` };
|
|
3241
|
+
}
|
|
3242
|
+
let correct = false;
|
|
3243
|
+
try {
|
|
3244
|
+
correct = lstatSync2(link).isSymbolicLink() && resolve3(dirname7(link), readlinkSync(link)) === target;
|
|
3245
|
+
} catch {
|
|
3246
|
+
correct = false;
|
|
3247
|
+
}
|
|
3248
|
+
if (!correct) affected.add(name);
|
|
3249
|
+
}
|
|
3250
|
+
const manifestChanged = manifestBytes?.toString("utf8") !== nextManifest;
|
|
3251
|
+
const changedFiles = [
|
|
3252
|
+
...manifestChanged ? [manifestPath] : [],
|
|
3253
|
+
...affected.size ? [skillsDir] : []
|
|
3254
|
+
];
|
|
3255
|
+
if (ctx.dryRun || changedFiles.length === 0) {
|
|
3256
|
+
try {
|
|
3257
|
+
const postflight = validateTrustedBmadPack(packRoot);
|
|
3258
|
+
if (JSON.stringify(postflight.skillNames) !== JSON.stringify(packSkills.map((entry) => entry.name))) {
|
|
3259
|
+
throw new Error("BMAD pack inventory changed after preflight");
|
|
3260
|
+
}
|
|
3261
|
+
return { ok: true, changedFiles };
|
|
3262
|
+
} catch (error) {
|
|
3263
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
const transaction = mkdtempSync(join15(safeDirs.agentsDir, ".bmad-transaction-"));
|
|
3267
|
+
const backup = join15(transaction, "entries");
|
|
3268
|
+
mkdirSync6(backup);
|
|
3269
|
+
const moved = [];
|
|
3270
|
+
const rollback = () => {
|
|
3271
|
+
const errors = [];
|
|
3272
|
+
try {
|
|
3273
|
+
for (const name of affected) {
|
|
3274
|
+
removeProjectEntry(join15(skillsDir, validateSkillName(name)));
|
|
3275
|
+
}
|
|
3276
|
+
for (const name of originalCorrectLinks.keys()) {
|
|
3277
|
+
removeProjectEntry(join15(skillsDir, validateSkillName(name)));
|
|
3278
|
+
}
|
|
3279
|
+
} catch (error) {
|
|
3280
|
+
errors.push(`remove applied projection: ${String(error)}`);
|
|
3281
|
+
}
|
|
3282
|
+
for (const name of [...moved].reverse()) {
|
|
3283
|
+
try {
|
|
3284
|
+
renameSync2(join15(backup, name), join15(skillsDir, name));
|
|
3285
|
+
} catch (error) {
|
|
3286
|
+
errors.push(`restore ${name}: ${String(error)}`);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
for (const [name, rawTarget] of originalCorrectLinks) {
|
|
3290
|
+
try {
|
|
3291
|
+
symlinkSync(rawTarget, join15(skillsDir, name), "dir");
|
|
3292
|
+
} catch (error) {
|
|
3293
|
+
errors.push(`restore ${name}: ${String(error)}`);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
try {
|
|
3297
|
+
if (manifestBytes === null) removeProjectEntry(manifestPath);
|
|
3298
|
+
else atomicWriteBuffer(manifestPath, manifestBytes, manifestMode, join15(transaction, "manifest.restore"));
|
|
3299
|
+
} catch (error) {
|
|
3300
|
+
errors.push(`restore manifest: ${String(error)}`);
|
|
3301
|
+
}
|
|
3302
|
+
rmSync(transaction, { recursive: true, force: true });
|
|
3303
|
+
try {
|
|
3304
|
+
if (!skillsExisted && existsSync10(skillsDir) && readdirSync3(skillsDir).length === 0) rmdirSync(skillsDir);
|
|
3305
|
+
if (!agentsExisted && existsSync10(safeDirs.agentsDir) && readdirSync3(safeDirs.agentsDir).length === 0) rmdirSync(safeDirs.agentsDir);
|
|
3306
|
+
} catch (error) {
|
|
3307
|
+
errors.push(`remove created directories: ${String(error)}`);
|
|
3308
|
+
}
|
|
3309
|
+
if (errors.length) throw new Error(`BMAD rollback was incomplete: ${errors.join("; ")}`);
|
|
3310
|
+
};
|
|
3311
|
+
try {
|
|
3312
|
+
for (const name of affected) {
|
|
3313
|
+
const entry = join15(skillsDir, name);
|
|
3314
|
+
if (lstatIfPresent(entry)) {
|
|
3315
|
+
renameSync2(entry, join15(backup, name));
|
|
3316
|
+
moved.push(name);
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
let index = 0;
|
|
3320
|
+
for (const [name, target] of expected) {
|
|
3321
|
+
index += 1;
|
|
3322
|
+
const link = join15(skillsDir, name);
|
|
3323
|
+
let correct = false;
|
|
3324
|
+
try {
|
|
3325
|
+
correct = lstatSync2(link).isSymbolicLink() && resolve3(skillsDir, readlinkSync(link)) === target;
|
|
3326
|
+
} catch {
|
|
3327
|
+
correct = false;
|
|
3328
|
+
}
|
|
3329
|
+
if (correct) continue;
|
|
3330
|
+
if (hooks.createLink) hooks.createLink(target, link, index);
|
|
3331
|
+
else symlinkSync(target, link, "dir");
|
|
3332
|
+
}
|
|
3333
|
+
if (manifestChanged) {
|
|
3334
|
+
atomicWriteBuffer(manifestPath, Buffer.from(nextManifest), manifestMode, join15(transaction, "manifest.next"));
|
|
3335
|
+
}
|
|
3336
|
+
const postflight = validateTrustedBmadPack(packRoot);
|
|
3337
|
+
if (JSON.stringify(postflight.skillNames) !== JSON.stringify(packSkills.map((entry) => entry.name))) {
|
|
3338
|
+
throw new Error("BMAD pack inventory changed after preflight");
|
|
3339
|
+
}
|
|
3340
|
+
hooks.afterApply?.(manifestPath, skillsDir);
|
|
3341
|
+
for (const name of staleManagedNames) {
|
|
3342
|
+
if (lstatIfPresent(join15(skillsDir, name))) {
|
|
3343
|
+
throw new Error(`Applied BMAD projection retained stale managed entry: ${name}`);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
for (const [name, target] of expected) {
|
|
3347
|
+
const link = join15(skillsDir, name);
|
|
3348
|
+
let correct = false;
|
|
3349
|
+
try {
|
|
3350
|
+
correct = lstatSync2(link).isSymbolicLink() && resolve3(skillsDir, readlinkSync(link)) === target;
|
|
3351
|
+
} catch {
|
|
3352
|
+
correct = false;
|
|
3353
|
+
}
|
|
3354
|
+
if (!correct) throw new Error(`Applied BMAD projection link differs from plan: ${name}`);
|
|
3355
|
+
}
|
|
3356
|
+
const finalManifestStat = lstatIfPresent(manifestPath);
|
|
3357
|
+
if (!finalManifestStat || finalManifestStat.isSymbolicLink() || !finalManifestStat.isFile() || (Number(finalManifestStat.mode) & 511) !== manifestMode || readFileSync8(manifestPath).toString("utf8") !== nextManifest) {
|
|
3358
|
+
throw new Error("Applied BMAD skills manifest differs from planned bytes or mode");
|
|
3359
|
+
}
|
|
3360
|
+
const finalManifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
3361
|
+
if (finalManifest.$schema !== "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json" || finalManifest.inherit_global !== true || finalManifest.registry !== SKILLS_REGISTRY_URL || !Array.isArray(finalManifest.skills)) {
|
|
3362
|
+
throw new Error("Applied BMAD skills manifest schema differs from plan");
|
|
3363
|
+
}
|
|
3364
|
+
} catch (error) {
|
|
3365
|
+
try {
|
|
3366
|
+
rollback();
|
|
3367
|
+
} catch (rollbackError) {
|
|
3368
|
+
return { ok: false, changedFiles: [], error: `BMAD provisioning failed (${String(error)}); ${String(rollbackError)}` };
|
|
3369
|
+
}
|
|
3370
|
+
return { ok: false, changedFiles: [], error: error instanceof Error ? error.message : String(error) };
|
|
3371
|
+
}
|
|
3372
|
+
rmSync(transaction, { recursive: true });
|
|
3373
|
+
return { ok: true, changedFiles };
|
|
3374
|
+
}
|
|
3375
|
+
function templateVersionFilesConf(ctx, repoRoot) {
|
|
3376
|
+
const packageJson = join15(repoRoot, "package.json");
|
|
3377
|
+
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";
|
|
3378
|
+
}
|
|
3379
|
+
function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
|
|
3380
|
+
if (startMarker.test(text2)) {
|
|
3381
|
+
return text2.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
|
|
3382
|
+
}
|
|
3383
|
+
if (beforePattern) {
|
|
3384
|
+
const match = text2.match(beforePattern);
|
|
3385
|
+
if (match && typeof match.index === "number") {
|
|
3386
|
+
return `${text2.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
3387
|
+
|
|
3388
|
+
${text2.slice(match.index)}`;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
return `${text2.replace(/\s*$/, "")}
|
|
3392
|
+
|
|
3393
|
+
${block}
|
|
3394
|
+
`;
|
|
3395
|
+
}
|
|
3396
|
+
var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
|
|
3397
|
+
var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/hermes"];
|
|
3398
|
+
function requiredMisePathEntries(ctx) {
|
|
3399
|
+
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
3400
|
+
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
3401
|
+
if (existsSync10(join15(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
3402
|
+
}
|
|
3403
|
+
return required;
|
|
3404
|
+
}
|
|
3405
|
+
function upsertMisePath(text2, required = BASE_MISE_PATH_ENTRIES) {
|
|
3406
|
+
const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
3407
|
+
const envMatch = text2.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
|
|
3408
|
+
if (!envMatch || typeof envMatch.index !== "number") {
|
|
3409
|
+
return `[env]
|
|
3410
|
+
${render(required)}
|
|
3411
|
+
|
|
3412
|
+
${text2.replace(/^\s+/, "")}`;
|
|
3413
|
+
}
|
|
3414
|
+
const prefix = text2.slice(0, envMatch.index + envMatch[1].length);
|
|
3415
|
+
const section = envMatch[2];
|
|
3416
|
+
const suffix = text2.slice(envMatch.index + envMatch[1].length + section.length);
|
|
3417
|
+
const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
|
|
3418
|
+
if (!pathLine) {
|
|
3419
|
+
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
3420
|
+
}
|
|
3421
|
+
const current = [...pathLine[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
3422
|
+
const merged = [...current];
|
|
3423
|
+
for (const value of required) {
|
|
3424
|
+
if (!merged.includes(value)) merged.push(value);
|
|
3425
|
+
}
|
|
3426
|
+
const nextLine = render(merged);
|
|
3427
|
+
if (pathLine[0] === nextLine) return text2;
|
|
3428
|
+
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
3429
|
+
}
|
|
3430
|
+
function removeTomlSection(text2, headerPattern, marker, options) {
|
|
3431
|
+
const lines = text2.split("\n");
|
|
3432
|
+
let start = -1;
|
|
3433
|
+
let end = -1;
|
|
3434
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3435
|
+
if (!headerPattern.test(lines[i])) continue;
|
|
3436
|
+
if (marker) {
|
|
3437
|
+
let hasMarker = false;
|
|
3438
|
+
for (let j = i + 1; j < lines.length && !/^\[[^\]]+\]/.test(lines[j]); j++) {
|
|
3439
|
+
if (marker.test(lines[j])) {
|
|
3440
|
+
hasMarker = true;
|
|
3441
|
+
break;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
if (!hasMarker) continue;
|
|
3445
|
+
}
|
|
3446
|
+
start = i;
|
|
3447
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
3448
|
+
if (/^\[[^\]]+\]/.test(lines[j])) {
|
|
3449
|
+
end = j;
|
|
3450
|
+
break;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
if (end === -1) end = lines.length;
|
|
3454
|
+
break;
|
|
3455
|
+
}
|
|
3456
|
+
if (start === -1) return text2;
|
|
2632
3457
|
while (end > start + 1 && (lines[end - 1].trim() === "" || lines[end - 1].trim().startsWith("#"))) {
|
|
2633
3458
|
end--;
|
|
2634
3459
|
}
|
|
@@ -2674,6 +3499,12 @@ function stripTomlStringsAndComments(line) {
|
|
|
2674
3499
|
function isManagedHookEntry(value) {
|
|
2675
3500
|
const trimmed = value.trim();
|
|
2676
3501
|
if (trimmed === OP_INJECT_SCRIPT) return true;
|
|
3502
|
+
if (trimmed === SYNC_SKILLS_SCRIPT) return true;
|
|
3503
|
+
if (trimmed === PROVISION_BMAD_SKILLS_SCRIPT) return true;
|
|
3504
|
+
if (/sync-skills(?:\.py)?["']?\s+--scope project/.test(trimmed)) return true;
|
|
3505
|
+
if (/provision-bmad-skills\.py/.test(trimmed)) return true;
|
|
3506
|
+
if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
3507
|
+
if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2677
3508
|
return /link-agentfiles\.sh'?\s*$/.test(trimmed);
|
|
2678
3509
|
}
|
|
2679
3510
|
function normalizeHookScript(script) {
|
|
@@ -2776,12 +3607,18 @@ function upsertLinkAgentfilesHooks(text2) {
|
|
|
2776
3607
|
function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
2777
3608
|
const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
|
|
2778
3609
|
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
3610
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
|
|
3611
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-provision-bmad\]$/, void 0, { includePrecedingComments: false });
|
|
3612
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3613
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3614
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
|
|
2779
3615
|
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
3616
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
|
|
2780
3617
|
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
2781
3618
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2782
3619
|
}
|
|
2783
3620
|
function readProjectJson(ctx) {
|
|
2784
|
-
return tryParseJson(safeReadText(
|
|
3621
|
+
return tryParseJson(safeReadText(join15(ctx.repoRoot, ".project.json")));
|
|
2785
3622
|
}
|
|
2786
3623
|
function boolSetting(value, fallback) {
|
|
2787
3624
|
if (typeof value === "boolean") return value;
|
|
@@ -2819,7 +3656,7 @@ function canonicalProjectJson(ctx) {
|
|
|
2819
3656
|
role.agentId || `${slug}-${role.role}`,
|
|
2820
3657
|
{
|
|
2821
3658
|
role: role.role,
|
|
2822
|
-
role_dir:
|
|
3659
|
+
role_dir: relative2(ctx.repoRoot, role.roleDir)
|
|
2823
3660
|
}
|
|
2824
3661
|
])
|
|
2825
3662
|
);
|
|
@@ -2856,12 +3693,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2856
3693
|
};
|
|
2857
3694
|
}
|
|
2858
3695
|
function projectJsonFinding(ctx) {
|
|
2859
|
-
const projectPath =
|
|
2860
|
-
const planeJsonPath =
|
|
3696
|
+
const projectPath = join15(ctx.repoRoot, ".project.json");
|
|
3697
|
+
const planeJsonPath = join15(ctx.repoRoot, ".plane.json");
|
|
2861
3698
|
const details = [];
|
|
2862
3699
|
const data = readProjectJson(ctx);
|
|
2863
3700
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2864
|
-
if (!
|
|
3701
|
+
if (!existsSync10(projectPath)) {
|
|
2865
3702
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2866
3703
|
}
|
|
2867
3704
|
if (!data) {
|
|
@@ -2879,8 +3716,8 @@ function projectJsonFinding(ctx) {
|
|
|
2879
3716
|
continue;
|
|
2880
3717
|
}
|
|
2881
3718
|
if (agent.role !== role.role) details.push(`agents.${role.agentId}.role should be ${role.role}`);
|
|
2882
|
-
if (agent.role_dir !==
|
|
2883
|
-
details.push(`agents.${role.agentId}.role_dir should be ${
|
|
3719
|
+
if (agent.role_dir !== relative2(ctx.repoRoot, role.roleDir)) {
|
|
3720
|
+
details.push(`agents.${role.agentId}.role_dir should be ${relative2(ctx.repoRoot, role.roleDir)}`);
|
|
2884
3721
|
}
|
|
2885
3722
|
}
|
|
2886
3723
|
const ticketProvider = data.ticket_provider ?? {};
|
|
@@ -2896,7 +3733,7 @@ function projectJsonFinding(ctx) {
|
|
|
2896
3733
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2897
3734
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2898
3735
|
}
|
|
2899
|
-
if (
|
|
3736
|
+
if (existsSync10(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2900
3737
|
return {
|
|
2901
3738
|
id: "sot.project-json",
|
|
2902
3739
|
title: "Canonical .project.json",
|
|
@@ -2909,8 +3746,7 @@ function projectJsonFinding(ctx) {
|
|
|
2909
3746
|
function renderSoul(role) {
|
|
2910
3747
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
2911
3748
|
const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` : "Direct and brief.";
|
|
2912
|
-
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat
|
|
2913
|
-
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
3749
|
+
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checks runtime health; when this repo opts into reconciliation (\`automation.reconcile.enabled\` in repo-root \`.project.json\`), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
|
|
2914
3750
|
return `# ${role.displayName || role.agentId}
|
|
2915
3751
|
|
|
2916
3752
|
You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisioned to work inside the
|
|
@@ -2929,7 +3765,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
|
|
|
2929
3765
|
|
|
2930
3766
|
## Scope
|
|
2931
3767
|
|
|
2932
|
-
You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME is the
|
|
3768
|
+
You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME is the ignored local directory at \`./runtime/\`, which \`~/.hermes/profiles/${role.profileName || role.agentId}\` projects into (so \`--profile\` invocations resolve here too). Secrets, SOUL, memories, skills, sessions, gateway state, and runtime files stay local to that runtime and are never project gitlinks.
|
|
2933
3769
|
|
|
2934
3770
|
## Tone
|
|
2935
3771
|
|
|
@@ -2941,12 +3777,12 @@ ${roleSpecific}
|
|
|
2941
3777
|
|
|
2942
3778
|
## Memory hygiene
|
|
2943
3779
|
|
|
2944
|
-
Your memory is
|
|
3780
|
+
Your memory is stored locally at \`./runtime/memories/\`. Use durable memory deliberately and keep \`memories/MEMORY.md\` current.
|
|
2945
3781
|
`;
|
|
2946
3782
|
}
|
|
2947
3783
|
function renderHermesWrapper(role) {
|
|
2948
3784
|
return `#!/usr/bin/env bash
|
|
2949
|
-
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime
|
|
3785
|
+
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the local runtime.
|
|
2950
3786
|
|
|
2951
3787
|
set -euo pipefail
|
|
2952
3788
|
|
|
@@ -2959,35 +3795,49 @@ if [[ -f "$FLEET_ENV" ]]; then
|
|
|
2959
3795
|
source "$FLEET_ENV"
|
|
2960
3796
|
fi
|
|
2961
3797
|
|
|
2962
|
-
HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN
|
|
2963
|
-
HERMES_OAUTH_FILE="{HERMES_OAUTH_FILE:-{HERMES_FLEET_OAUTH_FILE:-$HOME/.hermes/auth.json}}"
|
|
3798
|
+
HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN:-$HOME/.hermes/hermes-agent/.venv/bin/hermes}}"
|
|
2964
3799
|
CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
|
|
2965
3800
|
|
|
2966
3801
|
FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
2967
3802
|
PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
|
|
2968
|
-
|
|
3803
|
+
|
|
3804
|
+
# Singleton-runtime contract: HERMES_HOME MUST be the named profile dir, never
|
|
3805
|
+
# the raw runtime path. Hermes treats any HERMES_HOME that is neither under
|
|
3806
|
+
# ~/.hermes nor a child of a "profiles" dir as its own standalone root, which
|
|
3807
|
+
# makes get_active_profile_name() report "default" and _global_auth_file_path()
|
|
3808
|
+
# return None -- silently disabling shared fleet auth and giving every agent a
|
|
3809
|
+
# divergent config.yaml. The profile dir is a REAL dir whose shared entries
|
|
3810
|
+
# (config.yaml, .env, skills) symlink to the fleet root and whose person-owned
|
|
3811
|
+
# entries (memories, sessions, state.db, workspace) symlink into $RUNTIME_HOME.
|
|
3812
|
+
HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"
|
|
2969
3813
|
|
|
2970
3814
|
if [[ ! -d "$RUNTIME_HOME" ]]; then
|
|
2971
|
-
echo "hermes: runtime
|
|
2972
|
-
echo " fix:
|
|
3815
|
+
echo "hermes: local runtime not provisioned at $RUNTIME_HOME" >&2
|
|
3816
|
+
echo " fix: run the role's .scripts/20-runtime-repo.sh" >&2
|
|
2973
3817
|
exit 1
|
|
2974
3818
|
fi
|
|
2975
3819
|
|
|
2976
|
-
|
|
3820
|
+
if [[ ! -d "$HERMES_HOME" ]]; then
|
|
3821
|
+
echo "hermes: profile not provisioned at $HERMES_HOME" >&2
|
|
3822
|
+
echo " fix: pj migrate hermes.runtime-singleton" >&2
|
|
3823
|
+
exit 1
|
|
3824
|
+
fi
|
|
3825
|
+
|
|
3826
|
+
exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" CODEX_HOME="$CODEX_HOME" "$HERMES_BIN" "$@"
|
|
2977
3827
|
`.replace(/\u0010/g, "$");
|
|
2978
3828
|
}
|
|
2979
3829
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2980
|
-
if (!
|
|
3830
|
+
if (!existsSync10(sourceDir)) return;
|
|
2981
3831
|
mkdirSync6(targetDir, { recursive: true });
|
|
2982
|
-
for (const entry of
|
|
2983
|
-
const sourcePath =
|
|
3832
|
+
for (const entry of readdirSync3(sourceDir, { withFileTypes: true })) {
|
|
3833
|
+
const sourcePath = join15(sourceDir, entry.name);
|
|
2984
3834
|
if (skip?.(sourcePath)) continue;
|
|
2985
|
-
const targetPath =
|
|
3835
|
+
const targetPath = join15(targetDir, entry.name);
|
|
2986
3836
|
if (entry.isDirectory()) {
|
|
2987
3837
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2988
3838
|
continue;
|
|
2989
3839
|
}
|
|
2990
|
-
if (
|
|
3840
|
+
if (existsSync10(targetPath)) continue;
|
|
2991
3841
|
changedFiles.push(targetPath);
|
|
2992
3842
|
if (!dryRun) {
|
|
2993
3843
|
ensureParent(targetPath);
|
|
@@ -2995,21 +3845,78 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2995
3845
|
}
|
|
2996
3846
|
}
|
|
2997
3847
|
}
|
|
2998
|
-
function
|
|
2999
|
-
const
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3848
|
+
function runtimeSubmodulePath(repoRoot, role) {
|
|
3849
|
+
const rolePath = relative2(repoRoot, role.roleDir).replace(/\\/g, "/");
|
|
3850
|
+
if (!/^agents\/hermes\/[^/]+$/.test(rolePath)) return null;
|
|
3851
|
+
return `${rolePath}/runtime`;
|
|
3852
|
+
}
|
|
3853
|
+
function submoduleSectionHasPath(section, targetPath) {
|
|
3854
|
+
return section.split(/\r?\n/).some((line) => /^\s*path\s*=/.test(line) && line.replace(/^\s*path\s*=\s*/, "").trim() === targetPath);
|
|
3855
|
+
}
|
|
3856
|
+
function hasRuntimeSubmoduleMapping(repoRoot, role) {
|
|
3857
|
+
const gitmodulesPath = join15(repoRoot, ".gitmodules");
|
|
3006
3858
|
const current = safeReadText(gitmodulesPath) ?? "";
|
|
3007
|
-
const
|
|
3008
|
-
|
|
3859
|
+
const sections = current.match(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm) ?? [];
|
|
3860
|
+
const targetPath = runtimeSubmodulePath(repoRoot, role);
|
|
3861
|
+
return Boolean(targetPath && sections.some((section) => submoduleSectionHasPath(section, targetPath)));
|
|
3862
|
+
}
|
|
3863
|
+
function removeRuntimeSubmoduleMapping(repoRoot, role, changedFiles, dryRun) {
|
|
3864
|
+
const gitmodulesPath = join15(repoRoot, ".gitmodules");
|
|
3865
|
+
const current = safeReadText(gitmodulesPath) ?? "";
|
|
3866
|
+
if (!hasRuntimeSubmoduleMapping(repoRoot, role)) return [];
|
|
3867
|
+
const targetPath = runtimeSubmodulePath(repoRoot, role);
|
|
3868
|
+
if (!targetPath) return [];
|
|
3869
|
+
const next = current.replace(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm, (section) => submoduleSectionHasPath(section, targetPath) ? "" : section).replace(/\n{3,}/g, "\n\n").trim();
|
|
3009
3870
|
changedFiles.push(gitmodulesPath);
|
|
3010
|
-
if (!dryRun) writeText(gitmodulesPath,
|
|
3871
|
+
if (!dryRun) writeText(gitmodulesPath, next ? `${next}
|
|
3872
|
+
` : "");
|
|
3011
3873
|
return [gitmodulesPath];
|
|
3012
3874
|
}
|
|
3875
|
+
function retireRuntimeSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
3876
|
+
const runtimePath = runtimeSubmodulePath(repoRoot, role);
|
|
3877
|
+
if (!runtimePath) {
|
|
3878
|
+
return { ok: false, details: [], error: `refusing unsafe runtime path for ${role.roleDir}` };
|
|
3879
|
+
}
|
|
3880
|
+
const probe = spawnSync6("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
3881
|
+
cwd: repoRoot,
|
|
3882
|
+
encoding: "utf8"
|
|
3883
|
+
});
|
|
3884
|
+
if (probe.status !== 0) {
|
|
3885
|
+
return { ok: false, details: [], error: `failed to inspect runtime index at ${runtimePath}: ${probe.stderr.trim() || `exit ${probe.status}`}` };
|
|
3886
|
+
}
|
|
3887
|
+
const details = [];
|
|
3888
|
+
if (probe.stdout.trim()) {
|
|
3889
|
+
details.push(`untrack ${runtimePath}`);
|
|
3890
|
+
if (dryRun) {
|
|
3891
|
+
changedFiles.push(runtimePath);
|
|
3892
|
+
} else {
|
|
3893
|
+
const removal = spawnSync6("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
|
|
3894
|
+
cwd: repoRoot,
|
|
3895
|
+
encoding: "utf8"
|
|
3896
|
+
});
|
|
3897
|
+
if (removal.status !== 0) {
|
|
3898
|
+
return { ok: false, details, error: `failed to untrack ${runtimePath}: ${removal.stderr.trim() || `exit ${removal.status}`}` };
|
|
3899
|
+
}
|
|
3900
|
+
const verification = spawnSync6("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
3901
|
+
cwd: repoRoot,
|
|
3902
|
+
encoding: "utf8"
|
|
3903
|
+
});
|
|
3904
|
+
if (verification.status !== 0 || verification.stdout.trim()) {
|
|
3905
|
+
return {
|
|
3906
|
+
ok: false,
|
|
3907
|
+
details,
|
|
3908
|
+
error: verification.status !== 0 ? `failed to verify untracked runtime ${runtimePath}: ${verification.stderr.trim() || `exit ${verification.status}`}` : `runtime remains tracked after index-only removal: ${runtimePath}`
|
|
3909
|
+
};
|
|
3910
|
+
}
|
|
3911
|
+
changedFiles.push(runtimePath);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
if (hasRuntimeSubmoduleMapping(repoRoot, role)) {
|
|
3915
|
+
details.push(`remove stale .gitmodules mapping for ${runtimePath}`);
|
|
3916
|
+
removeRuntimeSubmoduleMapping(repoRoot, role, changedFiles, dryRun);
|
|
3917
|
+
}
|
|
3918
|
+
return { ok: true, details };
|
|
3919
|
+
}
|
|
3013
3920
|
function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
3014
3921
|
const path = registryPath(homeDir);
|
|
3015
3922
|
const current = safeReadText(path) ?? "# Hermes agent fleet registry.\n# One entry per provisioned agent. Managed by hermes-agent-template/.scripts/80-registry.sh.\nschema_version: 1\nagents: {}\n";
|
|
@@ -3028,9 +3935,11 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
3028
3935
|
project_id: ${ctxEscape(role.ticketProviderBoardId)}
|
|
3029
3936
|
identifier: ${ctxEscape(role.ticketProviderIdentifier)}
|
|
3030
3937
|
runtime_repo: ${ctxEscape(role.runtimeRepo)}
|
|
3938
|
+
bloodbank:
|
|
3939
|
+
gateway_scope: fleet
|
|
3940
|
+
target_agent_id: ${role.agentId}
|
|
3031
3941
|
systemd:
|
|
3032
3942
|
gateway_unit: hermes-${role.agentId}-gateway.service
|
|
3033
|
-
consumer_unit: hermes-${role.agentId}-consumer.service
|
|
3034
3943
|
heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
|
|
3035
3944
|
`;
|
|
3036
3945
|
const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
|
|
@@ -3094,6 +4003,7 @@ function checkUnit(unit) {
|
|
|
3094
4003
|
var BMAD_NPM_PACKAGE = "bmad-method";
|
|
3095
4004
|
var BMAD_TARGET_CHANNEL = "next";
|
|
3096
4005
|
var BMAD_DIST_TAGS_TTL_MS = 60 * 60 * 1e3;
|
|
4006
|
+
var DEFAULT_BMAD_MODULES = ["bmm", "bmb", "cis"];
|
|
3097
4007
|
var BMAD_INSTALL_TOOLS = [
|
|
3098
4008
|
"claude-code",
|
|
3099
4009
|
"codex",
|
|
@@ -3141,7 +4051,53 @@ var BMAD_INSTALL_TOOLS = [
|
|
|
3141
4051
|
"windsurf",
|
|
3142
4052
|
"zencoder"
|
|
3143
4053
|
];
|
|
3144
|
-
function
|
|
4054
|
+
function manifestBmadModules(repoRoot) {
|
|
4055
|
+
const manifestPath = join15(repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
4056
|
+
const raw = safeReadText(manifestPath);
|
|
4057
|
+
if (raw === null) return { status: "absent" };
|
|
4058
|
+
try {
|
|
4059
|
+
const parsed = YAML3.parse(raw);
|
|
4060
|
+
if (!Array.isArray(parsed?.modules)) {
|
|
4061
|
+
return { status: "invalid", error: `${manifestPath} must define a modules array` };
|
|
4062
|
+
}
|
|
4063
|
+
const declared = [];
|
|
4064
|
+
for (const entry of parsed.modules) {
|
|
4065
|
+
const name = typeof entry === "string" ? entry : entry && typeof entry === "object" ? entry.name : void 0;
|
|
4066
|
+
if (typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) {
|
|
4067
|
+
return { status: "invalid", error: `${manifestPath} contains an invalid module entry` };
|
|
4068
|
+
}
|
|
4069
|
+
if (name !== "core" && name !== "custom") declared.push(name);
|
|
4070
|
+
}
|
|
4071
|
+
return { status: "valid", modules: Array.from(new Set(declared)) };
|
|
4072
|
+
} catch (error) {
|
|
4073
|
+
return {
|
|
4074
|
+
status: "invalid",
|
|
4075
|
+
error: `Could not parse ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
4076
|
+
};
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
function configuredBmadModules(repoRoot) {
|
|
4080
|
+
const raw = safeReadText(join15(repoRoot, "_bmad", "config.toml"));
|
|
4081
|
+
if (raw === null) return void 0;
|
|
4082
|
+
const modules = [...raw.matchAll(/^\[modules\.([A-Za-z0-9][A-Za-z0-9_-]*)\]\s*$/gm)].map((match) => match[1]);
|
|
4083
|
+
return Array.from(new Set(modules));
|
|
4084
|
+
}
|
|
4085
|
+
function selectedBmadModules(repoRoot) {
|
|
4086
|
+
const manifest = manifestBmadModules(repoRoot);
|
|
4087
|
+
if (manifest.status === "valid") return manifest.modules;
|
|
4088
|
+
if (manifest.status === "invalid") throw new Error(manifest.error);
|
|
4089
|
+
return configuredBmadModules(repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
4090
|
+
}
|
|
4091
|
+
function requiredBmadSentinels(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
4092
|
+
return [
|
|
4093
|
+
join15("core", "config.yaml"),
|
|
4094
|
+
join15("config.toml"),
|
|
4095
|
+
join15("_config", "manifest.yaml"),
|
|
4096
|
+
...modules.map((module) => join15(module, "config.yaml"))
|
|
4097
|
+
];
|
|
4098
|
+
}
|
|
4099
|
+
function bmadInstallArgs(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
4100
|
+
const installerModules = modules.length ? modules.join(",") : "core";
|
|
3145
4101
|
return [
|
|
3146
4102
|
"-y",
|
|
3147
4103
|
`${BMAD_NPM_PACKAGE}@${BMAD_TARGET_CHANNEL}`,
|
|
@@ -3150,20 +4106,20 @@ function bmadInstallArgs(repoRoot) {
|
|
|
3150
4106
|
"--directory",
|
|
3151
4107
|
repoRoot,
|
|
3152
4108
|
"--modules",
|
|
3153
|
-
|
|
4109
|
+
installerModules,
|
|
3154
4110
|
"--tools",
|
|
3155
4111
|
BMAD_INSTALL_TOOLS.join(",")
|
|
3156
4112
|
];
|
|
3157
4113
|
}
|
|
3158
|
-
function runBmadInstall(repoRoot) {
|
|
3159
|
-
const result = spawnSync6("npx", bmadInstallArgs(repoRoot), { encoding: "utf8" });
|
|
4114
|
+
function runBmadInstall(repoRoot, modules = selectedBmadModules(repoRoot)) {
|
|
4115
|
+
const result = spawnSync6("npx", bmadInstallArgs(repoRoot, modules), { encoding: "utf8" });
|
|
3160
4116
|
if (result.status !== 0) {
|
|
3161
4117
|
return { ok: false, error: result.stderr || result.error?.message || "Unknown error" };
|
|
3162
4118
|
}
|
|
3163
4119
|
return { ok: true };
|
|
3164
4120
|
}
|
|
3165
4121
|
function readInstalledBmadVersion(repoRoot) {
|
|
3166
|
-
const raw = safeReadText(
|
|
4122
|
+
const raw = safeReadText(join15(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
3167
4123
|
if (!raw) return void 0;
|
|
3168
4124
|
try {
|
|
3169
4125
|
const parsed = YAML3.parse(raw);
|
|
@@ -3174,8 +4130,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
3174
4130
|
}
|
|
3175
4131
|
}
|
|
3176
4132
|
function bmadCachePath(homeDir) {
|
|
3177
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
3178
|
-
return
|
|
4133
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join15(homeDir, ".cache");
|
|
4134
|
+
return join15(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
3179
4135
|
}
|
|
3180
4136
|
function readBmadDistTagsCache(homeDir) {
|
|
3181
4137
|
const raw = safeReadText(bmadCachePath(homeDir));
|
|
@@ -3218,7 +4174,7 @@ function resolveBmadDistTags(homeDir) {
|
|
|
3218
4174
|
try {
|
|
3219
4175
|
const path = bmadCachePath(homeDir);
|
|
3220
4176
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
3221
|
-
|
|
4177
|
+
writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
|
|
3222
4178
|
} catch {
|
|
3223
4179
|
}
|
|
3224
4180
|
return { distTags: fetched, stale: false };
|
|
@@ -3258,19 +4214,233 @@ function compareBmadVersions(a, b) {
|
|
|
3258
4214
|
}
|
|
3259
4215
|
return 0;
|
|
3260
4216
|
}
|
|
4217
|
+
var SHARED_PROFILE_ENTRIES = ["config.yaml", ".env", "skills"];
|
|
4218
|
+
var OWNED_PROFILE_ENTRIES = [
|
|
4219
|
+
"memories",
|
|
4220
|
+
"sessions",
|
|
4221
|
+
"workspace",
|
|
4222
|
+
"logs",
|
|
4223
|
+
"cron",
|
|
4224
|
+
"plans",
|
|
4225
|
+
"hooks",
|
|
4226
|
+
"pairing",
|
|
4227
|
+
"audio_cache",
|
|
4228
|
+
"image_cache"
|
|
4229
|
+
];
|
|
4230
|
+
var OWNED_PROFILE_FILES = ["SOUL.md", "state.db", "kanban.db"];
|
|
4231
|
+
function fleetHome(ctx) {
|
|
4232
|
+
return process.env.HERMES_FLEET_HOME || join15(ctx.homeDir, ".hermes");
|
|
4233
|
+
}
|
|
4234
|
+
function fleetBinPath(ctx) {
|
|
4235
|
+
const candidates = [
|
|
4236
|
+
process.env.HERMES_FLEET_BIN,
|
|
4237
|
+
join15(fleetHome(ctx), "hermes-agent", ".venv", "bin", "hermes"),
|
|
4238
|
+
join15(fleetHome(ctx), "hermes-agent", "venv", "bin", "hermes"),
|
|
4239
|
+
join15(ctx.homeDir, ".local", "bin", "hermes")
|
|
4240
|
+
].filter(Boolean);
|
|
4241
|
+
return candidates.find((candidate) => existsSync10(candidate)) ?? "";
|
|
4242
|
+
}
|
|
4243
|
+
function singletonPlan(ctx, role) {
|
|
4244
|
+
const fleetRoot = fleetHome(ctx);
|
|
4245
|
+
const profileName = role.profileName || role.agentId;
|
|
4246
|
+
const profileDir = join15(fleetRoot, "profiles", profileName);
|
|
4247
|
+
const runtimeDir = join15(role.roleDir, "runtime");
|
|
4248
|
+
const links = [];
|
|
4249
|
+
for (const entry of SHARED_PROFILE_ENTRIES) {
|
|
4250
|
+
links.push({ path: join15(profileDir, entry), target: join15(fleetRoot, entry), ensureTargetDir: entry === "skills" });
|
|
4251
|
+
}
|
|
4252
|
+
for (const entry of OWNED_PROFILE_ENTRIES) {
|
|
4253
|
+
links.push({ path: join15(profileDir, entry), target: join15(runtimeDir, entry), ensureTargetDir: true });
|
|
4254
|
+
}
|
|
4255
|
+
for (const entry of OWNED_PROFILE_FILES) {
|
|
4256
|
+
links.push({ path: join15(profileDir, entry), target: join15(runtimeDir, entry), ensureTargetDir: false });
|
|
4257
|
+
}
|
|
4258
|
+
const sharedSeeds = ["config.yaml", "auth.json", ".env"].map((entry) => ({
|
|
4259
|
+
rootPath: join15(fleetRoot, entry),
|
|
4260
|
+
runtimePath: join15(runtimeDir, entry)
|
|
4261
|
+
}));
|
|
4262
|
+
return { fleetRoot, profileDir, runtimeDir, links, sharedSeeds };
|
|
4263
|
+
}
|
|
4264
|
+
function isDanglingLink(path) {
|
|
4265
|
+
try {
|
|
4266
|
+
return lstatSync2(path).isSymbolicLink() && !existsSync10(path);
|
|
4267
|
+
} catch {
|
|
4268
|
+
return false;
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
function linkState(path, target) {
|
|
4272
|
+
let stat;
|
|
4273
|
+
try {
|
|
4274
|
+
stat = lstatSync2(path);
|
|
4275
|
+
} catch {
|
|
4276
|
+
return "missing";
|
|
4277
|
+
}
|
|
4278
|
+
if (!stat.isSymbolicLink()) return "not-a-symlink";
|
|
4279
|
+
try {
|
|
4280
|
+
return readlinkSync(path) === target ? "ok" : "wrong-target";
|
|
4281
|
+
} catch {
|
|
4282
|
+
return "wrong-target";
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
function realOrSelf(path) {
|
|
4286
|
+
try {
|
|
4287
|
+
return realpathSync(path);
|
|
4288
|
+
} catch {
|
|
4289
|
+
return path;
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
function profileUnits(role) {
|
|
4293
|
+
return [
|
|
4294
|
+
`hermes-${role.agentId}-gateway.service`,
|
|
4295
|
+
`hermes-${role.agentId}-heartbeat.service`,
|
|
4296
|
+
`hermes-${role.agentId}-heartbeat.timer`,
|
|
4297
|
+
`hermes-${role.agentId}-checkpoint.service`
|
|
4298
|
+
];
|
|
4299
|
+
}
|
|
4300
|
+
function readRegistry(registryPath2) {
|
|
4301
|
+
const raw = safeReadText(registryPath2);
|
|
4302
|
+
if (raw === null) return null;
|
|
4303
|
+
try {
|
|
4304
|
+
const doc = YAML3.parse(raw);
|
|
4305
|
+
return doc?.agents ?? {};
|
|
4306
|
+
} catch {
|
|
4307
|
+
return null;
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
function declaredAgentIds(repoRoot) {
|
|
4311
|
+
return declaredAgentEntries(repoRoot).map(([agentId]) => agentId);
|
|
4312
|
+
}
|
|
4313
|
+
function declaredAgentEntries(repoRoot) {
|
|
4314
|
+
const raw = safeReadText(join15(repoRoot, ".project.json"));
|
|
4315
|
+
if (raw === null) return [];
|
|
4316
|
+
try {
|
|
4317
|
+
const doc = JSON.parse(raw);
|
|
4318
|
+
return Object.entries(doc.agents ?? {}).map(([agentId, entry]) => [agentId, entry ?? {}]);
|
|
4319
|
+
} catch {
|
|
4320
|
+
return [];
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
function ownedRegistryEntries(registry, repoRoot) {
|
|
4324
|
+
const want = realOrSelf(repoRoot);
|
|
4325
|
+
const owned = [];
|
|
4326
|
+
for (const [agentId, raw] of Object.entries(registry)) {
|
|
4327
|
+
const entry = raw ?? {};
|
|
4328
|
+
const roleDir = String(entry.role_dir ?? "");
|
|
4329
|
+
if (!roleDir) continue;
|
|
4330
|
+
if (realOrSelf(dirname7(dirname7(dirname7(roleDir)))) !== want) continue;
|
|
4331
|
+
owned.push([agentId, entry]);
|
|
4332
|
+
}
|
|
4333
|
+
return owned;
|
|
4334
|
+
}
|
|
4335
|
+
function unprovisionedRoleAgents(registry, repoRoot, canonical) {
|
|
4336
|
+
const blockers = /* @__PURE__ */ new Map();
|
|
4337
|
+
const record = (agentId, roleDir, source) => {
|
|
4338
|
+
const current = blockers.get(agentId) ?? { roleDir, sources: /* @__PURE__ */ new Set() };
|
|
4339
|
+
if (!current.roleDir && roleDir) current.roleDir = roleDir;
|
|
4340
|
+
current.sources.add(source);
|
|
4341
|
+
blockers.set(agentId, current);
|
|
4342
|
+
};
|
|
4343
|
+
for (const [agentId, entry] of ownedRegistryEntries(registry, repoRoot)) {
|
|
4344
|
+
if (canonical.has(agentId)) continue;
|
|
4345
|
+
const roleDir = String(entry.role_dir ?? "");
|
|
4346
|
+
if (!roleDir || !existsSync10(join15(roleDir, "role.yaml"))) record(agentId, roleDir, "registry");
|
|
4347
|
+
}
|
|
4348
|
+
for (const [agentId, entry] of declaredAgentEntries(repoRoot)) {
|
|
4349
|
+
if (canonical.has(agentId)) continue;
|
|
4350
|
+
const configured = String(entry.role_dir ?? "");
|
|
4351
|
+
const roleDir = configured ? resolve3(repoRoot, configured) : "";
|
|
4352
|
+
if (!roleDir || !existsSync10(join15(roleDir, "role.yaml"))) record(agentId, roleDir, ".project.json");
|
|
4353
|
+
}
|
|
4354
|
+
return [...blockers.entries()].map(([agentId, value]) => ({
|
|
4355
|
+
agentId,
|
|
4356
|
+
roleDir: value.roleDir,
|
|
4357
|
+
sources: [...value.sources]
|
|
4358
|
+
}));
|
|
4359
|
+
}
|
|
4360
|
+
function dropDeclaredAgent(ctx, agentId, changedFiles, details) {
|
|
4361
|
+
const path = join15(ctx.repoRoot, ".project.json");
|
|
4362
|
+
const raw = safeReadText(path);
|
|
4363
|
+
if (raw === null) return;
|
|
4364
|
+
let doc;
|
|
4365
|
+
try {
|
|
4366
|
+
doc = JSON.parse(raw);
|
|
4367
|
+
} catch {
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
if (!doc.agents || !(agentId in doc.agents)) return;
|
|
4371
|
+
delete doc.agents[agentId];
|
|
4372
|
+
details.push(`drop agent "${agentId}" from .project.json`);
|
|
4373
|
+
changedFiles.push(path);
|
|
4374
|
+
if (!ctx.dryRun) writeText(path, `${JSON.stringify(doc, null, 2)}
|
|
4375
|
+
`);
|
|
4376
|
+
}
|
|
4377
|
+
function rewriteLauncher(text2) {
|
|
4378
|
+
let next = text2.replace(/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m, 'HERMES_HOME="$FLEET_HOME/profiles/$PROFILE_NAME"');
|
|
4379
|
+
next = next.replace(/^HERMES_OAUTH_FILE=.*\n/m, "");
|
|
4380
|
+
next = next.replace(/\s*HERMES_OAUTH_FILE="\$HERMES_OAUTH_FILE"/g, "");
|
|
4381
|
+
next = next.replace(
|
|
4382
|
+
/^.*\/home\/delorenj\/code\/hermes-agent\/\.venv\/bin\/hermes.*$/m,
|
|
4383
|
+
(line) => line.replace("/home/delorenj/code/hermes-agent/.venv/bin/hermes", "$HOME/.hermes/hermes-agent/.venv/bin/hermes")
|
|
4384
|
+
);
|
|
4385
|
+
return next;
|
|
4386
|
+
}
|
|
4387
|
+
function isValidOpReference(value) {
|
|
4388
|
+
if (!value.startsWith("op://") || /[\[\]{}<>]/.test(value)) return false;
|
|
4389
|
+
const withoutScheme = value.slice("op://".length);
|
|
4390
|
+
const fragmentIndex = withoutScheme.indexOf("#");
|
|
4391
|
+
if (fragmentIndex >= 0) return false;
|
|
4392
|
+
const queryIndex = withoutScheme.indexOf("?");
|
|
4393
|
+
const pathPart = queryIndex >= 0 ? withoutScheme.slice(0, queryIndex) : withoutScheme;
|
|
4394
|
+
const queryPart = queryIndex >= 0 ? withoutScheme.slice(queryIndex + 1) : "";
|
|
4395
|
+
const parts = pathPart.split("/");
|
|
4396
|
+
if (parts.length < 3 || parts.length > 4 || parts.some((part) => !part)) return false;
|
|
4397
|
+
try {
|
|
4398
|
+
for (const part of parts) decodeURIComponent(part);
|
|
4399
|
+
} catch {
|
|
4400
|
+
return false;
|
|
4401
|
+
}
|
|
4402
|
+
if (queryIndex >= 0 && !/^attribute=[A-Za-z0-9._~-]+$/.test(queryPart)) return false;
|
|
4403
|
+
return true;
|
|
4404
|
+
}
|
|
4405
|
+
function malformedOpReferences(text2) {
|
|
4406
|
+
const occurrences = [];
|
|
4407
|
+
const lines = text2.split("\n");
|
|
4408
|
+
for (let index = 0; index < lines.length; index++) {
|
|
4409
|
+
const line = lines[index];
|
|
4410
|
+
for (const match of line.matchAll(/op:\/\/[^\s"'`]+/g)) {
|
|
4411
|
+
const value = match[0];
|
|
4412
|
+
if (!isValidOpReference(value)) {
|
|
4413
|
+
occurrences.push({ line: index + 1, value, commentOnly: line.trimStart().startsWith("#") });
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
return occurrences;
|
|
4418
|
+
}
|
|
4419
|
+
function removeMalformedCommentOpReferences(text2) {
|
|
4420
|
+
let changed = false;
|
|
4421
|
+
const lines = text2.split("\n").map((line) => {
|
|
4422
|
+
if (!line.trimStart().startsWith("#")) return line;
|
|
4423
|
+
return line.replace(/op:\/\/[^\s"'`]+/g, (value) => {
|
|
4424
|
+
if (isValidOpReference(value)) return value;
|
|
4425
|
+
changed = true;
|
|
4426
|
+
return "<invalid 1Password reference removed by pjangler>";
|
|
4427
|
+
});
|
|
4428
|
+
});
|
|
4429
|
+
return { text: lines.join("\n"), changed };
|
|
4430
|
+
}
|
|
3261
4431
|
var RULES = [
|
|
3262
4432
|
{
|
|
3263
4433
|
id: "mise.config-root",
|
|
3264
4434
|
title: "mise config_root + AGENTS link hooks",
|
|
3265
4435
|
audit: (ctx) => {
|
|
3266
|
-
const misePath =
|
|
3267
|
-
if (!
|
|
4436
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
4437
|
+
if (!existsSync10(misePath)) {
|
|
3268
4438
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
3269
4439
|
}
|
|
3270
4440
|
const text2 = readText(misePath);
|
|
3271
4441
|
const details = [];
|
|
3272
|
-
const linkAgentfilesPath =
|
|
3273
|
-
if (!
|
|
4442
|
+
const linkAgentfilesPath = join15(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
4443
|
+
if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
3274
4444
|
const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
3275
4445
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
3276
4446
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -3288,10 +4458,10 @@ var RULES = [
|
|
|
3288
4458
|
};
|
|
3289
4459
|
},
|
|
3290
4460
|
migrate: (ctx, finding) => {
|
|
3291
|
-
const path =
|
|
4461
|
+
const path = join15(ctx.repoRoot, "mise.toml");
|
|
3292
4462
|
const changedFiles = [];
|
|
3293
4463
|
const details = [];
|
|
3294
|
-
if (!
|
|
4464
|
+
if (!existsSync10(path)) {
|
|
3295
4465
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3296
4466
|
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
4467
|
}
|
|
@@ -3307,7 +4477,7 @@ var RULES = [
|
|
|
3307
4477
|
if (!ctx.dryRun) writeText(path, next);
|
|
3308
4478
|
text2 = next;
|
|
3309
4479
|
}
|
|
3310
|
-
const linkAgentfilesPath =
|
|
4480
|
+
const linkAgentfilesPath = join15(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3311
4481
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
3312
4482
|
if (expectedScript === void 0) {
|
|
3313
4483
|
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 +4486,7 @@ var RULES = [
|
|
|
3316
4486
|
changedFiles.push(linkAgentfilesPath);
|
|
3317
4487
|
if (!ctx.dryRun) {
|
|
3318
4488
|
writeText(linkAgentfilesPath, expectedScript);
|
|
3319
|
-
|
|
4489
|
+
chmodSync4(linkAgentfilesPath, 493);
|
|
3320
4490
|
}
|
|
3321
4491
|
}
|
|
3322
4492
|
return {
|
|
@@ -3334,13 +4504,13 @@ var RULES = [
|
|
|
3334
4504
|
title: "managed mise versioning block",
|
|
3335
4505
|
audit: (ctx) => {
|
|
3336
4506
|
const details = [];
|
|
3337
|
-
const misePath =
|
|
3338
|
-
const versioningPath =
|
|
3339
|
-
const manifestPath =
|
|
4507
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
4508
|
+
const versioningPath = join15(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
4509
|
+
const manifestPath = join15(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3340
4510
|
const text2 = safeReadText(misePath);
|
|
3341
4511
|
if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
3342
|
-
if (!
|
|
3343
|
-
if (!
|
|
4512
|
+
if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
4513
|
+
if (!existsSync10(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
3344
4514
|
return {
|
|
3345
4515
|
id: "mise.versioning",
|
|
3346
4516
|
title: "managed mise versioning block",
|
|
@@ -3353,8 +4523,8 @@ var RULES = [
|
|
|
3353
4523
|
migrate: (ctx, finding) => {
|
|
3354
4524
|
const changedFiles = [];
|
|
3355
4525
|
const details = [];
|
|
3356
|
-
const misePath =
|
|
3357
|
-
if (!
|
|
4526
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
4527
|
+
if (!existsSync10(misePath)) {
|
|
3358
4528
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3359
4529
|
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
4530
|
}
|
|
@@ -3363,46 +4533,266 @@ var RULES = [
|
|
|
3363
4533
|
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
3364
4534
|
}
|
|
3365
4535
|
}
|
|
3366
|
-
const currentMise = readText(misePath);
|
|
3367
|
-
let cleanedMise = currentMise;
|
|
3368
|
-
if (!currentMise.includes("# >>> mise-versioning >>>")) {
|
|
3369
|
-
const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
|
|
3370
|
-
for (const taskName of taskNames) {
|
|
3371
|
-
const escaped = taskName.replace(/:/g, "\\:");
|
|
3372
|
-
const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
|
|
3373
|
-
cleanedMise = removeTomlSection(cleanedMise, headerPattern);
|
|
4536
|
+
const currentMise = readText(misePath);
|
|
4537
|
+
let cleanedMise = currentMise;
|
|
4538
|
+
if (!currentMise.includes("# >>> mise-versioning >>>")) {
|
|
4539
|
+
const taskNames = ["version", "version:bump", "version:bump-patch", "version:bump-minor", "version:bump-major", "version:check", "version:sync"];
|
|
4540
|
+
for (const taskName of taskNames) {
|
|
4541
|
+
const escaped = taskName.replace(/:/g, "\\:");
|
|
4542
|
+
const headerPattern = new RegExp(`^\\[tasks\\.(?:"${escaped}"|'${escaped}'|${escaped})\\]$`);
|
|
4543
|
+
cleanedMise = removeTomlSection(cleanedMise, headerPattern);
|
|
4544
|
+
}
|
|
4545
|
+
}
|
|
4546
|
+
const nextMise = replaceOrAppendManagedBlock(cleanedMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
4547
|
+
if (nextMise !== currentMise) {
|
|
4548
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
4549
|
+
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
4550
|
+
}
|
|
4551
|
+
const versioningPath = join15(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
4552
|
+
const expectedScript = templateVersioningScript(ctx);
|
|
4553
|
+
if (expectedScript === void 0) {
|
|
4554
|
+
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: [] };
|
|
4555
|
+
}
|
|
4556
|
+
if (safeReadText(versioningPath) !== expectedScript) {
|
|
4557
|
+
changedFiles.push(versioningPath);
|
|
4558
|
+
if (!ctx.dryRun) {
|
|
4559
|
+
writeText(versioningPath, expectedScript);
|
|
4560
|
+
chmodSync4(versioningPath, 493);
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
const manifestPath = join15(ctx.repoRoot, ".mise", "version-files.conf");
|
|
4564
|
+
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
4565
|
+
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
4566
|
+
changedFiles.push(manifestPath);
|
|
4567
|
+
if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
|
|
4568
|
+
}
|
|
4569
|
+
return {
|
|
4570
|
+
id: finding.id,
|
|
4571
|
+
title: finding.title,
|
|
4572
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
4573
|
+
summary: changedFiles.length ? "Versioning block/script/manifest normalized" : "No changes required",
|
|
4574
|
+
changedFiles,
|
|
4575
|
+
details: []
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
},
|
|
4579
|
+
{
|
|
4580
|
+
id: "skills.project-manifest",
|
|
4581
|
+
title: "Skillex project skills manifest",
|
|
4582
|
+
audit: (ctx) => {
|
|
4583
|
+
const details = [];
|
|
4584
|
+
const manifestPath = join15(ctx.repoRoot, ".agents", "skills.json");
|
|
4585
|
+
const legacyDir = join15(ctx.repoRoot, ".agents", "skills");
|
|
4586
|
+
const localExamplePath = join15(ctx.repoRoot, ".agents", "local.example.json");
|
|
4587
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
4588
|
+
let fixable = true;
|
|
4589
|
+
let expectedBmad = [];
|
|
4590
|
+
try {
|
|
4591
|
+
expectedBmad = canonicalBmadSkillEntries(ctx);
|
|
4592
|
+
} catch (error) {
|
|
4593
|
+
details.push(
|
|
4594
|
+
`BMAD Skillex pack ${BMAD_PACK_VERSION} is not trusted at ${bmadPackRoot(ctx)}: ${error instanceof Error ? error.message : String(error)}`
|
|
4595
|
+
);
|
|
4596
|
+
fixable = false;
|
|
4597
|
+
}
|
|
4598
|
+
const expectedByName = new Map(expectedBmad.map((entry) => [entry.name, fileURLToPath4(entry.source)]));
|
|
4599
|
+
const expectedNames = new Set(expectedByName.keys());
|
|
4600
|
+
const packRoot = bmadPackRoot(ctx);
|
|
4601
|
+
const manifest = tryParseJson(safeReadText(manifestPath));
|
|
4602
|
+
if (!manifest) {
|
|
4603
|
+
details.push(".agents/skills.json missing or invalid JSON");
|
|
4604
|
+
} else {
|
|
4605
|
+
if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
|
|
4606
|
+
if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
|
|
4607
|
+
if (!Array.isArray(manifest.skills)) {
|
|
4608
|
+
details.push(".agents/skills.json should define a skills array");
|
|
4609
|
+
} else {
|
|
4610
|
+
const actualBmad = new Map(
|
|
4611
|
+
manifest.skills.filter((entry) => Boolean(entry) && typeof entry === "object" && isPackManagedManifestEntry(entry, expectedNames, packRoot)).map((entry) => [String(entry.name), String(entry.source ?? "")])
|
|
4612
|
+
);
|
|
4613
|
+
const stale = expectedBmad.filter((entry) => actualBmad.get(entry.name) !== entry.source);
|
|
4614
|
+
if (stale.length > 0 || actualBmad.size !== expectedBmad.length) {
|
|
4615
|
+
details.push(`.agents/skills.json should record all ${expectedBmad.length} BMAD ${BMAD_PACK_VERSION} pack entries as file:// sources`);
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
const invalidBmadLinkNames = /* @__PURE__ */ new Set();
|
|
4620
|
+
if (existsSync10(legacyDir)) {
|
|
4621
|
+
for (const name of readdirSync3(legacyDir)) {
|
|
4622
|
+
const expected = expectedByName.get(name);
|
|
4623
|
+
const path = join15(legacyDir, name);
|
|
4624
|
+
let linkTargetsPack = false;
|
|
4625
|
+
try {
|
|
4626
|
+
linkTargetsPack = lstatSync2(path).isSymbolicLink() && isContainedBy(packRoot, resolve3(dirname7(path), readlinkSync(path)));
|
|
4627
|
+
} catch {
|
|
4628
|
+
linkTargetsPack = false;
|
|
4629
|
+
}
|
|
4630
|
+
if (!expected && !linkTargetsPack) continue;
|
|
4631
|
+
try {
|
|
4632
|
+
if (!expected || !lstatSync2(path).isSymbolicLink() || resolve3(dirname7(path), readlinkSync(path)) !== expected) invalidBmadLinkNames.add(name);
|
|
4633
|
+
} catch {
|
|
4634
|
+
invalidBmadLinkNames.add(name);
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
for (const [name, expected] of expectedByName) {
|
|
4638
|
+
const path = join15(legacyDir, name);
|
|
4639
|
+
try {
|
|
4640
|
+
if (!lstatSync2(path).isSymbolicLink() || resolve3(dirname7(path), readlinkSync(path)) !== expected) invalidBmadLinkNames.add(name);
|
|
4641
|
+
} catch {
|
|
4642
|
+
invalidBmadLinkNames.add(name);
|
|
4643
|
+
}
|
|
4644
|
+
}
|
|
4645
|
+
} else {
|
|
4646
|
+
for (const name of expectedByName.keys()) invalidBmadLinkNames.add(name);
|
|
4647
|
+
}
|
|
4648
|
+
if (invalidBmadLinkNames.size > 0) {
|
|
4649
|
+
details.push(`${invalidBmadLinkNames.size} managed BMAD skill path(s) should be symlinks into the ${BMAD_PACK_VERSION} pack`);
|
|
4650
|
+
}
|
|
4651
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
4652
|
+
if (existsSync10(join15(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
|
|
4653
|
+
}
|
|
4654
|
+
const localExample = tryParseJson(safeReadText(localExamplePath));
|
|
4655
|
+
if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
|
|
4656
|
+
details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
|
|
4657
|
+
}
|
|
4658
|
+
const mise = safeReadText(misePath);
|
|
4659
|
+
if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run the shipped project-local sync-skills.py engine via config_root");
|
|
4660
|
+
if (!mise?.includes(PROVISION_BMAD_SKILLS_SCRIPT)) details.push("mise.toml should provision pinned BMAD pack links before syncing skills");
|
|
4661
|
+
if (mise?.includes(SYNC_SKILLS_SCRIPT) && mise.includes(PROVISION_BMAD_SKILLS_SCRIPT) && mise.indexOf(PROVISION_BMAD_SKILLS_SCRIPT) > mise.indexOf(SYNC_SKILLS_SCRIPT)) {
|
|
4662
|
+
details.push("mise.toml should run the BMAD provisioner before project skill sync");
|
|
4663
|
+
}
|
|
4664
|
+
if (mise?.includes('script = "sync-skills.py --scope project"') || mise?.includes('run = "sync-skills.py --scope project"')) {
|
|
4665
|
+
details.push("mise.toml still invokes the missing bare sync-skills.py executable");
|
|
4666
|
+
}
|
|
4667
|
+
if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
|
|
4668
|
+
if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
|
|
4669
|
+
if (!mise?.includes('depends = ["skills-provision-bmad"]')) details.push("skills-sync task should depend on skills-provision-bmad");
|
|
4670
|
+
for (const [rel, label] of [
|
|
4671
|
+
[".mise/scripts/provision-bmad-skills.py", "BMAD Skillex provisioning script"],
|
|
4672
|
+
[".mise/scripts/sync-skills.py", "Project-local skills sync engine"]
|
|
4673
|
+
]) {
|
|
4674
|
+
const target = join15(ctx.repoRoot, rel);
|
|
4675
|
+
const expected = templateCommonProjectText(ctx, rel);
|
|
4676
|
+
const stat = lstatIfPresent(target);
|
|
4677
|
+
if (!stat || !stat.isFile() || stat.isSymbolicLink()) {
|
|
4678
|
+
details.push(`${label} is missing or unsafe`);
|
|
4679
|
+
if (stat) fixable = false;
|
|
4680
|
+
} else {
|
|
4681
|
+
if (expected === void 0 || safeReadText(target) !== expected) details.push(`${label} differs from the shipped template`);
|
|
4682
|
+
if ((Number(stat.mode) & 73) === 0) details.push(`${label} is not executable`);
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
const topologyIssues = projectSkillTopologyIssues(ctx.repoRoot);
|
|
4686
|
+
if (topologyIssues.length) {
|
|
4687
|
+
details.push(...topologyIssues.map((issue) => `CLI skill topology: ${issue}`));
|
|
4688
|
+
fixable = false;
|
|
4689
|
+
}
|
|
4690
|
+
if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
|
|
4691
|
+
details.push("mise.toml still contains legacy skill-link wiring");
|
|
4692
|
+
}
|
|
4693
|
+
return {
|
|
4694
|
+
id: "skills.project-manifest",
|
|
4695
|
+
title: "Skillex project skills manifest",
|
|
4696
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
4697
|
+
summary: details.length === 0 ? "Skillex skills manifest parity verified" : `${details.length} Skillex migration issue(s) detected`,
|
|
4698
|
+
details,
|
|
4699
|
+
fixable
|
|
4700
|
+
};
|
|
4701
|
+
},
|
|
4702
|
+
migrate: (ctx, finding) => {
|
|
4703
|
+
const changedFiles = [];
|
|
4704
|
+
const details = [];
|
|
4705
|
+
const manifestPath = join15(ctx.repoRoot, ".agents", "skills.json");
|
|
4706
|
+
const localExamplePath = join15(ctx.repoRoot, ".agents", "local.example.json");
|
|
4707
|
+
const misePath = join15(ctx.repoRoot, "mise.toml");
|
|
4708
|
+
const provisionScriptPath = join15(ctx.repoRoot, ".mise", "scripts", "provision-bmad-skills.py");
|
|
4709
|
+
const syncScriptPath = join15(ctx.repoRoot, ".mise", "scripts", "sync-skills.py");
|
|
4710
|
+
const expectedProvisionScript = templateCommonProjectText(ctx, ".mise/scripts/provision-bmad-skills.py");
|
|
4711
|
+
const expectedSyncScript = templateCommonProjectText(ctx, ".mise/scripts/sync-skills.py");
|
|
4712
|
+
const topologyIssues = projectSkillTopologyIssues(ctx.repoRoot);
|
|
4713
|
+
if (topologyIssues.length) {
|
|
4714
|
+
return {
|
|
4715
|
+
id: finding.id,
|
|
4716
|
+
title: finding.title,
|
|
4717
|
+
status: "blocked",
|
|
4718
|
+
summary: "Unsafe project CLI skill topology must be repaired manually",
|
|
4719
|
+
changedFiles,
|
|
4720
|
+
details: topologyIssues
|
|
4721
|
+
};
|
|
4722
|
+
}
|
|
4723
|
+
if (!expectedProvisionScript || !expectedSyncScript) {
|
|
4724
|
+
return {
|
|
4725
|
+
id: finding.id,
|
|
4726
|
+
title: finding.title,
|
|
4727
|
+
status: "blocked",
|
|
4728
|
+
summary: "pjangler install is missing a shipped skills executable",
|
|
4729
|
+
changedFiles,
|
|
4730
|
+
details: [
|
|
4731
|
+
...!expectedProvisionScript ? ["Missing BMAD Skillex provisioning script template"] : [],
|
|
4732
|
+
...!expectedSyncScript ? ["Missing project-local skills sync engine template"] : []
|
|
4733
|
+
]
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
const unsafeScriptTargets = [provisionScriptPath, syncScriptPath].filter((path) => {
|
|
4737
|
+
const stat = lstatIfPresent(path);
|
|
4738
|
+
return Boolean(stat && (!stat.isFile() || stat.isSymbolicLink()));
|
|
4739
|
+
});
|
|
4740
|
+
if (unsafeScriptTargets.length) {
|
|
4741
|
+
return {
|
|
4742
|
+
id: finding.id,
|
|
4743
|
+
title: finding.title,
|
|
4744
|
+
status: "blocked",
|
|
4745
|
+
summary: "Refusing non-regular managed skills executable target",
|
|
4746
|
+
changedFiles,
|
|
4747
|
+
details: unsafeScriptTargets.map((path) => `${path} must be removed or repaired manually`)
|
|
4748
|
+
};
|
|
4749
|
+
}
|
|
4750
|
+
const provisioned = provisionBmadSkills(ctx);
|
|
4751
|
+
if (!provisioned.ok) {
|
|
4752
|
+
return {
|
|
4753
|
+
id: finding.id,
|
|
4754
|
+
title: finding.title,
|
|
4755
|
+
status: "blocked",
|
|
4756
|
+
summary: `BMAD Skillex pack ${BMAD_PACK_VERSION} is unavailable`,
|
|
4757
|
+
changedFiles,
|
|
4758
|
+
details: [provisioned.error ?? "Unknown BMAD pack error"]
|
|
4759
|
+
};
|
|
4760
|
+
}
|
|
4761
|
+
changedFiles.push(...provisioned.changedFiles);
|
|
4762
|
+
if (provisioned.changedFiles.includes(manifestPath)) details.push(`Recorded BMAD pack ${BMAD_PACK_VERSION} in .agents/skills.json`);
|
|
4763
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
4764
|
+
const path = join15(ctx.repoRoot, rel);
|
|
4765
|
+
if (existsSync10(path)) {
|
|
4766
|
+
changedFiles.push(path);
|
|
4767
|
+
if (!ctx.dryRun) unlinkSync3(path);
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
const templateLocalExample = templateCommonProjectText(ctx, ".agents/local.example.json");
|
|
4771
|
+
const currentLocalExample = safeReadText(localExamplePath);
|
|
4772
|
+
if (templateLocalExample && currentLocalExample && currentLocalExample !== templateLocalExample) {
|
|
4773
|
+
changedFiles.push(localExamplePath);
|
|
4774
|
+
if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
|
|
4775
|
+
}
|
|
4776
|
+
normalizeExecutableTemplate(ctx, provisionScriptPath, expectedProvisionScript, changedFiles);
|
|
4777
|
+
normalizeExecutableTemplate(ctx, syncScriptPath, expectedSyncScript, changedFiles);
|
|
4778
|
+
if (!existsSync10(misePath)) {
|
|
4779
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
4780
|
+
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 };
|
|
3374
4781
|
}
|
|
3375
4782
|
}
|
|
3376
|
-
const
|
|
4783
|
+
const currentMise = readText(misePath);
|
|
4784
|
+
const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
|
|
3377
4785
|
if (nextMise !== currentMise) {
|
|
3378
4786
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3379
4787
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3380
4788
|
}
|
|
3381
|
-
const versioningPath = join12(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3382
|
-
const expectedScript = templateVersioningScript(ctx);
|
|
3383
|
-
if (expectedScript === void 0) {
|
|
3384
|
-
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: [] };
|
|
3385
|
-
}
|
|
3386
|
-
if (safeReadText(versioningPath) !== expectedScript) {
|
|
3387
|
-
changedFiles.push(versioningPath);
|
|
3388
|
-
if (!ctx.dryRun) {
|
|
3389
|
-
writeText(versioningPath, expectedScript);
|
|
3390
|
-
chmodSync2(versioningPath, 493);
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
const manifestPath = join12(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3394
|
-
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
3395
|
-
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3396
|
-
changedFiles.push(manifestPath);
|
|
3397
|
-
if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
|
|
3398
|
-
}
|
|
3399
4789
|
return {
|
|
3400
4790
|
id: finding.id,
|
|
3401
4791
|
title: finding.title,
|
|
3402
4792
|
status: changedFiles.length ? "applied" : "noop",
|
|
3403
|
-
summary: changedFiles.length ? "
|
|
4793
|
+
summary: changedFiles.length ? "Skillex skills manifest contract normalized" : "No changes required",
|
|
3404
4794
|
changedFiles,
|
|
3405
|
-
details
|
|
4795
|
+
details
|
|
3406
4796
|
};
|
|
3407
4797
|
}
|
|
3408
4798
|
},
|
|
@@ -3410,9 +4800,9 @@ var RULES = [
|
|
|
3410
4800
|
id: "sot.agent-symlinks",
|
|
3411
4801
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
3412
4802
|
audit: (ctx) => {
|
|
3413
|
-
const agentsPath =
|
|
3414
|
-
if (!
|
|
3415
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
4803
|
+
const agentsPath = join15(ctx.repoRoot, "AGENTS.md");
|
|
4804
|
+
if (!existsSync10(agentsPath)) {
|
|
4805
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join15(ctx.repoRoot, file)));
|
|
3416
4806
|
if (fallbackSources.length === 0) {
|
|
3417
4807
|
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
4808
|
}
|
|
@@ -3427,7 +4817,7 @@ var RULES = [
|
|
|
3427
4817
|
}
|
|
3428
4818
|
const details = [];
|
|
3429
4819
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3430
|
-
const full =
|
|
4820
|
+
const full = join15(ctx.repoRoot, file);
|
|
3431
4821
|
const target = readSymlinkTarget(full);
|
|
3432
4822
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
3433
4823
|
}
|
|
@@ -3451,7 +4841,7 @@ var RULES = [
|
|
|
3451
4841
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
3452
4842
|
}
|
|
3453
4843
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3454
|
-
const full =
|
|
4844
|
+
const full = join15(ctx.repoRoot, file);
|
|
3455
4845
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
3456
4846
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
3457
4847
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3473,7 +4863,7 @@ var RULES = [
|
|
|
3473
4863
|
migrate: (ctx, finding) => {
|
|
3474
4864
|
const changedFiles = [];
|
|
3475
4865
|
const details = [];
|
|
3476
|
-
const path =
|
|
4866
|
+
const path = join15(ctx.repoRoot, ".project.json");
|
|
3477
4867
|
const existing = readProjectJson(ctx) ?? {};
|
|
3478
4868
|
const canonical = canonicalProjectJson(ctx);
|
|
3479
4869
|
const merged = { ...existing, ...canonical };
|
|
@@ -3483,11 +4873,11 @@ var RULES = [
|
|
|
3483
4873
|
changedFiles.push(path);
|
|
3484
4874
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3485
4875
|
}
|
|
3486
|
-
const planeJson =
|
|
3487
|
-
if (
|
|
4876
|
+
const planeJson = join15(ctx.repoRoot, ".plane.json");
|
|
4877
|
+
if (existsSync10(planeJson)) {
|
|
3488
4878
|
const backup = `${planeJson}.migrated-backup`;
|
|
3489
|
-
if (
|
|
3490
|
-
details.push(`cannot back up .plane.json because ${
|
|
4879
|
+
if (existsSync10(backup)) {
|
|
4880
|
+
details.push(`cannot back up .plane.json because ${relative2(ctx.repoRoot, backup)} already exists`);
|
|
3491
4881
|
} else {
|
|
3492
4882
|
changedFiles.push(backup);
|
|
3493
4883
|
if (!ctx.dryRun) renameSync2(planeJson, backup);
|
|
@@ -3508,17 +4898,21 @@ var RULES = [
|
|
|
3508
4898
|
title: ".env.op + gitignore secrets contract",
|
|
3509
4899
|
audit: (ctx) => {
|
|
3510
4900
|
const details = [];
|
|
3511
|
-
const envOp = safeReadText(
|
|
3512
|
-
const gitignore = safeReadText(
|
|
4901
|
+
const envOp = safeReadText(join15(ctx.repoRoot, ".env.op"));
|
|
4902
|
+
const gitignore = safeReadText(join15(ctx.repoRoot, ".gitignore"));
|
|
3513
4903
|
if (!envOp) {
|
|
3514
4904
|
details.push(".env.op missing");
|
|
3515
4905
|
} else {
|
|
3516
|
-
const
|
|
4906
|
+
const malformed = malformedOpReferences(envOp);
|
|
4907
|
+
if (malformed.length) {
|
|
4908
|
+
details.push(`.env.op has malformed op:// reference(s) on line(s): ${Array.from(new Set(malformed.map((entry) => entry.line))).join(", ")}`);
|
|
4909
|
+
}
|
|
4910
|
+
const invalidLines = envOp.split("\n").map((line, index) => ({ line: line.trim(), number: index + 1 })).filter(({ line }) => line && !line.startsWith("#") && line.includes("=")).filter(({ line }) => {
|
|
3517
4911
|
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
3518
4912
|
const quotedLiteral = /^"[^"\r\n]*"$/.test(value) || /^'[^'\r\n]*'$/.test(value);
|
|
3519
4913
|
return !value.startsWith("op://") && !/^https?:\/\//.test(value) && !/^[A-Za-z0-9_.:-]+$/.test(value) && !quotedLiteral;
|
|
3520
4914
|
});
|
|
3521
|
-
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals: ${invalidLines.join(", ")}`);
|
|
4915
|
+
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals on line(s): ${invalidLines.map((entry) => entry.number).join(", ")}`);
|
|
3522
4916
|
}
|
|
3523
4917
|
if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
|
|
3524
4918
|
if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
|
|
@@ -3535,12 +4929,23 @@ var RULES = [
|
|
|
3535
4929
|
migrate: (ctx, finding) => {
|
|
3536
4930
|
const changedFiles = [];
|
|
3537
4931
|
const details = [];
|
|
3538
|
-
const envOpPath =
|
|
3539
|
-
if (!
|
|
4932
|
+
const envOpPath = join15(ctx.repoRoot, ".env.op");
|
|
4933
|
+
if (!existsSync10(envOpPath)) {
|
|
3540
4934
|
changedFiles.push(envOpPath);
|
|
3541
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
4935
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join15(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
4936
|
+
} else {
|
|
4937
|
+
const current = readText(envOpPath);
|
|
4938
|
+
const repaired = removeMalformedCommentOpReferences(current);
|
|
4939
|
+
if (repaired.changed) {
|
|
4940
|
+
changedFiles.push(envOpPath);
|
|
4941
|
+
if (!ctx.dryRun) writeText(envOpPath, repaired.text);
|
|
4942
|
+
}
|
|
4943
|
+
const remaining = malformedOpReferences(repaired.text).filter((entry) => !entry.commentOnly);
|
|
4944
|
+
if (remaining.length) {
|
|
4945
|
+
details.push(`Malformed active op:// reference(s) remain on line(s) ${Array.from(new Set(remaining.map((entry) => entry.line))).join(", ")}; repair them manually without replacing valid user references`);
|
|
4946
|
+
}
|
|
3542
4947
|
}
|
|
3543
|
-
const gitignorePath =
|
|
4948
|
+
const gitignorePath = join15(ctx.repoRoot, ".gitignore");
|
|
3544
4949
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3545
4950
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3546
4951
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3567,7 +4972,7 @@ var RULES = [
|
|
|
3567
4972
|
title: ".copier-answers.yml provenance + drift report",
|
|
3568
4973
|
audit: (ctx) => {
|
|
3569
4974
|
const details = [];
|
|
3570
|
-
const path =
|
|
4975
|
+
const path = join15(ctx.repoRoot, ".copier-answers.yml");
|
|
3571
4976
|
const text2 = safeReadText(path);
|
|
3572
4977
|
const project = readProjectJson(ctx);
|
|
3573
4978
|
if (!text2) {
|
|
@@ -3598,12 +5003,12 @@ var RULES = [
|
|
|
3598
5003
|
const changedFiles = [];
|
|
3599
5004
|
const project = canonicalProjectJson(ctx);
|
|
3600
5005
|
const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3601
|
-
_src_path: ${
|
|
5006
|
+
_src_path: ${join15(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3602
5007
|
project_description: ${String(project.project_description)}
|
|
3603
5008
|
project_name: ${String(project.project_name)}
|
|
3604
5009
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3605
5010
|
`;
|
|
3606
|
-
const path =
|
|
5011
|
+
const path = join15(ctx.repoRoot, ".copier-answers.yml");
|
|
3607
5012
|
if (safeReadText(path) !== text2) {
|
|
3608
5013
|
changedFiles.push(path);
|
|
3609
5014
|
if (!ctx.dryRun) writeText(path, text2);
|
|
@@ -3622,14 +5027,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3622
5027
|
id: "bmad.scaffold",
|
|
3623
5028
|
title: "BMAD modules/docs scaffold",
|
|
3624
5029
|
audit: (ctx) => {
|
|
3625
|
-
const
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
5030
|
+
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
5031
|
+
if (manifestSelection.status === "invalid") {
|
|
5032
|
+
return {
|
|
5033
|
+
id: "bmad.scaffold",
|
|
5034
|
+
title: "BMAD modules/docs scaffold",
|
|
5035
|
+
status: "fail",
|
|
5036
|
+
summary: "BMAD module manifest is invalid; refusing fallback module selection",
|
|
5037
|
+
details: [manifestSelection.error],
|
|
5038
|
+
fixable: false
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
const targetRoot = join15(ctx.repoRoot, "_bmad");
|
|
5042
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
5043
|
+
const sentinels = requiredBmadSentinels(ctx.repoRoot, selectedModules);
|
|
5044
|
+
const missing = sentinels.filter((file) => !existsSync10(join15(targetRoot, file)));
|
|
3633
5045
|
return {
|
|
3634
5046
|
id: "bmad.scaffold",
|
|
3635
5047
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3641,9 +5053,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3641
5053
|
},
|
|
3642
5054
|
migrate: (ctx, finding) => {
|
|
3643
5055
|
const changedFiles = [];
|
|
5056
|
+
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
5057
|
+
if (manifestSelection.status === "invalid") {
|
|
5058
|
+
return {
|
|
5059
|
+
id: finding.id,
|
|
5060
|
+
title: finding.title,
|
|
5061
|
+
status: "blocked",
|
|
5062
|
+
summary: "BMAD module manifest is invalid; refusing fallback module selection",
|
|
5063
|
+
changedFiles,
|
|
5064
|
+
details: [manifestSelection.error]
|
|
5065
|
+
};
|
|
5066
|
+
}
|
|
5067
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
3644
5068
|
if (ctx.dryRun) {
|
|
3645
5069
|
for (const detail of finding.details) {
|
|
3646
|
-
changedFiles.push(
|
|
5070
|
+
changedFiles.push(join15(ctx.repoRoot, detail));
|
|
3647
5071
|
}
|
|
3648
5072
|
return {
|
|
3649
5073
|
id: finding.id,
|
|
@@ -3652,11 +5076,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3652
5076
|
summary: changedFiles.length ? "Would run non-interactive bmad-method install" : "No changes required",
|
|
3653
5077
|
changedFiles,
|
|
3654
5078
|
details: [
|
|
3655
|
-
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
5079
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot, selectedModules).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
3656
5080
|
]
|
|
3657
5081
|
};
|
|
3658
5082
|
}
|
|
3659
|
-
const
|
|
5083
|
+
const preservedSkillsManifest = tryParseJson(
|
|
5084
|
+
safeReadText(join15(ctx.repoRoot, ".agents", "skills.json"))
|
|
5085
|
+
);
|
|
5086
|
+
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
3660
5087
|
if (!install.ok) {
|
|
3661
5088
|
return {
|
|
3662
5089
|
id: finding.id,
|
|
@@ -3667,16 +5094,28 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3667
5094
|
details: [install.error ?? "Unknown error"]
|
|
3668
5095
|
};
|
|
3669
5096
|
}
|
|
5097
|
+
const provisioned = provisionBmadSkills(ctx, preservedSkillsManifest);
|
|
5098
|
+
if (!provisioned.ok) {
|
|
5099
|
+
return {
|
|
5100
|
+
id: finding.id,
|
|
5101
|
+
title: finding.title,
|
|
5102
|
+
status: "blocked",
|
|
5103
|
+
summary: `BMAD installed but Skillex pack ${BMAD_PACK_VERSION} provisioning failed`,
|
|
5104
|
+
changedFiles: [],
|
|
5105
|
+
details: [provisioned.error ?? "Unknown BMAD pack error"]
|
|
5106
|
+
};
|
|
5107
|
+
}
|
|
3670
5108
|
for (const detail of finding.details) {
|
|
3671
|
-
if (
|
|
3672
|
-
changedFiles.push(
|
|
5109
|
+
if (existsSync10(join15(ctx.repoRoot, detail))) {
|
|
5110
|
+
changedFiles.push(join15(ctx.repoRoot, detail));
|
|
3673
5111
|
}
|
|
3674
5112
|
}
|
|
5113
|
+
changedFiles.push(...provisioned.changedFiles);
|
|
3675
5114
|
return {
|
|
3676
5115
|
id: finding.id,
|
|
3677
5116
|
title: finding.title,
|
|
3678
5117
|
status: changedFiles.length ? "applied" : "noop",
|
|
3679
|
-
summary: changedFiles.length ?
|
|
5118
|
+
summary: changedFiles.length ? `Installed BMAD scaffold with Skillex pack ${BMAD_PACK_VERSION} skills` : "No changes required",
|
|
3680
5119
|
changedFiles,
|
|
3681
5120
|
details: []
|
|
3682
5121
|
};
|
|
@@ -3692,7 +5131,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3692
5131
|
id: "bmad.version",
|
|
3693
5132
|
title: "BMAD version currency",
|
|
3694
5133
|
status: "skip",
|
|
3695
|
-
summary:
|
|
5134
|
+
summary: existsSync10(join15(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
3696
5135
|
details: [],
|
|
3697
5136
|
fixable: false
|
|
3698
5137
|
};
|
|
@@ -3747,7 +5186,19 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3747
5186
|
}
|
|
3748
5187
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3749
5188
|
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3750
|
-
const manifestPath =
|
|
5189
|
+
const manifestPath = join15(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
5190
|
+
const manifestSelection = manifestBmadModules(ctx.repoRoot);
|
|
5191
|
+
if (manifestSelection.status === "invalid") {
|
|
5192
|
+
return {
|
|
5193
|
+
id: finding.id,
|
|
5194
|
+
title: finding.title,
|
|
5195
|
+
status: "blocked",
|
|
5196
|
+
summary: "BMAD module manifest is invalid; refusing fallback module selection",
|
|
5197
|
+
changedFiles: [],
|
|
5198
|
+
details: [manifestSelection.error]
|
|
5199
|
+
};
|
|
5200
|
+
}
|
|
5201
|
+
const selectedModules = manifestSelection.status === "valid" ? manifestSelection.modules : configuredBmadModules(ctx.repoRoot) ?? [...DEFAULT_BMAD_MODULES];
|
|
3751
5202
|
if (ctx.dryRun) {
|
|
3752
5203
|
return {
|
|
3753
5204
|
id: finding.id,
|
|
@@ -3756,11 +5207,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3756
5207
|
summary: `Would upgrade BMAD ${installed ?? "?"} -> ${available ?? BMAD_TARGET_CHANNEL}`,
|
|
3757
5208
|
changedFiles: [manifestPath],
|
|
3758
5209
|
details: [
|
|
3759
|
-
`Would run: npx ${bmadInstallArgs(ctx.repoRoot).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
5210
|
+
`Would run: npx ${bmadInstallArgs(ctx.repoRoot, selectedModules).join(" ").replace(BMAD_INSTALL_TOOLS.join(","), "...")}`
|
|
3760
5211
|
]
|
|
3761
5212
|
};
|
|
3762
5213
|
}
|
|
3763
|
-
const
|
|
5214
|
+
const preservedSkillsManifest = tryParseJson(
|
|
5215
|
+
safeReadText(join15(ctx.repoRoot, ".agents", "skills.json"))
|
|
5216
|
+
);
|
|
5217
|
+
const install = runBmadInstall(ctx.repoRoot, selectedModules);
|
|
3764
5218
|
if (!install.ok) {
|
|
3765
5219
|
return {
|
|
3766
5220
|
id: finding.id,
|
|
@@ -3771,6 +5225,17 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3771
5225
|
details: [install.error ?? "Unknown error"]
|
|
3772
5226
|
};
|
|
3773
5227
|
}
|
|
5228
|
+
const provisioned = provisionBmadSkills(ctx, preservedSkillsManifest);
|
|
5229
|
+
if (!provisioned.ok) {
|
|
5230
|
+
return {
|
|
5231
|
+
id: finding.id,
|
|
5232
|
+
title: finding.title,
|
|
5233
|
+
status: "blocked",
|
|
5234
|
+
summary: `BMAD upgraded but Skillex pack ${BMAD_PACK_VERSION} provisioning failed`,
|
|
5235
|
+
changedFiles: [],
|
|
5236
|
+
details: [provisioned.error ?? "Unknown BMAD pack error"]
|
|
5237
|
+
};
|
|
5238
|
+
}
|
|
3774
5239
|
const nowInstalled = readInstalledBmadVersion(ctx.repoRoot);
|
|
3775
5240
|
const upgraded = Boolean(nowInstalled && installed && compareBmadVersions(nowInstalled, installed) > 0);
|
|
3776
5241
|
return {
|
|
@@ -3778,7 +5243,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3778
5243
|
title: finding.title,
|
|
3779
5244
|
status: upgraded ? "applied" : "noop",
|
|
3780
5245
|
summary: upgraded ? `Upgraded BMAD ${installed} -> ${nowInstalled}` : `BMAD reinstalled (${nowInstalled ?? "?"})`,
|
|
3781
|
-
changedFiles:
|
|
5246
|
+
changedFiles: Array.from(/* @__PURE__ */ new Set([
|
|
5247
|
+
...upgraded ? [manifestPath] : [],
|
|
5248
|
+
...provisioned.changedFiles
|
|
5249
|
+
])),
|
|
3782
5250
|
details: []
|
|
3783
5251
|
};
|
|
3784
5252
|
}
|
|
@@ -3793,12 +5261,13 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3793
5261
|
return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
|
|
3794
5262
|
}
|
|
3795
5263
|
const details = [];
|
|
3796
|
-
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"
|
|
3797
|
-
if (!
|
|
5264
|
+
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"]) {
|
|
5265
|
+
if (!existsSync10(join15(role.roleDir, rel))) details.push(`missing ${relative2(ctx.repoRoot, join15(role.roleDir, rel))}`);
|
|
3798
5266
|
}
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
5267
|
+
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
|
|
5268
|
+
details.push(".gitmodules contains retired pm runtime submodule mapping");
|
|
5269
|
+
}
|
|
5270
|
+
if (!profileMetaInheritsDefault(join15(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3802
5271
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3803
5272
|
}
|
|
3804
5273
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3819,21 +5288,32 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3819
5288
|
if (!role) {
|
|
3820
5289
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3821
5290
|
}
|
|
3822
|
-
const
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
5291
|
+
const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
5292
|
+
details.push(...retirement.details);
|
|
5293
|
+
if (!retirement.ok) {
|
|
5294
|
+
return {
|
|
5295
|
+
id: finding.id,
|
|
5296
|
+
title: finding.title,
|
|
5297
|
+
status: "blocked",
|
|
5298
|
+
summary: "Failed to retire PM runtime submodule metadata safely",
|
|
5299
|
+
changedFiles,
|
|
5300
|
+
details: [retirement.error ?? "unknown runtime retirement failure"]
|
|
5301
|
+
};
|
|
5302
|
+
}
|
|
5303
|
+
const templateRoleDir = join15(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
5304
|
+
writeIfDifferent(join15(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
5305
|
+
writeIfDifferent(join15(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
5306
|
+
writeIfDifferent(join15(role.roleDir, ".gitignore"), readText(join15(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
5307
|
+
copyMissingRecursive(join15(templateRoleDir, ".runtime-scaffold"), join15(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
5308
|
+
copyMissingRecursive(join15(templateRoleDir, ".runtime-scaffold"), join15(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
5309
|
+
copyMissingRecursive(join15(templateRoleDir, ".scripts"), join15(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
5310
|
+
const promptSource = join15(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
5311
|
+
const promptTarget = join15(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
5312
|
+
if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
|
|
3832
5313
|
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
5314
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3834
5315
|
}
|
|
3835
|
-
|
|
3836
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(join12(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
5316
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join15(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3837
5317
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3838
5318
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3839
5319
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3864,8 +5344,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3864
5344
|
}
|
|
3865
5345
|
const details = [];
|
|
3866
5346
|
for (const role of roles) {
|
|
3867
|
-
const roleRelDir =
|
|
3868
|
-
const runtimeRelPath =
|
|
5347
|
+
const roleRelDir = relative2(ctx.repoRoot, role.roleDir);
|
|
5348
|
+
const runtimeRelPath = join15(roleRelDir, "runtime");
|
|
3869
5349
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3870
5350
|
cwd: ctx.repoRoot,
|
|
3871
5351
|
encoding: "utf8"
|
|
@@ -3873,15 +5353,18 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3873
5353
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3874
5354
|
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3875
5355
|
}
|
|
3876
|
-
|
|
3877
|
-
|
|
5356
|
+
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) {
|
|
5357
|
+
details.push(`stale .gitmodules mapping exists for ${runtimeRelPath}`);
|
|
5358
|
+
}
|
|
5359
|
+
const gitignorePath = join15(role.roleDir, ".gitignore");
|
|
5360
|
+
if (existsSync10(gitignorePath)) {
|
|
3878
5361
|
const content = safeReadText(gitignorePath) ?? "";
|
|
3879
5362
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3880
5363
|
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
3881
|
-
details.push(`.gitignore missing runtime/ ignore entry in ${
|
|
5364
|
+
details.push(`.gitignore missing runtime/ ignore entry in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
3882
5365
|
}
|
|
3883
5366
|
} else {
|
|
3884
|
-
details.push(`.gitignore is missing in ${
|
|
5367
|
+
details.push(`.gitignore is missing in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
3885
5368
|
}
|
|
3886
5369
|
}
|
|
3887
5370
|
return {
|
|
@@ -3898,32 +5381,28 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3898
5381
|
const changedFiles = [];
|
|
3899
5382
|
const details = [];
|
|
3900
5383
|
for (const role of roles) {
|
|
3901
|
-
const
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
cwd: ctx.repoRoot,
|
|
3913
|
-
encoding: "utf8"
|
|
3914
|
-
});
|
|
3915
|
-
}
|
|
5384
|
+
const retirement = retireRuntimeSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
5385
|
+
details.push(...retirement.details);
|
|
5386
|
+
if (!retirement.ok) {
|
|
5387
|
+
return {
|
|
5388
|
+
id: finding.id,
|
|
5389
|
+
title: finding.title,
|
|
5390
|
+
status: "blocked",
|
|
5391
|
+
summary: "Failed to retire Hermes runtime submodule metadata safely",
|
|
5392
|
+
changedFiles,
|
|
5393
|
+
details: [retirement.error ?? "unknown runtime retirement failure"]
|
|
5394
|
+
};
|
|
3916
5395
|
}
|
|
3917
|
-
const gitignorePath =
|
|
5396
|
+
const gitignorePath = join15(role.roleDir, ".gitignore");
|
|
3918
5397
|
let content = "";
|
|
3919
5398
|
let isIgnored = false;
|
|
3920
|
-
if (
|
|
5399
|
+
if (existsSync10(gitignorePath)) {
|
|
3921
5400
|
content = safeReadText(gitignorePath) ?? "";
|
|
3922
5401
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3923
5402
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
3924
5403
|
}
|
|
3925
5404
|
if (!isIgnored) {
|
|
3926
|
-
details.push(`ignore runtime/ in ${
|
|
5405
|
+
details.push(`ignore runtime/ in ${relative2(ctx.repoRoot, gitignorePath)}`);
|
|
3927
5406
|
changedFiles.push(gitignorePath);
|
|
3928
5407
|
if (!ctx.dryRun) {
|
|
3929
5408
|
if (content && !content.endsWith("\n")) {
|
|
@@ -3958,7 +5437,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3958
5437
|
}
|
|
3959
5438
|
const details = [];
|
|
3960
5439
|
for (const role of roles) {
|
|
3961
|
-
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-
|
|
5440
|
+
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
|
|
3962
5441
|
const state = checkUnit(unit);
|
|
3963
5442
|
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
3964
5443
|
}
|
|
@@ -3984,9 +5463,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3984
5463
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3985
5464
|
}
|
|
3986
5465
|
for (const role of roles) {
|
|
3987
|
-
const sysDir =
|
|
3988
|
-
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-
|
|
3989
|
-
const allUnitsPresent = units.every((unit) =>
|
|
5466
|
+
const sysDir = join15(ctx.homeDir, ".config", "systemd", "user");
|
|
5467
|
+
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
5468
|
+
const allUnitsPresent = units.every((unit) => existsSync10(join15(sysDir, unit)));
|
|
3990
5469
|
if (allUnitsPresent) {
|
|
3991
5470
|
if (ctx.dryRun) {
|
|
3992
5471
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3998,8 +5477,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3998
5477
|
}
|
|
3999
5478
|
continue;
|
|
4000
5479
|
}
|
|
4001
|
-
for (const script of [
|
|
4002
|
-
if (!script || !
|
|
5480
|
+
for (const script of [join15(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
5481
|
+
if (!script || !existsSync10(script)) continue;
|
|
4003
5482
|
if (ctx.dryRun) {
|
|
4004
5483
|
details.push(`would run: bash ${script}`);
|
|
4005
5484
|
} else {
|
|
@@ -4017,6 +5496,349 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
4017
5496
|
details
|
|
4018
5497
|
};
|
|
4019
5498
|
}
|
|
5499
|
+
},
|
|
5500
|
+
{
|
|
5501
|
+
id: "hermes.runtime-singleton",
|
|
5502
|
+
title: "Hermes singleton runtime (shared config/auth, per-agent memory)",
|
|
5503
|
+
audit: (ctx) => {
|
|
5504
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5505
|
+
if (!roles.length) {
|
|
5506
|
+
return { id: "hermes.runtime-singleton", title: "Hermes singleton runtime (shared config/auth, per-agent memory)", status: "skip", summary: "No Hermes roles present", details: [], fixable: false };
|
|
5507
|
+
}
|
|
5508
|
+
const details = [];
|
|
5509
|
+
for (const role of roles) {
|
|
5510
|
+
const plan = singletonPlan(ctx, role);
|
|
5511
|
+
if (!existsSync10(plan.fleetRoot)) {
|
|
5512
|
+
details.push(`fleet root missing at ${plan.fleetRoot}`);
|
|
5513
|
+
continue;
|
|
5514
|
+
}
|
|
5515
|
+
if (!existsSync10(plan.profileDir)) {
|
|
5516
|
+
details.push(`profile dir missing: ${plan.profileDir}`);
|
|
5517
|
+
} else if (lstatSync2(plan.profileDir).isSymbolicLink()) {
|
|
5518
|
+
details.push(`profile dir is a symlink (must be a real dir): ${plan.profileDir}`);
|
|
5519
|
+
}
|
|
5520
|
+
for (const link of plan.links) {
|
|
5521
|
+
const state = linkState(link.path, link.target);
|
|
5522
|
+
if (state !== "ok") details.push(`${state}: ${link.path} -> ${link.target}`);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
return {
|
|
5526
|
+
id: "hermes.runtime-singleton",
|
|
5527
|
+
title: "Hermes singleton runtime (shared config/auth, per-agent memory)",
|
|
5528
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
5529
|
+
summary: details.length === 0 ? "Singleton runtime contract satisfied" : `${details.length} singleton-runtime issue(s) detected`,
|
|
5530
|
+
details,
|
|
5531
|
+
fixable: true
|
|
5532
|
+
};
|
|
5533
|
+
},
|
|
5534
|
+
migrate: (ctx, finding) => {
|
|
5535
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5536
|
+
const changedFiles = [];
|
|
5537
|
+
const details = [];
|
|
5538
|
+
for (const role of roles) {
|
|
5539
|
+
const plan = singletonPlan(ctx, role);
|
|
5540
|
+
if (!existsSync10(plan.fleetRoot)) {
|
|
5541
|
+
details.push(`blocked: fleet root missing at ${plan.fleetRoot}`);
|
|
5542
|
+
continue;
|
|
5543
|
+
}
|
|
5544
|
+
for (const shared of plan.sharedSeeds) {
|
|
5545
|
+
if (existsSync10(shared.rootPath)) continue;
|
|
5546
|
+
const donor = existsSync10(shared.runtimePath) ? shared.runtimePath : null;
|
|
5547
|
+
if (!donor) continue;
|
|
5548
|
+
details.push(`seed fleet ${basename4(shared.rootPath)} from ${donor}`);
|
|
5549
|
+
changedFiles.push(shared.rootPath);
|
|
5550
|
+
if (!ctx.dryRun) copyFileSync(donor, shared.rootPath);
|
|
5551
|
+
}
|
|
5552
|
+
if (existsSync10(plan.profileDir) && lstatSync2(plan.profileDir).isSymbolicLink()) {
|
|
5553
|
+
details.push(`convert profile symlink to real dir: ${plan.profileDir}`);
|
|
5554
|
+
changedFiles.push(plan.profileDir);
|
|
5555
|
+
if (!ctx.dryRun) unlinkSync3(plan.profileDir);
|
|
5556
|
+
}
|
|
5557
|
+
if (!existsSync10(plan.profileDir)) {
|
|
5558
|
+
details.push(`create profile dir: ${plan.profileDir}`);
|
|
5559
|
+
changedFiles.push(plan.profileDir);
|
|
5560
|
+
if (!ctx.dryRun) mkdirSync6(plan.profileDir, { recursive: true });
|
|
5561
|
+
}
|
|
5562
|
+
for (const link of plan.links) {
|
|
5563
|
+
const state = linkState(link.path, link.target);
|
|
5564
|
+
if (state === "ok") continue;
|
|
5565
|
+
if (link.ensureTargetDir && !existsSync10(link.target) && !ctx.dryRun) {
|
|
5566
|
+
mkdirSync6(link.target, { recursive: true });
|
|
5567
|
+
}
|
|
5568
|
+
details.push(`link ${link.path} -> ${link.target}`);
|
|
5569
|
+
changedFiles.push(link.path);
|
|
5570
|
+
if (ctx.dryRun) continue;
|
|
5571
|
+
if (existsSync10(link.path) || isDanglingLink(link.path)) {
|
|
5572
|
+
const lst = lstatSync2(link.path);
|
|
5573
|
+
if (lst.isSymbolicLink()) {
|
|
5574
|
+
unlinkSync3(link.path);
|
|
5575
|
+
} else {
|
|
5576
|
+
const parked = `${link.path}.pre-singleton`;
|
|
5577
|
+
renameSync2(link.path, parked);
|
|
5578
|
+
details.push(`parked pre-existing ${link.path} at ${parked}`);
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
ensureParent(link.path);
|
|
5582
|
+
symlinkSync(link.target, link.path);
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
return {
|
|
5586
|
+
id: finding.id,
|
|
5587
|
+
title: finding.title,
|
|
5588
|
+
status: details.some((d) => d.startsWith("blocked:")) ? "blocked" : changedFiles.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
5589
|
+
summary: changedFiles.length ? ctx.dryRun ? "Planned singleton-runtime wiring" : "Singleton runtime wired" : "No changes required",
|
|
5590
|
+
changedFiles,
|
|
5591
|
+
details
|
|
5592
|
+
};
|
|
5593
|
+
}
|
|
5594
|
+
},
|
|
5595
|
+
{
|
|
5596
|
+
id: "hermes.profile-wiring",
|
|
5597
|
+
title: "Launcher + systemd HERMES_HOME points at the named profile",
|
|
5598
|
+
audit: (ctx) => {
|
|
5599
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5600
|
+
if (!roles.length) {
|
|
5601
|
+
return { id: "hermes.profile-wiring", title: "Launcher + systemd HERMES_HOME points at the named profile", status: "skip", summary: "No Hermes roles present", details: [], fixable: false };
|
|
5602
|
+
}
|
|
5603
|
+
const details = [];
|
|
5604
|
+
for (const role of roles) {
|
|
5605
|
+
const plan = singletonPlan(ctx, role);
|
|
5606
|
+
const launcher = join15(role.roleDir, "hermes");
|
|
5607
|
+
const text2 = safeReadText(launcher);
|
|
5608
|
+
if (text2 === null) {
|
|
5609
|
+
details.push(`launcher missing: ${relative2(ctx.repoRoot, launcher)}`);
|
|
5610
|
+
} else {
|
|
5611
|
+
if (/^HERMES_HOME="\$RUNTIME_HOME"\s*$/m.test(text2)) {
|
|
5612
|
+
details.push(`launcher sets HERMES_HOME to the raw runtime path (disables shared auth + profile identity): ${relative2(ctx.repoRoot, launcher)}`);
|
|
5613
|
+
}
|
|
5614
|
+
if (/HERMES_OAUTH_FILE/.test(text2)) {
|
|
5615
|
+
details.push(`launcher exports HERMES_OAUTH_FILE, which Hermes does not implement (dead config): ${relative2(ctx.repoRoot, launcher)}`);
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
for (const unit of profileUnits(role)) {
|
|
5619
|
+
const unitPath = join15(ctx.homeDir, ".config", "systemd", "user", unit);
|
|
5620
|
+
const unitText = safeReadText(unitPath);
|
|
5621
|
+
if (unitText === null) continue;
|
|
5622
|
+
const current = /^Environment=HERMES_HOME=(.*)$/m.exec(unitText)?.[1]?.trim();
|
|
5623
|
+
if (current && current !== plan.profileDir) {
|
|
5624
|
+
details.push(`${unit} HERMES_HOME=${current} (expected ${plan.profileDir})`);
|
|
5625
|
+
}
|
|
5626
|
+
if (/^Environment=HERMES_OAUTH_FILE=/m.test(unitText)) {
|
|
5627
|
+
details.push(`${unit} sets HERMES_OAUTH_FILE (dead config)`);
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
return {
|
|
5632
|
+
id: "hermes.profile-wiring",
|
|
5633
|
+
title: "Launcher + systemd HERMES_HOME points at the named profile",
|
|
5634
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
5635
|
+
summary: details.length === 0 ? "HERMES_HOME wiring is in parity" : `${details.length} HERMES_HOME wiring issue(s) detected`,
|
|
5636
|
+
details,
|
|
5637
|
+
fixable: true
|
|
5638
|
+
};
|
|
5639
|
+
},
|
|
5640
|
+
migrate: (ctx, finding) => {
|
|
5641
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5642
|
+
const changedFiles = [];
|
|
5643
|
+
const details = [];
|
|
5644
|
+
let unitsTouched = false;
|
|
5645
|
+
for (const role of roles) {
|
|
5646
|
+
const plan = singletonPlan(ctx, role);
|
|
5647
|
+
const launcher = join15(role.roleDir, "hermes");
|
|
5648
|
+
const text2 = safeReadText(launcher);
|
|
5649
|
+
if (text2 !== null) {
|
|
5650
|
+
const rewritten = rewriteLauncher(text2);
|
|
5651
|
+
if (rewritten !== text2) {
|
|
5652
|
+
details.push(`rewrite launcher HERMES_HOME -> profile path: ${relative2(ctx.repoRoot, launcher)}`);
|
|
5653
|
+
writeIfDifferent(launcher, rewritten, ctx.dryRun, changedFiles, 493);
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
for (const unit of profileUnits(role)) {
|
|
5657
|
+
const unitPath = join15(ctx.homeDir, ".config", "systemd", "user", unit);
|
|
5658
|
+
const unitText = safeReadText(unitPath);
|
|
5659
|
+
if (unitText === null) continue;
|
|
5660
|
+
let next = unitText.replace(/^Environment=HERMES_HOME=.*$/m, `Environment=HERMES_HOME=${plan.profileDir}`);
|
|
5661
|
+
next = next.replace(/^Environment=HERMES_OAUTH_FILE=.*\n/m, "");
|
|
5662
|
+
if (next !== unitText) {
|
|
5663
|
+
details.push(`repoint ${unit} HERMES_HOME -> ${plan.profileDir}`);
|
|
5664
|
+
writeIfDifferent(unitPath, next, ctx.dryRun, changedFiles);
|
|
5665
|
+
unitsTouched = true;
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
if (unitsTouched && !ctx.dryRun) {
|
|
5670
|
+
systemctlUser(["daemon-reload"]);
|
|
5671
|
+
details.push("systemctl --user daemon-reload (restart units to pick up the new HERMES_HOME)");
|
|
5672
|
+
}
|
|
5673
|
+
return {
|
|
5674
|
+
id: finding.id,
|
|
5675
|
+
title: finding.title,
|
|
5676
|
+
status: changedFiles.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
5677
|
+
summary: changedFiles.length ? ctx.dryRun ? "Planned HERMES_HOME rewiring" : "HERMES_HOME rewired to named profiles" : "No changes required",
|
|
5678
|
+
changedFiles,
|
|
5679
|
+
details
|
|
5680
|
+
};
|
|
5681
|
+
}
|
|
5682
|
+
},
|
|
5683
|
+
{
|
|
5684
|
+
id: "hermes.registry-parity",
|
|
5685
|
+
title: "Fleet registry matches .project.json (no duplicate or stale agents)",
|
|
5686
|
+
audit: (ctx) => {
|
|
5687
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5688
|
+
const details = [];
|
|
5689
|
+
const registryPath2 = join15(ctx.homeDir, ".hermes", "agents-registry.yaml");
|
|
5690
|
+
const registry = readRegistry(registryPath2);
|
|
5691
|
+
if (!registry) {
|
|
5692
|
+
if (!roles.length && declaredAgentIds(ctx.repoRoot).length === 0) {
|
|
5693
|
+
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "skip", summary: "No Hermes roles or declared agents present", details: [], fixable: false };
|
|
5694
|
+
}
|
|
5695
|
+
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "warn", summary: `registry unreadable at ${registryPath2}`, details: [], fixable: false };
|
|
5696
|
+
}
|
|
5697
|
+
const canonical = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
5698
|
+
const owned = ownedRegistryEntries(registry, ctx.repoRoot);
|
|
5699
|
+
const unprovisioned = unprovisionedRoleAgents(registry, ctx.repoRoot, canonical);
|
|
5700
|
+
if (unprovisioned.length) {
|
|
5701
|
+
return {
|
|
5702
|
+
id: "hermes.registry-parity",
|
|
5703
|
+
title: "Fleet registry matches .project.json (no duplicate or stale agents)",
|
|
5704
|
+
status: "fail",
|
|
5705
|
+
summary: `${unprovisioned.length} unprovisioned Hermes role blocker(s) detected`,
|
|
5706
|
+
details: unprovisioned.map(
|
|
5707
|
+
({ agentId, roleDir, sources }) => `agent "${agentId}" (${sources.join(" + ")}) has no role.yaml${roleDir ? ` at ${roleDir}` : ""}; provision or restore the role, do not delete its registry/declaration`
|
|
5708
|
+
),
|
|
5709
|
+
fixable: false
|
|
5710
|
+
};
|
|
5711
|
+
}
|
|
5712
|
+
if (canonical.size === 0) {
|
|
5713
|
+
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "skip", summary: "No Hermes roles, declarations, or registry entries present", details: [], fixable: false };
|
|
5714
|
+
}
|
|
5715
|
+
for (const [agentId, entry] of owned) {
|
|
5716
|
+
const roleDir = String(entry?.role_dir ?? "");
|
|
5717
|
+
if (!canonical.has(agentId)) {
|
|
5718
|
+
details.push(`stale/duplicate registry agent "${agentId}" for ${roleDir} (role.yaml declares ${[...canonical].join(", ")})`);
|
|
5719
|
+
}
|
|
5720
|
+
}
|
|
5721
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical.has(id))) {
|
|
5722
|
+
details.push(`.project.json declares agent "${extra}" that no role.yaml claims`);
|
|
5723
|
+
}
|
|
5724
|
+
for (const role of roles) {
|
|
5725
|
+
const entry = registry[role.agentId];
|
|
5726
|
+
if (!entry) {
|
|
5727
|
+
details.push(`registry is missing an entry for ${role.agentId}`);
|
|
5728
|
+
continue;
|
|
5729
|
+
}
|
|
5730
|
+
const entryRoleDir = String(entry.role_dir ?? "");
|
|
5731
|
+
if (entryRoleDir && realOrSelf(entryRoleDir) !== realOrSelf(role.roleDir)) {
|
|
5732
|
+
details.push(`registry role_dir for ${role.agentId} is ${entryRoleDir} (expected ${role.roleDir})`);
|
|
5733
|
+
}
|
|
5734
|
+
const bin = String(entry.hermes?.bin ?? "");
|
|
5735
|
+
if (bin && !existsSync10(bin)) {
|
|
5736
|
+
details.push(`registry hermes.bin for ${role.agentId} does not exist: ${bin}`);
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
return {
|
|
5740
|
+
id: "hermes.registry-parity",
|
|
5741
|
+
title: "Fleet registry matches .project.json (no duplicate or stale agents)",
|
|
5742
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
5743
|
+
summary: details.length === 0 ? "Fleet registry is in parity" : `${details.length} registry parity issue(s) detected`,
|
|
5744
|
+
details,
|
|
5745
|
+
fixable: true
|
|
5746
|
+
};
|
|
5747
|
+
},
|
|
5748
|
+
migrate: (ctx, finding) => {
|
|
5749
|
+
const changedFiles = [];
|
|
5750
|
+
const details = [];
|
|
5751
|
+
const registryPath2 = join15(ctx.homeDir, ".hermes", "agents-registry.yaml");
|
|
5752
|
+
const raw = safeReadText(registryPath2);
|
|
5753
|
+
if (raw === null) {
|
|
5754
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: `registry unreadable at ${registryPath2}`, changedFiles, details };
|
|
5755
|
+
}
|
|
5756
|
+
let doc;
|
|
5757
|
+
try {
|
|
5758
|
+
doc = YAML3.parse(raw);
|
|
5759
|
+
} catch {
|
|
5760
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "registry is not valid YAML", changedFiles, details };
|
|
5761
|
+
}
|
|
5762
|
+
const agents = doc?.agents ?? {};
|
|
5763
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
5764
|
+
const canonical = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
5765
|
+
const unprovisioned = unprovisionedRoleAgents(agents, ctx.repoRoot, canonical);
|
|
5766
|
+
if (unprovisioned.length) {
|
|
5767
|
+
return {
|
|
5768
|
+
id: finding.id,
|
|
5769
|
+
title: finding.title,
|
|
5770
|
+
status: "blocked",
|
|
5771
|
+
summary: "Registry parity is blocked by an unprovisioned Hermes role",
|
|
5772
|
+
changedFiles,
|
|
5773
|
+
details: unprovisioned.map(
|
|
5774
|
+
({ agentId, roleDir, sources }) => `blocked: "${agentId}" (${sources.join(" + ")}) has no role.yaml${roleDir ? ` at ${roleDir}` : ""}; provision or restore the role without pruning registry/declaration state`
|
|
5775
|
+
)
|
|
5776
|
+
};
|
|
5777
|
+
}
|
|
5778
|
+
const fleetBin = fleetBinPath(ctx);
|
|
5779
|
+
let dirty = false;
|
|
5780
|
+
if (canonical.size === 0) {
|
|
5781
|
+
for (const [agentId] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
5782
|
+
details.push(`blocked: "${agentId}" has no role.yaml; provision the role instead of pruning the registry`);
|
|
5783
|
+
}
|
|
5784
|
+
for (const agentId of declaredAgentIds(ctx.repoRoot)) {
|
|
5785
|
+
if (!details.some((detail) => detail.includes(`"${agentId}"`))) {
|
|
5786
|
+
details.push(`blocked: "${agentId}" is declared but has no role.yaml; provision or restore the role`);
|
|
5787
|
+
}
|
|
5788
|
+
}
|
|
5789
|
+
if (details.length) {
|
|
5790
|
+
return {
|
|
5791
|
+
id: finding.id,
|
|
5792
|
+
title: finding.title,
|
|
5793
|
+
status: "blocked",
|
|
5794
|
+
summary: "Registry parity is blocked by an unprovisioned Hermes role",
|
|
5795
|
+
changedFiles,
|
|
5796
|
+
details
|
|
5797
|
+
};
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
for (const [agentId, entry] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
5801
|
+
if (canonical.size > 0 && !canonical.has(agentId)) {
|
|
5802
|
+
details.push(`drop stale/duplicate registry agent "${agentId}"`);
|
|
5803
|
+
delete agents[agentId];
|
|
5804
|
+
dropDeclaredAgent(ctx, agentId, changedFiles, details);
|
|
5805
|
+
dirty = true;
|
|
5806
|
+
continue;
|
|
5807
|
+
}
|
|
5808
|
+
const hermes = entry.hermes ?? {};
|
|
5809
|
+
if (fleetBin && String(hermes.bin ?? "") !== fleetBin && !existsSync10(String(hermes.bin ?? ""))) {
|
|
5810
|
+
details.push(`repoint ${agentId} hermes.bin -> ${fleetBin}`);
|
|
5811
|
+
hermes.bin = fleetBin;
|
|
5812
|
+
entry.hermes = hermes;
|
|
5813
|
+
dirty = true;
|
|
5814
|
+
}
|
|
5815
|
+
if (hermes.oauth_file) {
|
|
5816
|
+
details.push(`drop dead hermes.oauth_file from ${agentId}`);
|
|
5817
|
+
delete hermes.oauth_file;
|
|
5818
|
+
dirty = true;
|
|
5819
|
+
}
|
|
5820
|
+
}
|
|
5821
|
+
if (canonical.size > 0) {
|
|
5822
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical.has(id))) {
|
|
5823
|
+
dropDeclaredAgent(ctx, extra, changedFiles, details);
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
if (dirty) {
|
|
5827
|
+
changedFiles.push(registryPath2);
|
|
5828
|
+
if (!ctx.dryRun) {
|
|
5829
|
+
doc.agents = agents;
|
|
5830
|
+
writeText(registryPath2, YAML3.stringify(doc));
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
return {
|
|
5834
|
+
id: finding.id,
|
|
5835
|
+
title: finding.title,
|
|
5836
|
+
status: changedFiles.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
5837
|
+
summary: changedFiles.length ? ctx.dryRun ? "Planned registry repair" : "Fleet registry repaired" : "No changes required",
|
|
5838
|
+
changedFiles,
|
|
5839
|
+
details
|
|
5840
|
+
};
|
|
5841
|
+
}
|
|
4020
5842
|
}
|
|
4021
5843
|
];
|
|
4022
5844
|
function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
@@ -4026,7 +5848,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
4026
5848
|
changedFiles.push(path);
|
|
4027
5849
|
if (!dryRun) {
|
|
4028
5850
|
writeText(path, normalized);
|
|
4029
|
-
if (mode)
|
|
5851
|
+
if (mode) chmodSync4(path, mode);
|
|
4030
5852
|
}
|
|
4031
5853
|
}
|
|
4032
5854
|
function getParityRuleIds() {
|
|
@@ -4035,7 +5857,7 @@ function getParityRuleIds() {
|
|
|
4035
5857
|
function runAudit(repoArg) {
|
|
4036
5858
|
const pjanglerRoot = resolvePjanglerRoot2();
|
|
4037
5859
|
const ctx = {
|
|
4038
|
-
repoRoot:
|
|
5860
|
+
repoRoot: resolve3(repoArg ?? process.cwd()),
|
|
4039
5861
|
dryRun: true,
|
|
4040
5862
|
pjanglerRoot,
|
|
4041
5863
|
homeDir: homedir5()
|
|
@@ -4051,7 +5873,7 @@ function runAudit(repoArg) {
|
|
|
4051
5873
|
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
4052
5874
|
const pjanglerRoot = resolvePjanglerRoot2();
|
|
4053
5875
|
const ctx = {
|
|
4054
|
-
repoRoot:
|
|
5876
|
+
repoRoot: resolve3(repoArg ?? process.cwd()),
|
|
4055
5877
|
dryRun,
|
|
4056
5878
|
pjanglerRoot,
|
|
4057
5879
|
homeDir: homedir5()
|
|
@@ -4085,7 +5907,22 @@ function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
|
4085
5907
|
};
|
|
4086
5908
|
}
|
|
4087
5909
|
function runMigration(selector, repoArg, dryRun, all) {
|
|
4088
|
-
|
|
5910
|
+
if (all) {
|
|
5911
|
+
const audit = runAudit(repoArg);
|
|
5912
|
+
const ruleIds2 = audit.rules.filter((finding) => finding.fixable && (finding.status === "fail" || finding.status === "warn")).map((finding) => finding.id);
|
|
5913
|
+
if (ruleIds2.length === 0) {
|
|
5914
|
+
return {
|
|
5915
|
+
repo: audit.repo,
|
|
5916
|
+
dryRun,
|
|
5917
|
+
ok: true,
|
|
5918
|
+
selectedRules: [],
|
|
5919
|
+
results: [],
|
|
5920
|
+
changedFiles: []
|
|
5921
|
+
};
|
|
5922
|
+
}
|
|
5923
|
+
return runMigrationForRules(ruleIds2, repoArg, dryRun);
|
|
5924
|
+
}
|
|
5925
|
+
const ruleIds = selector ? [selector] : [];
|
|
4089
5926
|
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
4090
5927
|
}
|
|
4091
5928
|
function prettyTimestamp(iso) {
|
|
@@ -4122,8 +5959,8 @@ var server = new McpServer({
|
|
|
4122
5959
|
});
|
|
4123
5960
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
4124
5961
|
function resolveTargetDir(targetDir) {
|
|
4125
|
-
const dir =
|
|
4126
|
-
if (!
|
|
5962
|
+
const dir = resolve4(targetDir ?? process.cwd());
|
|
5963
|
+
if (!existsSync11(dir)) {
|
|
4127
5964
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
4128
5965
|
}
|
|
4129
5966
|
if (!statSync2(dir).isDirectory()) {
|
|
@@ -4134,7 +5971,7 @@ function resolveTargetDir(targetDir) {
|
|
|
4134
5971
|
function resolvePjanglerRoot3() {
|
|
4135
5972
|
let dir = dirname8(fileURLToPath5(import.meta.url));
|
|
4136
5973
|
while (dir !== dirname8(dir)) {
|
|
4137
|
-
if (
|
|
5974
|
+
if (existsSync11(join16(dir, "package.json")) && existsSync11(join16(dir, "templates", "commonproject", "copier.yml"))) {
|
|
4138
5975
|
return dir;
|
|
4139
5976
|
}
|
|
4140
5977
|
dir = dirname8(dir);
|
|
@@ -4335,9 +6172,9 @@ server.registerTool(
|
|
|
4335
6172
|
try {
|
|
4336
6173
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
4337
6174
|
const projectSlug = input.projectSlug ?? slugify(input.projectName);
|
|
4338
|
-
const parentDir =
|
|
4339
|
-
if (!
|
|
4340
|
-
const targetDir =
|
|
6175
|
+
const parentDir = resolve4(input.parentDir ?? process.cwd());
|
|
6176
|
+
if (!existsSync11(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
6177
|
+
const targetDir = resolve4(input.targetDir ?? join16(parentDir, projectSlug));
|
|
4341
6178
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
4342
6179
|
const dryRun = input.dryRun ?? true;
|
|
4343
6180
|
const local = input.local ?? true;
|
|
@@ -4347,7 +6184,7 @@ server.registerTool(
|
|
|
4347
6184
|
if (!skipPlane && ticketProvider === "plane" && !boardId) {
|
|
4348
6185
|
throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
|
|
4349
6186
|
}
|
|
4350
|
-
if (!dryRun &&
|
|
6187
|
+
if (!dryRun && existsSync11(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
4351
6188
|
const plan = planProjectInit({
|
|
4352
6189
|
name: input.projectName,
|
|
4353
6190
|
description: input.projectDescription,
|
|
@@ -4580,7 +6417,7 @@ server.registerTool(
|
|
|
4580
6417
|
targetDir: resolvedTarget,
|
|
4581
6418
|
yes: true,
|
|
4582
6419
|
local,
|
|
4583
|
-
targetRepo: input.targetRepo ??
|
|
6420
|
+
targetRepo: input.targetRepo ?? basename5(resolvedTarget),
|
|
4584
6421
|
role: input.role,
|
|
4585
6422
|
agentPurpose: input.agentPurpose,
|
|
4586
6423
|
soulTone: input.soulTone,
|