@delorenj/pjangler 1.2.18 → 1.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +884 -295
- package/dist/mcp-server.js +892 -303
- package/package.json +8 -2
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +11 -12
- package/templates/commonproject/copier.yml +11 -31
- 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/.project.json.jinja +9 -13
- package/templates/commonproject/template/mise.toml.jinja +9 -16
- package/templates/hermes-agent/README.md +9 -9
- package/templates/hermes-agent/config.example.toml +1 -66
- package/templates/hermes-agent/copier.yml +4 -3
- package/templates/hermes-agent/docs/architecture.md +9 -12
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/operations.md +6 -5
- package/templates/hermes-agent/docs/sentinel/README.md +9 -7
- package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
- package/templates/hermes-agent/docs/sentinel/development.md +12 -13
- package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
- package/templates/hermes-agent/install-local.sh +15 -14
- package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
- package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
- package/templates/hermes-agent/template/.gitignore.jinja +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
- package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
- package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +33 -66
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -33
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +22 -27
- package/templates/hermes-agent/template/SOUL.md.jinja +49 -30
- package/templates/hermes-agent/template/role.yaml.jinja +35 -4
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -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/docs/bloodbank-gateway.md +0 -57
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +0 -60
package/dist/mcp-server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/mcp-server.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { basename as basename4, dirname as dirname8, join as
|
|
4
|
+
import { existsSync as existsSync11, statSync as statSync2 } from "node:fs";
|
|
5
|
+
import { basename as basename4, dirname as dirname8, join as join15, resolve as resolve3 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -285,97 +285,284 @@ var AddMiseCodegraphScript = class extends Command {
|
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
287
|
const content = `#!/usr/bin/env bash
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
# Auto-generated by pjangler
|
|
289
|
+
|
|
290
|
+
REPO_ROOT="$(pwd)"
|
|
291
|
+
PROJECT_NAME="$(basename "$REPO_ROOT")"
|
|
292
|
+
CONTAINER_NAME="codegraph-mcp-$PROJECT_NAME"
|
|
293
|
+
CACHE_DIR="$REPO_ROOT/.codegraph"
|
|
294
|
+
|
|
295
|
+
# Deterministically generate a port based on the repository path
|
|
296
|
+
PORT=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}' | tr -d 'a-f' | cut -c1-4)
|
|
297
|
+
# Ensure port is > 1024
|
|
298
|
+
PORT=$(( (PORT % 60000) + 1025 ))
|
|
299
|
+
|
|
300
|
+
if ! docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then
|
|
301
|
+
echo "\u{1F680} Starting CodeGraph MCP Server on port $PORT..."
|
|
302
|
+
|
|
303
|
+
# Ensure the container isn't lingering in a stopped state
|
|
304
|
+
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
305
|
+
|
|
306
|
+
# Run the true Colby McHenry CodeGraph Docker image
|
|
307
|
+
# which we built locally as colbymchenry-codegraph-mcp:latest
|
|
308
|
+
docker run -d \\
|
|
309
|
+
--name "$CONTAINER_NAME" \\
|
|
310
|
+
--restart unless-stopped \\
|
|
311
|
+
-p "$PORT:8045" \\
|
|
312
|
+
-v "$REPO_ROOT:/repo" \\
|
|
313
|
+
colbymchenry-codegraph-mcp:latest >/dev/null
|
|
314
|
+
|
|
315
|
+
echo "\u2705 CodeGraph MCP running in background. SSE URL: http://localhost:$PORT/sse"
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
# Run init inside the container to ensure the index is bootstrapped.
|
|
319
|
+
# We run this using the standard codegraph CLI inside the container
|
|
320
|
+
docker exec "$CONTAINER_NAME" codegraph init -i /repo >/dev/null 2>&1 || true
|
|
321
|
+
|
|
322
|
+
# Wire up the MCP server to local agents
|
|
323
|
+
WIRE_SCRIPT="$(dirname "$0")/codegraph-wire.sh"
|
|
324
|
+
if [ -x "$WIRE_SCRIPT" ]; then
|
|
325
|
+
"$WIRE_SCRIPT"
|
|
326
|
+
fi
|
|
327
|
+
`;
|
|
328
|
+
this.writeFile(filePath, content);
|
|
329
|
+
if (!this.context.dryRun) {
|
|
330
|
+
chmodSync(join2(this.context.targetDir, filePath), 493);
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
335
|
+
filePath
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/commands/AddMiseCodegraphWireScript.ts
|
|
341
|
+
import { chmodSync as chmodSync2 } from "fs";
|
|
342
|
+
import { join as join3 } from "path";
|
|
343
|
+
var AddMiseCodegraphWireScript = class extends Command {
|
|
344
|
+
async invoke() {
|
|
345
|
+
const filePath = ".mise/scripts/codegraph-wire.sh";
|
|
346
|
+
if (this.fileExists(filePath) && !this.context.force) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
message: this.formatMessage("\u26A0\uFE0F .mise/scripts/codegraph-wire.sh already exists"),
|
|
350
|
+
filePath
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const content = `#!/usr/bin/env bash
|
|
354
|
+
# Mise enter hook: Auto-wire CodeGraph MCP Server to local agents.
|
|
355
|
+
# Detects Claude Code, Codex, Gemini, Kimi, and OpenCode and configures
|
|
356
|
+
# them to use the local SSE endpoint for the current project.
|
|
294
357
|
|
|
295
358
|
set -euo pipefail
|
|
296
359
|
|
|
297
360
|
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
298
|
-
PROJECT_BIN_DIR="$REPO_ROOT/.mise/bin"
|
|
299
|
-
mkdir -p "$PROJECT_BIN_DIR"
|
|
300
361
|
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
|
|
362
|
+
# Compute the same port as the container script
|
|
363
|
+
PORT_HASH=$(echo -n "$REPO_ROOT" | md5sum | awk '{print $1}')
|
|
364
|
+
PORT_DEC=$(printf "%d" "0x\${PORT_HASH:0:4}")
|
|
365
|
+
PORT=$(( 8045 + (PORT_DEC % 955) ))
|
|
306
366
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
367
|
+
SSE_URL="http://localhost:$PORT/sse"
|
|
368
|
+
|
|
369
|
+
inject_sse() {
|
|
370
|
+
local target="$1"
|
|
371
|
+
local agent="$2"
|
|
372
|
+
|
|
373
|
+
if [ ! -f "$target" ]; then
|
|
374
|
+
mkdir -p "$(dirname "$target")"
|
|
375
|
+
echo '{"mcpServers": {}}' > "$target"
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
# Ensure the file is valid JSON (fail gracefully if it's garbled)
|
|
379
|
+
if ! jq . "$target" >/dev/null 2>&1; then
|
|
380
|
+
echo "[mise] WARNING: $target is not valid JSON, skipping $agent wiring" >&2
|
|
381
|
+
return
|
|
312
382
|
fi
|
|
383
|
+
|
|
384
|
+
# Inject or update the codegraph server
|
|
385
|
+
jq --arg url "$SSE_URL" '.mcpServers.codegraph = {"type": "sse", "url": $url}' "$target" > "$target.tmp" && mv "$target.tmp" "$target"
|
|
386
|
+
echo "[mise] Wired CodeGraph SSE for $agent -> $target"
|
|
313
387
|
}
|
|
314
388
|
|
|
315
|
-
#
|
|
316
|
-
|
|
317
|
-
if command -v codegraph >/dev/null 2>&1; then
|
|
318
|
-
return 0
|
|
319
|
-
fi
|
|
389
|
+
# 1. Claude Code (Project-scoped)
|
|
390
|
+
inject_sse "$REPO_ROOT/.claude.json" "Claude Code"
|
|
320
391
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
export PATH="$PROJECT_BIN_DIR:$PATH"
|
|
324
|
-
return 0
|
|
325
|
-
fi
|
|
392
|
+
# 2. Codex (Global)
|
|
393
|
+
inject_sse "$HOME/.codex/mcp.json" "Codex"
|
|
326
394
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if [ -x "$d/codegraph" ]; then
|
|
330
|
-
export PATH="$d:$PATH"
|
|
331
|
-
return 0
|
|
332
|
-
fi
|
|
333
|
-
done
|
|
395
|
+
# 3. Gemini (Global)
|
|
396
|
+
inject_sse "$HOME/.gemini/config/mcp.json" "Gemini"
|
|
334
397
|
|
|
335
|
-
|
|
336
|
-
|
|
398
|
+
# 4. Kimi (Global)
|
|
399
|
+
inject_sse "$HOME/.kimi-code/mcp.json" "Kimi"
|
|
337
400
|
|
|
338
|
-
#
|
|
339
|
-
|
|
340
|
-
init_project() {
|
|
341
|
-
local err_file
|
|
342
|
-
err_file="$(mktemp)"
|
|
343
|
-
trap 'rm -f "$err_file"' RETURN
|
|
401
|
+
# 5. OpenCode (Global)
|
|
402
|
+
inject_sse "$HOME/.opencode/mcp.json" "OpenCode"
|
|
344
403
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
fi
|
|
404
|
+
# 6. Cursor (Global)
|
|
405
|
+
inject_sse "$HOME/.cursor/mcp.json" "Cursor"
|
|
348
406
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
ensure_codegraph
|
|
352
|
-
codegraph init -i "$REPO_ROOT"
|
|
353
|
-
return 0
|
|
354
|
-
fi
|
|
407
|
+
# 7. VSCode native MCP (Global)
|
|
408
|
+
inject_sse "$HOME/.vscode/mcp.json" "VSCode"
|
|
355
409
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
410
|
+
# 8. Cline (VSCode extension)
|
|
411
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json" "Cline"
|
|
359
412
|
|
|
360
|
-
|
|
413
|
+
# 9. Roo (VSCode extension)
|
|
414
|
+
inject_sse "$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json" "Roo Code"
|
|
361
415
|
`;
|
|
362
416
|
this.writeFile(filePath, content);
|
|
363
417
|
if (!this.context.dryRun) {
|
|
364
|
-
|
|
418
|
+
chmodSync2(join3(this.context.targetDir, filePath), 493);
|
|
365
419
|
}
|
|
366
420
|
return {
|
|
367
421
|
success: true,
|
|
368
|
-
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph.sh" : "\u2705 Created .mise/scripts/codegraph.sh"),
|
|
422
|
+
message: this.formatMessage(this.context.dryRun ? "Would create .mise/scripts/codegraph-wire.sh" : "\u2705 Created .mise/scripts/codegraph-wire.sh"),
|
|
369
423
|
filePath
|
|
370
424
|
};
|
|
371
425
|
}
|
|
372
426
|
};
|
|
373
427
|
|
|
428
|
+
// src/commands/WireMiseOpInject.ts
|
|
429
|
+
import { join as join4 } from "node:path";
|
|
430
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2, chmodSync as chmodSync3 } from "node:fs";
|
|
431
|
+
var WireMiseOpInject = class _WireMiseOpInject extends Command {
|
|
432
|
+
static MARKER = "# pjangler:op-inject";
|
|
433
|
+
static CR = "{{config_root}}";
|
|
434
|
+
async invoke() {
|
|
435
|
+
const scriptPath = ".mise/scripts/inject-op-secrets.sh";
|
|
436
|
+
const fullScriptPath = join4(this.context.targetDir, scriptPath);
|
|
437
|
+
const scriptContent = `#!/usr/bin/env bash
|
|
438
|
+
# Resolve 1Password \`op://\` references in .env.op into .env.secrets, which mise
|
|
439
|
+
# loads via \`_.file\`.
|
|
440
|
+
#
|
|
441
|
+
# Runs from the mise enter hook: fail-open and non-interactive.
|
|
442
|
+
# No .env.op means no 1Password, so this exits before spending anything.
|
|
443
|
+
|
|
444
|
+
set -euo pipefail
|
|
445
|
+
|
|
446
|
+
REPO_ROOT="\${MISE_PROJECT_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}"
|
|
447
|
+
SRC="$REPO_ROOT/.env.op"
|
|
448
|
+
OUT="$REPO_ROOT/.env.secrets"
|
|
449
|
+
|
|
450
|
+
TTL_HOURS="\${OP_INJECT_TTL_HOURS:-12}"
|
|
451
|
+
|
|
452
|
+
[[ -f "$SRC" ]] || exit 0
|
|
453
|
+
command -v op >/dev/null 2>&1 || exit 0
|
|
454
|
+
|
|
455
|
+
if [[ -f "$OUT" && "$SRC" -ot "$OUT" ]]; then
|
|
456
|
+
if [[ "$TTL_HOURS" != "0" ]] && [[ -z "$(find "$OUT" -mmin "+$((TTL_HOURS * 60))" 2>/dev/null)" ]]; then
|
|
457
|
+
exit 0
|
|
458
|
+
fi
|
|
459
|
+
fi
|
|
460
|
+
|
|
461
|
+
op account list >/dev/null 2>&1 || exit 0
|
|
462
|
+
|
|
463
|
+
tmp="$(mktemp "\${OUT}.XXXXXX")"
|
|
464
|
+
trap 'rm -f "$tmp"' EXIT
|
|
465
|
+
chmod 600 "$tmp"
|
|
466
|
+
|
|
467
|
+
if op inject -i "$SRC" -o "$tmp" --force >/dev/null 2>&1; then
|
|
468
|
+
mv "$tmp" "$OUT"
|
|
469
|
+
trap - EXIT
|
|
470
|
+
else
|
|
471
|
+
echo "mise: op inject from .env.op failed \u2014 $(basename "$OUT") unchanged." >&2
|
|
472
|
+
echo " Check 'op signin', or that .env.op's op:// items are readable." >&2
|
|
473
|
+
echo " To disable: rm .env.op" >&2
|
|
474
|
+
fi
|
|
475
|
+
`;
|
|
476
|
+
this.writeFile(scriptPath, scriptContent);
|
|
477
|
+
if (!this.context.dryRun) {
|
|
478
|
+
chmodSync3(fullScriptPath, 493);
|
|
479
|
+
}
|
|
480
|
+
const misePath = join4(this.context.targetDir, "mise.toml");
|
|
481
|
+
if (!existsSync2(misePath)) {
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pj init mise` first, then re-run."
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
let content = readFileSync(misePath, "utf8");
|
|
488
|
+
if (content.includes(_WireMiseOpInject.MARKER)) {
|
|
489
|
+
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for op-inject") };
|
|
490
|
+
}
|
|
491
|
+
const cr = _WireMiseOpInject.CR;
|
|
492
|
+
const envRe = /\[env\]\s*\n([\s\S]*?(?=\n\[|$))/;
|
|
493
|
+
if (envRe.test(content)) {
|
|
494
|
+
content = content.replace(envRe, (m) => {
|
|
495
|
+
let block = m;
|
|
496
|
+
if (!block.includes("_.file")) {
|
|
497
|
+
block += `
|
|
498
|
+
_.file = ['.env', '.env.secrets']
|
|
499
|
+
`;
|
|
500
|
+
} else if (!block.includes(".env.secrets")) {
|
|
501
|
+
block = block.replace(/(_\.file\s*=\s*\[)([^\]]*?)(\])/, (m2, prefix, files, suffix) => {
|
|
502
|
+
const added = files.trim() ? `${files}, '.env.secrets'` : `'.env.secrets'`;
|
|
503
|
+
return `${prefix}${added}${suffix}`;
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return block;
|
|
507
|
+
});
|
|
508
|
+
} else {
|
|
509
|
+
const envBlock = `[env]
|
|
510
|
+
_.file = ['.env', '.env.secrets']
|
|
511
|
+
|
|
512
|
+
`;
|
|
513
|
+
if (content.includes("[tools]")) {
|
|
514
|
+
content = content.replace(/(\[tools\][\s\S]*?(?=\n\[|$))/, `$1
|
|
515
|
+
${envBlock}`);
|
|
516
|
+
} else {
|
|
517
|
+
content = envBlock + content;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const enterAdds = ` '${cr}/.mise/scripts/inject-op-secrets.sh',`;
|
|
521
|
+
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
522
|
+
if (enterRe.test(content)) {
|
|
523
|
+
content = content.replace(enterRe, (_m, head, close) => {
|
|
524
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
525
|
+
return `${head}${sep}
|
|
526
|
+
${enterAdds}${close}`;
|
|
527
|
+
});
|
|
528
|
+
} else {
|
|
529
|
+
if (content.includes("[hooks]")) {
|
|
530
|
+
content = content.replace(/\[hooks\]/, `[hooks]
|
|
531
|
+
enter = [
|
|
532
|
+
${enterAdds}
|
|
533
|
+
]`);
|
|
534
|
+
} else {
|
|
535
|
+
content += `
|
|
536
|
+
[hooks]
|
|
537
|
+
enter = [
|
|
538
|
+
${enterAdds}
|
|
539
|
+
]
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const appended = [
|
|
544
|
+
"",
|
|
545
|
+
_WireMiseOpInject.MARKER,
|
|
546
|
+
"[tasks.secrets-inject]",
|
|
547
|
+
'description = "Re-resolve .env.op secrets from 1Password into .env.secrets"',
|
|
548
|
+
`run = "OP_INJECT_TTL_HOURS=0 .mise/scripts/inject-op-secrets.sh"`,
|
|
549
|
+
_WireMiseOpInject.MARKER + ":end",
|
|
550
|
+
""
|
|
551
|
+
].join("\n");
|
|
552
|
+
content = content.replace(/\n*$/, "\n") + appended;
|
|
553
|
+
if (!this.context.dryRun) writeFileSync2(misePath, content);
|
|
554
|
+
return {
|
|
555
|
+
success: true,
|
|
556
|
+
message: this.formatMessage("\u2705 Wired mise.toml for op-inject (_.file, [hooks] enter, tasks.secrets-inject)")
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
374
561
|
// src/recipes/MiseRecipe.ts
|
|
375
562
|
var MiseRecipe = class extends Recipe {
|
|
376
563
|
constructor(context) {
|
|
377
564
|
super(context);
|
|
378
|
-
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript);
|
|
565
|
+
this.addIngredient(AddMiseToml).addIngredient(AddDotenv).addIngredient(AddMiseTasksStructure).addIngredient(AddMiseBaseToml).addIngredient(AddMiseBaseScript).addIngredient(AddMiseCodegraphScript).addIngredient(AddMiseCodegraphWireScript).addIngredient(WireMiseOpInject);
|
|
379
566
|
}
|
|
380
567
|
printNextSteps() {
|
|
381
568
|
console.log("\u{1F389} Mise subsystem initialized successfully!");
|
|
@@ -604,34 +791,34 @@ var NodeRecipe = class extends Recipe {
|
|
|
604
791
|
|
|
605
792
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
606
793
|
import { homedir, platform } from "node:os";
|
|
607
|
-
import { existsSync as
|
|
608
|
-
import { join as
|
|
794
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
795
|
+
import { join as join5, dirname as dirname2 } from "node:path";
|
|
609
796
|
function resolveTemplateConfigPath() {
|
|
610
797
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
611
798
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
612
799
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
613
|
-
const base = xdg && xdg.length ? xdg :
|
|
614
|
-
return
|
|
800
|
+
const base = xdg && xdg.length ? xdg : join5(homedir(), ".config");
|
|
801
|
+
return join5(base, "hermes-agent-template", "config.toml");
|
|
615
802
|
}
|
|
616
803
|
function detectHermesBin(home) {
|
|
617
804
|
const candidates = [
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
805
|
+
join5(home, "code", "hermes-agent", "venv", "bin", "hermes"),
|
|
806
|
+
join5(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
|
|
807
|
+
join5(home, ".local", "bin", "hermes")
|
|
621
808
|
];
|
|
622
809
|
for (const c of candidates) {
|
|
623
|
-
if (
|
|
810
|
+
if (existsSync3(c)) return c;
|
|
624
811
|
}
|
|
625
812
|
return candidates[0];
|
|
626
813
|
}
|
|
627
814
|
function renderHostConfig() {
|
|
628
815
|
const home = homedir();
|
|
629
816
|
const hermesBin = detectHermesBin(home);
|
|
630
|
-
const hermesRepo =
|
|
631
|
-
const scaffoldDir =
|
|
632
|
-
const skillsDir =
|
|
633
|
-
const pmExternalSkillGlobalDir =
|
|
634
|
-
const pmExternalSkillBmadDir =
|
|
817
|
+
const hermesRepo = join5(home, "code", "hermes-agent");
|
|
818
|
+
const scaffoldDir = join5(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
819
|
+
const skillsDir = join5(home, ".agents", "skills");
|
|
820
|
+
const pmExternalSkillGlobalDir = join5(home, "code", "skillex", "skill-sets", "global", ".system");
|
|
821
|
+
const pmExternalSkillBmadDir = join5(home, "code", "skillex", "packs", "bmad", "6.10.2");
|
|
635
822
|
return `# hermes-agent-template \u2014 host configuration
|
|
636
823
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
637
824
|
#
|
|
@@ -680,7 +867,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
680
867
|
const ctx = this.context;
|
|
681
868
|
const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
|
|
682
869
|
const path = resolveTemplateConfigPath();
|
|
683
|
-
const exists =
|
|
870
|
+
const exists = existsSync3(path);
|
|
684
871
|
if (exists && !force) {
|
|
685
872
|
console.log(`\u2713 Config present: ${path}`);
|
|
686
873
|
return { success: true, message: "" };
|
|
@@ -691,7 +878,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
691
878
|
}
|
|
692
879
|
try {
|
|
693
880
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
694
|
-
|
|
881
|
+
writeFileSync3(path, renderHostConfig());
|
|
695
882
|
} catch (err) {
|
|
696
883
|
const msg = err instanceof Error ? err.message : String(err);
|
|
697
884
|
return { success: false, message: `\u2717 Failed to write ${path}: ${msg}` };
|
|
@@ -703,8 +890,8 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
703
890
|
};
|
|
704
891
|
|
|
705
892
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
706
|
-
import { basename, join as
|
|
707
|
-
import { readFileSync } from "node:fs";
|
|
893
|
+
import { basename, join as join6 } from "node:path";
|
|
894
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
708
895
|
import * as p from "@clack/prompts";
|
|
709
896
|
|
|
710
897
|
// src/commands/hermes/types.ts
|
|
@@ -719,7 +906,7 @@ function deriveProfileName(repo, role) {
|
|
|
719
906
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
720
907
|
function detectTicketProvider(targetDir) {
|
|
721
908
|
try {
|
|
722
|
-
const t = JSON.parse(
|
|
909
|
+
const t = JSON.parse(readFileSync2(join6(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
723
910
|
return t === "plane" || t === "trello" ? t : void 0;
|
|
724
911
|
} catch {
|
|
725
912
|
return void 0;
|
|
@@ -777,8 +964,8 @@ var PromptForAgentConfig = class extends Command {
|
|
|
777
964
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
778
965
|
import { spawnSync } from "node:child_process";
|
|
779
966
|
import { homedir as homedir2 } from "node:os";
|
|
780
|
-
import { join as
|
|
781
|
-
import { existsSync as
|
|
967
|
+
import { join as join7, dirname as dirname3 } from "node:path";
|
|
968
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
782
969
|
import { fileURLToPath } from "node:url";
|
|
783
970
|
import * as p2 from "@clack/prompts";
|
|
784
971
|
function resolveVendoredTemplate(name) {
|
|
@@ -789,8 +976,8 @@ function resolveVendoredTemplate(name) {
|
|
|
789
976
|
return void 0;
|
|
790
977
|
}
|
|
791
978
|
for (let i = 0; i < 8; i++) {
|
|
792
|
-
const candidate =
|
|
793
|
-
if (
|
|
979
|
+
const candidate = join7(dir, "templates", name);
|
|
980
|
+
if (existsSync4(join7(candidate, "copier.yml"))) return candidate;
|
|
794
981
|
const parent = dirname3(dir);
|
|
795
982
|
if (parent === dir) break;
|
|
796
983
|
dir = parent;
|
|
@@ -809,7 +996,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
809
996
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
810
997
|
};
|
|
811
998
|
}
|
|
812
|
-
const roleDir =
|
|
999
|
+
const roleDir = join7(ctx.targetDir, "agents", "hermes", role);
|
|
813
1000
|
ctx.roleDir = roleDir;
|
|
814
1001
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
815
1002
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -819,7 +1006,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
819
1006
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
820
1007
|
};
|
|
821
1008
|
}
|
|
822
|
-
if (
|
|
1009
|
+
if (existsSync4(join7(roleDir, "role.yaml")) && !ctx.force) {
|
|
823
1010
|
if (ctx.yes) {
|
|
824
1011
|
ctx.force = true;
|
|
825
1012
|
} else {
|
|
@@ -846,9 +1033,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
846
1033
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
847
1034
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
848
1035
|
};
|
|
849
|
-
const LOCAL_TEMPLATE =
|
|
1036
|
+
const LOCAL_TEMPLATE = join7(homedir2(), "code", "hermes-agent-template");
|
|
850
1037
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
851
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (
|
|
1038
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync4(join7(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
852
1039
|
const args = [
|
|
853
1040
|
"copy",
|
|
854
1041
|
templateSrc,
|
|
@@ -879,7 +1066,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
879
1066
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
880
1067
|
};
|
|
881
1068
|
}
|
|
882
|
-
mkdirSync3(
|
|
1069
|
+
mkdirSync3(join7(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
883
1070
|
const spinner4 = p2.spinner();
|
|
884
1071
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
885
1072
|
const result = spawnSync("copier", args, {
|
|
@@ -903,14 +1090,14 @@ var RunCopierTemplate = class extends Command {
|
|
|
903
1090
|
};
|
|
904
1091
|
|
|
905
1092
|
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
906
|
-
import { existsSync as
|
|
907
|
-
import { join as
|
|
1093
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4, readdirSync } from "fs";
|
|
1094
|
+
import { join as join8 } from "path";
|
|
908
1095
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
909
1096
|
var UntrackHermesRuntimes = class extends Command {
|
|
910
1097
|
async invoke() {
|
|
911
1098
|
const targetDir = this.context.targetDir;
|
|
912
|
-
const rolesDir =
|
|
913
|
-
if (!
|
|
1099
|
+
const rolesDir = join8(targetDir, "agents", "hermes");
|
|
1100
|
+
if (!existsSync5(rolesDir)) {
|
|
914
1101
|
return {
|
|
915
1102
|
success: true,
|
|
916
1103
|
message: "No Hermes agents found (no agents/hermes directory)."
|
|
@@ -926,9 +1113,9 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
926
1113
|
let modifiedAny = false;
|
|
927
1114
|
const details = [];
|
|
928
1115
|
for (const role of roles) {
|
|
929
|
-
const roleDir =
|
|
930
|
-
const runtimePath =
|
|
931
|
-
const gitignorePath =
|
|
1116
|
+
const roleDir = join8("agents", "hermes", role);
|
|
1117
|
+
const runtimePath = join8(roleDir, "runtime");
|
|
1118
|
+
const gitignorePath = join8(roleDir, ".gitignore");
|
|
932
1119
|
let isTracked = false;
|
|
933
1120
|
const lsResult = spawnSync2("git", ["ls-files", "--stage", runtimePath], {
|
|
934
1121
|
cwd: targetDir,
|
|
@@ -938,9 +1125,9 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
938
1125
|
isTracked = true;
|
|
939
1126
|
}
|
|
940
1127
|
let isIgnored = false;
|
|
941
|
-
const fullGitignorePath =
|
|
942
|
-
if (
|
|
943
|
-
const content =
|
|
1128
|
+
const fullGitignorePath = join8(targetDir, gitignorePath);
|
|
1129
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1130
|
+
const content = readFileSync3(fullGitignorePath, "utf8");
|
|
944
1131
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
945
1132
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
946
1133
|
}
|
|
@@ -965,14 +1152,14 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
965
1152
|
details.push(`ignore runtime/ in agents/hermes/${role}/.gitignore`);
|
|
966
1153
|
if (!this.context.dryRun) {
|
|
967
1154
|
let content = "";
|
|
968
|
-
if (
|
|
969
|
-
content =
|
|
1155
|
+
if (existsSync5(fullGitignorePath)) {
|
|
1156
|
+
content = readFileSync3(fullGitignorePath, "utf8");
|
|
970
1157
|
}
|
|
971
1158
|
if (content && !content.endsWith("\n")) {
|
|
972
1159
|
content += "\n";
|
|
973
1160
|
}
|
|
974
1161
|
content += "runtime/\n";
|
|
975
|
-
|
|
1162
|
+
writeFileSync4(fullGitignorePath, content, "utf8");
|
|
976
1163
|
}
|
|
977
1164
|
}
|
|
978
1165
|
}
|
|
@@ -994,8 +1181,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
|
|
|
994
1181
|
|
|
995
1182
|
// src/commands/hermes/WireTelegram.ts
|
|
996
1183
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
997
|
-
import { join as
|
|
998
|
-
import { existsSync as
|
|
1184
|
+
import { join as join9 } from "node:path";
|
|
1185
|
+
import { existsSync as existsSync6, unlinkSync } from "node:fs";
|
|
999
1186
|
import * as p3 from "@clack/prompts";
|
|
1000
1187
|
var WireTelegram = class extends Command {
|
|
1001
1188
|
async invoke() {
|
|
@@ -1083,15 +1270,15 @@ var WireTelegram = class extends Command {
|
|
|
1083
1270
|
if (p3.isCancel(allowedAnswer)) {
|
|
1084
1271
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
1085
1272
|
}
|
|
1086
|
-
const script =
|
|
1087
|
-
if (!
|
|
1273
|
+
const script = join9(roleDir, ".scripts", "30-telegram.sh");
|
|
1274
|
+
if (!existsSync6(script)) {
|
|
1088
1275
|
return {
|
|
1089
1276
|
success: false,
|
|
1090
1277
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
1091
1278
|
};
|
|
1092
1279
|
}
|
|
1093
|
-
const marker =
|
|
1094
|
-
if (
|
|
1280
|
+
const marker = join9(roleDir, ".scripts", ".done-30-telegram");
|
|
1281
|
+
if (existsSync6(marker)) unlinkSync(marker);
|
|
1095
1282
|
const spinner4 = p3.spinner();
|
|
1096
1283
|
spinner4.start("Verifying token + wiring profile");
|
|
1097
1284
|
const result = spawnSync3("bash", [script], {
|
|
@@ -1118,8 +1305,8 @@ function cap(s) {
|
|
|
1118
1305
|
|
|
1119
1306
|
// src/commands/hermes/WireEmail.ts
|
|
1120
1307
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1121
|
-
import { join as
|
|
1122
|
-
import { existsSync as
|
|
1308
|
+
import { join as join10 } from "node:path";
|
|
1309
|
+
import { existsSync as existsSync7, unlinkSync as unlinkSync2 } from "node:fs";
|
|
1123
1310
|
import * as p4 from "@clack/prompts";
|
|
1124
1311
|
var WireEmail = class extends Command {
|
|
1125
1312
|
async invoke() {
|
|
@@ -1134,8 +1321,8 @@ var WireEmail = class extends Command {
|
|
|
1134
1321
|
if (!targetRepo || !role || !roleDir) {
|
|
1135
1322
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1136
1323
|
}
|
|
1137
|
-
const script =
|
|
1138
|
-
if (!
|
|
1324
|
+
const script = join10(roleDir, ".scripts", "50-email.sh");
|
|
1325
|
+
if (!existsSync7(script)) {
|
|
1139
1326
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1140
1327
|
}
|
|
1141
1328
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
@@ -1197,8 +1384,8 @@ var WireEmail = class extends Command {
|
|
|
1197
1384
|
}
|
|
1198
1385
|
}
|
|
1199
1386
|
}
|
|
1200
|
-
const marker =
|
|
1201
|
-
if (
|
|
1387
|
+
const marker = join10(roleDir, ".scripts", ".done-50-email");
|
|
1388
|
+
if (existsSync7(marker)) unlinkSync2(marker);
|
|
1202
1389
|
const spinner4 = p4.spinner();
|
|
1203
1390
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1204
1391
|
const result = spawnSync4("bash", [script], {
|
|
@@ -1285,33 +1472,261 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1285
1472
|
|
|
1286
1473
|
// src/commands/AgentHooksCommands.ts
|
|
1287
1474
|
import { homedir as homedir4 } from "node:os";
|
|
1288
|
-
import { join as
|
|
1289
|
-
import { existsSync as
|
|
1475
|
+
import { join as join12, dirname as dirname5 } from "node:path";
|
|
1476
|
+
import { existsSync as existsSync9, cpSync, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1290
1477
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1291
1478
|
|
|
1292
1479
|
// src/project/index.ts
|
|
1293
1480
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
1294
|
-
import { existsSync as
|
|
1481
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1295
1482
|
import { homedir as homedir3 } from "node:os";
|
|
1296
|
-
import { basename as basename2, delimiter, dirname as dirname4, join as
|
|
1483
|
+
import { basename as basename2, delimiter, dirname as dirname4, join as join11, resolve } from "node:path";
|
|
1484
|
+
import YAML2 from "yaml";
|
|
1485
|
+
|
|
1486
|
+
// src/project/RegistryStore.ts
|
|
1487
|
+
import { Pool } from "pg";
|
|
1297
1488
|
import YAML from "yaml";
|
|
1489
|
+
function pgRegistryConfigFromEnv(env2 = process.env) {
|
|
1490
|
+
return {
|
|
1491
|
+
host: env2.PGHOST || "localhost",
|
|
1492
|
+
port: parseInt(env2.PGPORT || "5432", 10),
|
|
1493
|
+
user: env2.PGUSER || "delorenj",
|
|
1494
|
+
password: env2.PGPASSWORD || "",
|
|
1495
|
+
database: env2.PGDATABASE || "33god"
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
var PgRegistryStore = class {
|
|
1499
|
+
pool;
|
|
1500
|
+
constructor(config) {
|
|
1501
|
+
this.pool = new Pool({
|
|
1502
|
+
host: config.host,
|
|
1503
|
+
port: config.port,
|
|
1504
|
+
user: config.user,
|
|
1505
|
+
password: config.password,
|
|
1506
|
+
database: config.database
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
async load() {
|
|
1510
|
+
const client = await this.pool.connect();
|
|
1511
|
+
try {
|
|
1512
|
+
const { rows } = await client.query(
|
|
1513
|
+
`SELECT p.id, p.name, p.description, p.slug, p.status,
|
|
1514
|
+
p.source_artifacts, p.template, p.automation,
|
|
1515
|
+
p.created_at, p.updated_at,
|
|
1516
|
+
r.id AS repo_id, r.local_path
|
|
1517
|
+
FROM public.projects p
|
|
1518
|
+
LEFT JOIN public.repos r ON r.project_id = p.id
|
|
1519
|
+
WHERE p.slug IS NOT NULL`
|
|
1520
|
+
);
|
|
1521
|
+
const projects = {};
|
|
1522
|
+
for (const row of rows) {
|
|
1523
|
+
const slug = row.slug;
|
|
1524
|
+
const ticketProvider = await this.loadTicketProvider(client, row.id);
|
|
1525
|
+
const agents = await this.loadAgents(client, row.id, slug);
|
|
1526
|
+
projects[slug] = {
|
|
1527
|
+
name: row.name ?? "",
|
|
1528
|
+
slug,
|
|
1529
|
+
repo_path: row.local_path ?? "",
|
|
1530
|
+
description: row.description ?? "",
|
|
1531
|
+
status: row.status ?? "planned",
|
|
1532
|
+
source_artifacts: row.source_artifacts ?? [],
|
|
1533
|
+
template: row.template ?? {
|
|
1534
|
+
commonproject: { enabled: false, primary_language: "python" }
|
|
1535
|
+
},
|
|
1536
|
+
ticket_provider: ticketProvider,
|
|
1537
|
+
agents,
|
|
1538
|
+
automation: row.automation ?? void 0,
|
|
1539
|
+
created_at: row.created_at.toISOString(),
|
|
1540
|
+
updated_at: row.updated_at.toISOString()
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
const registry = {
|
|
1544
|
+
schema_version: PROJECT_REGISTRY_SCHEMA_VERSION,
|
|
1545
|
+
projects
|
|
1546
|
+
};
|
|
1547
|
+
validateProjectRegistry(registry);
|
|
1548
|
+
return registry;
|
|
1549
|
+
} finally {
|
|
1550
|
+
client.release();
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async save(registry) {
|
|
1554
|
+
validateProjectRegistry(registry);
|
|
1555
|
+
const client = await this.pool.connect();
|
|
1556
|
+
try {
|
|
1557
|
+
await client.query("BEGIN");
|
|
1558
|
+
for (const [slug, record] of Object.entries(registry.projects)) {
|
|
1559
|
+
await this.upsertInTx(client, slug, record);
|
|
1560
|
+
}
|
|
1561
|
+
await client.query("COMMIT");
|
|
1562
|
+
} catch (err) {
|
|
1563
|
+
await client.query("ROLLBACK");
|
|
1564
|
+
throw err;
|
|
1565
|
+
} finally {
|
|
1566
|
+
client.release();
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
async upsert(slug, record) {
|
|
1570
|
+
const client = await this.pool.connect();
|
|
1571
|
+
try {
|
|
1572
|
+
await client.query("BEGIN");
|
|
1573
|
+
await this.upsertInTx(client, slug, record);
|
|
1574
|
+
await client.query("COMMIT");
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
await client.query("ROLLBACK");
|
|
1577
|
+
throw err;
|
|
1578
|
+
} finally {
|
|
1579
|
+
client.release();
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
async getBySlug(slug) {
|
|
1583
|
+
const registry = await this.load();
|
|
1584
|
+
return registry.projects[slug];
|
|
1585
|
+
}
|
|
1586
|
+
async getByRepoPath(repoPath) {
|
|
1587
|
+
const registry = await this.load();
|
|
1588
|
+
return Object.values(registry.projects).find(
|
|
1589
|
+
(p6) => p6.repo_path === repoPath
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
async close() {
|
|
1593
|
+
await this.pool.end();
|
|
1594
|
+
}
|
|
1595
|
+
// --- private helpers ---
|
|
1596
|
+
async upsertInTx(client, slug, record) {
|
|
1597
|
+
if (!slug) throw new Error("PgRegistryStore.upsert: slug is required");
|
|
1598
|
+
const projectResult = await client.query(
|
|
1599
|
+
`INSERT INTO public.projects (name, description, slug, status, source_artifacts, template, automation)
|
|
1600
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
1601
|
+
ON CONFLICT (slug) WHERE slug IS NOT NULL
|
|
1602
|
+
DO UPDATE SET
|
|
1603
|
+
name = EXCLUDED.name,
|
|
1604
|
+
description = EXCLUDED.description,
|
|
1605
|
+
status = EXCLUDED.status,
|
|
1606
|
+
source_artifacts = EXCLUDED.source_artifacts,
|
|
1607
|
+
template = EXCLUDED.template,
|
|
1608
|
+
automation = EXCLUDED.automation
|
|
1609
|
+
RETURNING id`,
|
|
1610
|
+
[
|
|
1611
|
+
record.name,
|
|
1612
|
+
record.description,
|
|
1613
|
+
slug,
|
|
1614
|
+
record.status,
|
|
1615
|
+
JSON.stringify(record.source_artifacts),
|
|
1616
|
+
JSON.stringify(record.template),
|
|
1617
|
+
record.automation ? JSON.stringify(record.automation) : null
|
|
1618
|
+
]
|
|
1619
|
+
);
|
|
1620
|
+
const projectId = projectResult.rows[0]?.id;
|
|
1621
|
+
if (!projectId) throw new Error(`Failed to upsert project: ${slug}`);
|
|
1622
|
+
await client.query(
|
|
1623
|
+
`INSERT INTO public.repos (project_id, local_path)
|
|
1624
|
+
VALUES ($1, $2)
|
|
1625
|
+
ON CONFLICT (local_path)
|
|
1626
|
+
DO UPDATE SET project_id = EXCLUDED.project_id
|
|
1627
|
+
RETURNING id`,
|
|
1628
|
+
[projectId, record.repo_path]
|
|
1629
|
+
);
|
|
1630
|
+
const repoResult = await client.query(
|
|
1631
|
+
`SELECT id FROM public.repos WHERE project_id = $1 AND local_path = $2`,
|
|
1632
|
+
[projectId, record.repo_path]
|
|
1633
|
+
);
|
|
1634
|
+
const repoId = repoResult.rows[0]?.id;
|
|
1635
|
+
if (!repoId) throw new Error(`Failed to find repo for project ${slug} at path ${record.repo_path}`);
|
|
1636
|
+
await this.upsertTicketProvider(client, projectId, repoId, record.ticket_provider);
|
|
1637
|
+
await client.query(
|
|
1638
|
+
`DELETE FROM public.project_agents WHERE project_id = $1`,
|
|
1639
|
+
[projectId]
|
|
1640
|
+
);
|
|
1641
|
+
for (const [agentKey, agent] of Object.entries(record.agents)) {
|
|
1642
|
+
await client.query(
|
|
1643
|
+
`INSERT INTO public.project_agents (repo_id, project_id, agent_key, role, role_dir, provisioning_state)
|
|
1644
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
1645
|
+
[repoId, projectId, agentKey, agent.role, agent.role_dir ?? null, agent.provisioning_state]
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
async loadTicketProvider(client, projectId) {
|
|
1650
|
+
const { rows } = await client.query(
|
|
1651
|
+
`SELECT provider_type, workspace, identifier, board_id, state
|
|
1652
|
+
FROM public.project_ticket_boards
|
|
1653
|
+
WHERE project_id = $1
|
|
1654
|
+
LIMIT 1`,
|
|
1655
|
+
[projectId]
|
|
1656
|
+
);
|
|
1657
|
+
if (!rows.length) {
|
|
1658
|
+
return { type: "plane", workspace: "33god", identifier: "", board_id: "", state: "planned" };
|
|
1659
|
+
}
|
|
1660
|
+
const row = rows[0];
|
|
1661
|
+
return {
|
|
1662
|
+
type: row.provider_type,
|
|
1663
|
+
workspace: row.workspace ?? void 0,
|
|
1664
|
+
identifier: row.identifier ?? void 0,
|
|
1665
|
+
board_id: row.board_id ?? void 0,
|
|
1666
|
+
state: row.state ?? void 0
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
async loadAgents(client, projectId, slug) {
|
|
1670
|
+
const { rows } = await client.query(
|
|
1671
|
+
`SELECT agent_key, role, role_dir, provisioning_state
|
|
1672
|
+
FROM public.project_agents
|
|
1673
|
+
WHERE project_id = $1`,
|
|
1674
|
+
[projectId]
|
|
1675
|
+
);
|
|
1676
|
+
const agents = {};
|
|
1677
|
+
for (const row of rows) {
|
|
1678
|
+
agents[row.agent_key] = {
|
|
1679
|
+
role: row.role,
|
|
1680
|
+
provisioning_state: row.provisioning_state,
|
|
1681
|
+
role_dir: row.role_dir ?? void 0
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
return agents;
|
|
1685
|
+
}
|
|
1686
|
+
async upsertTicketProvider(client, projectId, repoId, tp) {
|
|
1687
|
+
await client.query(
|
|
1688
|
+
`DELETE FROM public.project_ticket_boards WHERE project_id = $1`,
|
|
1689
|
+
[projectId]
|
|
1690
|
+
);
|
|
1691
|
+
await client.query(
|
|
1692
|
+
`INSERT INTO public.project_ticket_boards
|
|
1693
|
+
(repo_id, project_id, provider_type, workspace, identifier, board_id, state)
|
|
1694
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
1695
|
+
[
|
|
1696
|
+
repoId,
|
|
1697
|
+
projectId,
|
|
1698
|
+
tp.type,
|
|
1699
|
+
tp.workspace ?? null,
|
|
1700
|
+
tp.identifier ?? null,
|
|
1701
|
+
tp.board_id ?? null,
|
|
1702
|
+
tp.state ?? null
|
|
1703
|
+
]
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
var PJ_REGISTRY_PG_ENV = "PJ_REGISTRY_PG";
|
|
1708
|
+
function isPgRegistryEnabled(env2 = process.env) {
|
|
1709
|
+
return env2[PJ_REGISTRY_PG_ENV] === "1" || env2[PJ_REGISTRY_PG_ENV] === "true";
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// src/project/index.ts
|
|
1298
1713
|
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
1299
1714
|
var PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
1300
1715
|
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
1301
1716
|
var DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
1302
1717
|
"/home/delorenj/code/skillex/all-skills",
|
|
1303
|
-
|
|
1304
|
-
|
|
1718
|
+
join11(homedir3(), ".agents", "skills"),
|
|
1719
|
+
join11(homedir3(), ".codex", "skills")
|
|
1305
1720
|
];
|
|
1306
1721
|
function projectRegistryPath(env2 = process.env) {
|
|
1307
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1722
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join11(homedir3(), ".config", "pjangler", "projects.yaml"));
|
|
1308
1723
|
}
|
|
1309
1724
|
function emptyProjectRegistry() {
|
|
1310
1725
|
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
1311
1726
|
}
|
|
1312
1727
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1313
|
-
if (!
|
|
1314
|
-
const raw =
|
|
1728
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
1729
|
+
const raw = YAML2.parse(readFileSync4(path, "utf8"));
|
|
1315
1730
|
if (raw == null) return emptyProjectRegistry();
|
|
1316
1731
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1317
1732
|
const registry = raw;
|
|
@@ -1326,7 +1741,7 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1326
1741
|
validateProjectRegistry(registry);
|
|
1327
1742
|
mkdirSync4(dirname4(path), { recursive: true });
|
|
1328
1743
|
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
1329
|
-
|
|
1744
|
+
writeFileSync5(temp, YAML2.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
1330
1745
|
renameSync(temp, path);
|
|
1331
1746
|
}
|
|
1332
1747
|
function validateProjectRegistry(registry) {
|
|
@@ -1408,7 +1823,7 @@ function resolveAgentHooksLayer(input, env2 = process.env) {
|
|
|
1408
1823
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1409
1824
|
if (override === "0" || override === "false") return false;
|
|
1410
1825
|
if (override === "1" || override === "true") return true;
|
|
1411
|
-
return !
|
|
1826
|
+
return !existsSync8(join11(homedir3(), ".agents", "hooks"));
|
|
1412
1827
|
}
|
|
1413
1828
|
function jsonStable(value) {
|
|
1414
1829
|
return JSON.stringify(value);
|
|
@@ -1439,12 +1854,12 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1439
1854
|
if (!sourceSkill) return void 0;
|
|
1440
1855
|
const expanded = expandHome(sourceSkill);
|
|
1441
1856
|
const direct = resolve(expanded);
|
|
1442
|
-
if (
|
|
1857
|
+
if (existsSync8(direct)) return direct;
|
|
1443
1858
|
const name = basename2(sourceSkill);
|
|
1444
1859
|
const roots = sourceSkillRoots(env2);
|
|
1445
1860
|
for (const root of roots) {
|
|
1446
|
-
const candidate =
|
|
1447
|
-
if (
|
|
1861
|
+
const candidate = join11(root, name);
|
|
1862
|
+
if (existsSync8(candidate)) return candidate;
|
|
1448
1863
|
}
|
|
1449
1864
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
1450
1865
|
const hint = `${searched} Add project-specific roots with ${PROJECT_SOURCE_SKILL_ROOTS_ENV}.`;
|
|
@@ -1525,7 +1940,7 @@ function planProjectInit(input) {
|
|
|
1525
1940
|
}));
|
|
1526
1941
|
}
|
|
1527
1942
|
actions.push(
|
|
1528
|
-
{ kind: "project.write-manifest", path:
|
|
1943
|
+
{ kind: "project.write-manifest", path: join11(targetDir, ".project.json"), manifest },
|
|
1529
1944
|
{
|
|
1530
1945
|
kind: "ticket-provider.create-or-link",
|
|
1531
1946
|
enabled: live,
|
|
@@ -1553,7 +1968,7 @@ function planProjectInit(input) {
|
|
|
1553
1968
|
);
|
|
1554
1969
|
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
1555
1970
|
}
|
|
1556
|
-
function executeProjectInitPlan(plan) {
|
|
1971
|
+
async function executeProjectInitPlan(plan) {
|
|
1557
1972
|
const logs = [];
|
|
1558
1973
|
const errors = [];
|
|
1559
1974
|
const changedFiles = [];
|
|
@@ -1578,7 +1993,7 @@ function executeProjectInitPlan(plan) {
|
|
|
1578
1993
|
}
|
|
1579
1994
|
if (result.status !== 0) {
|
|
1580
1995
|
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
1581
|
-
if (
|
|
1996
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
1582
1997
|
break;
|
|
1583
1998
|
}
|
|
1584
1999
|
changedFiles.push(action.targetDir);
|
|
@@ -1586,9 +2001,9 @@ function executeProjectInitPlan(plan) {
|
|
|
1586
2001
|
mkdirSync4(dirname4(action.path), { recursive: true });
|
|
1587
2002
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
1588
2003
|
`;
|
|
1589
|
-
const current =
|
|
2004
|
+
const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
|
|
1590
2005
|
if (current !== next) {
|
|
1591
|
-
|
|
2006
|
+
writeFileSync5(action.path, next, "utf8");
|
|
1592
2007
|
changedFiles.push(action.path);
|
|
1593
2008
|
}
|
|
1594
2009
|
} else if (action.kind === "registry.upsert") {
|
|
@@ -1604,6 +2019,16 @@ function executeProjectInitPlan(plan) {
|
|
|
1604
2019
|
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
1605
2020
|
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
1606
2021
|
changedFiles.push(pendingRegistryAction.registryPath);
|
|
2022
|
+
if (isPgRegistryEnabled()) {
|
|
2023
|
+
try {
|
|
2024
|
+
const pgStore = new PgRegistryStore(pgRegistryConfigFromEnv());
|
|
2025
|
+
await pgStore.upsert(pendingRegistryAction.slug, pendingRegistryAction.project);
|
|
2026
|
+
await pgStore.close();
|
|
2027
|
+
logs.push("registry: PG dual-write complete");
|
|
2028
|
+
} catch (pgErr) {
|
|
2029
|
+
logs.push(`registry: PG dual-write failed (yaml is authoritative): ${pgErr instanceof Error ? pgErr.message : pgErr}`);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
1607
2032
|
}
|
|
1608
2033
|
}
|
|
1609
2034
|
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
@@ -1641,7 +2066,7 @@ function getProject(registry, slug) {
|
|
|
1641
2066
|
return project;
|
|
1642
2067
|
}
|
|
1643
2068
|
function buildCommonProjectCopierAction(input) {
|
|
1644
|
-
const templateDir =
|
|
2069
|
+
const templateDir = join11(input.pjanglerRoot, "templates", "commonproject");
|
|
1645
2070
|
const data = {
|
|
1646
2071
|
project_name: input.projectName,
|
|
1647
2072
|
project_description: input.projectDescription ?? "",
|
|
@@ -1670,7 +2095,7 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1670
2095
|
function resolvePjanglerRoot() {
|
|
1671
2096
|
let dir = dirname4(new URL(import.meta.url).pathname);
|
|
1672
2097
|
while (dir !== dirname4(dir)) {
|
|
1673
|
-
if (
|
|
2098
|
+
if (existsSync8(join11(dir, "package.json")) && existsSync8(join11(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
1674
2099
|
dir = dirname4(dir);
|
|
1675
2100
|
}
|
|
1676
2101
|
return resolve(process.cwd());
|
|
@@ -1702,7 +2127,7 @@ function validateProjectRecord(project, key) {
|
|
|
1702
2127
|
}
|
|
1703
2128
|
function expandHome(path) {
|
|
1704
2129
|
if (path === "~") return homedir3();
|
|
1705
|
-
if (path.startsWith("~/")) return
|
|
2130
|
+
if (path.startsWith("~/")) return join11(homedir3(), path.slice(2));
|
|
1706
2131
|
return path;
|
|
1707
2132
|
}
|
|
1708
2133
|
function isRecord(value) {
|
|
@@ -1719,16 +2144,16 @@ function resolveTemplateRoot() {
|
|
|
1719
2144
|
try {
|
|
1720
2145
|
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1721
2146
|
for (let i = 0; i < 8; i++) {
|
|
1722
|
-
candidates.push(
|
|
2147
|
+
candidates.push(join12(dir, "templates", "commonproject", "template"));
|
|
1723
2148
|
const parent = dirname5(dir);
|
|
1724
2149
|
if (parent === dir) break;
|
|
1725
2150
|
dir = parent;
|
|
1726
2151
|
}
|
|
1727
2152
|
} catch {
|
|
1728
2153
|
}
|
|
1729
|
-
candidates.push(
|
|
2154
|
+
candidates.push(join12(homedir4(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1730
2155
|
for (const c of candidates) {
|
|
1731
|
-
if (
|
|
2156
|
+
if (existsSync9(join12(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1732
2157
|
}
|
|
1733
2158
|
throw new Error(
|
|
1734
2159
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -1747,18 +2172,17 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
1747
2172
|
}
|
|
1748
2173
|
const items = [
|
|
1749
2174
|
{ rel: ".agents/hooks", dir: true },
|
|
2175
|
+
{ rel: ".agents/skills.json", dir: false },
|
|
1750
2176
|
{ rel: ".agents/local.example.json", dir: false },
|
|
1751
|
-
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
1752
|
-
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
1753
2177
|
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
1754
2178
|
];
|
|
1755
2179
|
const created = [];
|
|
1756
2180
|
const skipped = [];
|
|
1757
2181
|
for (const { rel, dir } of items) {
|
|
1758
|
-
const src =
|
|
1759
|
-
const dest =
|
|
1760
|
-
if (!
|
|
1761
|
-
if (
|
|
2182
|
+
const src = join12(templateRoot, rel);
|
|
2183
|
+
const dest = join12(this.context.targetDir, rel);
|
|
2184
|
+
if (!existsSync9(src)) continue;
|
|
2185
|
+
if (existsSync9(dest) && !this.context.force) {
|
|
1762
2186
|
skipped.push(rel);
|
|
1763
2187
|
continue;
|
|
1764
2188
|
}
|
|
@@ -1784,25 +2208,21 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
1784
2208
|
if (!resolveAgentHooksLayer()) {
|
|
1785
2209
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
1786
2210
|
}
|
|
1787
|
-
const misePath =
|
|
1788
|
-
if (!
|
|
2211
|
+
const misePath = join12(this.context.targetDir, "mise.toml");
|
|
2212
|
+
if (!existsSync9(misePath)) {
|
|
1789
2213
|
return {
|
|
1790
2214
|
success: false,
|
|
1791
2215
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1792
2216
|
};
|
|
1793
2217
|
}
|
|
1794
|
-
let content =
|
|
2218
|
+
let content = readFileSync5(misePath, "utf8");
|
|
1795
2219
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1796
2220
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1797
2221
|
}
|
|
1798
2222
|
const cr = _WireMiseAgentHooks.CR;
|
|
1799
|
-
const enterAdds = [
|
|
1800
|
-
` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
|
|
1801
|
-
` "${cr}/.agents/hooks/sync.py --install --quiet",`
|
|
1802
|
-
].join("\n");
|
|
2223
|
+
const enterAdds = [` "sync-skills.py --scope project",`, ` "${cr}/.agents/hooks/sync.py --install --quiet",`].join("\n");
|
|
1803
2224
|
const leaveBlock = [
|
|
1804
2225
|
"leave = [",
|
|
1805
|
-
` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
|
|
1806
2226
|
` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
|
|
1807
2227
|
"]"
|
|
1808
2228
|
].join("\n");
|
|
@@ -1819,7 +2239,6 @@ ${enterAdds}${close}`;
|
|
|
1819
2239
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
1820
2240
|
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
1821
2241
|
return `${head}${sep}
|
|
1822
|
-
"${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
|
|
1823
2242
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
1824
2243
|
});
|
|
1825
2244
|
} else {
|
|
@@ -1832,6 +2251,14 @@ ${leaveBlock}`);
|
|
|
1832
2251
|
"",
|
|
1833
2252
|
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
1834
2253
|
"[[watch_files]]",
|
|
2254
|
+
'patterns = [".agents/skills.json"]',
|
|
2255
|
+
'task = "skills-sync"',
|
|
2256
|
+
"",
|
|
2257
|
+
"[tasks.skills-sync]",
|
|
2258
|
+
'description = "Sync skills from manifest to local CLI dirs"',
|
|
2259
|
+
'run = "sync-skills.py --scope project"',
|
|
2260
|
+
"",
|
|
2261
|
+
"[[watch_files]]",
|
|
1835
2262
|
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
1836
2263
|
'task = "hooks-sync"',
|
|
1837
2264
|
"",
|
|
@@ -1847,18 +2274,6 @@ ${leaveBlock}`);
|
|
|
1847
2274
|
'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
|
|
1848
2275
|
`run = "${cr}/.agents/hooks/sync.py --uninstall"`,
|
|
1849
2276
|
"",
|
|
1850
|
-
"[tasks.link-project-skills-to-clis]",
|
|
1851
|
-
'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
|
|
1852
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1853
|
-
"",
|
|
1854
|
-
"[tasks.unlink-project-skills-from-clis]",
|
|
1855
|
-
'description = "Remove project skill symlinks from shared per-CLI dirs"',
|
|
1856
|
-
`run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
|
|
1857
|
-
"",
|
|
1858
|
-
"[tasks.skills-relink]",
|
|
1859
|
-
'description = "Re-fan the project skill set to all CLIs"',
|
|
1860
|
-
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1861
|
-
"",
|
|
1862
2277
|
"[tasks.hindsight-setup]",
|
|
1863
2278
|
`description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
|
|
1864
2279
|
`run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
|
|
@@ -1867,7 +2282,7 @@ ${leaveBlock}`);
|
|
|
1867
2282
|
""
|
|
1868
2283
|
].join("\n");
|
|
1869
2284
|
content = content.replace(/\n*$/, "\n") + appended;
|
|
1870
|
-
if (!this.context.dryRun)
|
|
2285
|
+
if (!this.context.dryRun) writeFileSync6(misePath, content);
|
|
1871
2286
|
if (wiredHooks) {
|
|
1872
2287
|
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1873
2288
|
}
|
|
@@ -1876,8 +2291,8 @@ ${leaveBlock}`);
|
|
|
1876
2291
|
message: this.formatMessage(
|
|
1877
2292
|
`\u2705 Added agent-hooks tasks to mise.toml.
|
|
1878
2293
|
\u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
|
|
1879
|
-
enter += "
|
|
1880
|
-
leave += "${cr}/.
|
|
2294
|
+
enter += "sync-skills.py --scope project", "${cr}/.agents/hooks/sync.py --install --quiet"
|
|
2295
|
+
leave += "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
|
|
1881
2296
|
)
|
|
1882
2297
|
};
|
|
1883
2298
|
}
|
|
@@ -1892,10 +2307,25 @@ var AgentHooksRecipe = class extends Recipe {
|
|
|
1892
2307
|
printNextSteps() {
|
|
1893
2308
|
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
1894
2309
|
console.log(" Next steps:");
|
|
1895
|
-
console.log(" 1. mise run
|
|
1896
|
-
console.log(" 2.
|
|
1897
|
-
console.log(" 3.
|
|
1898
|
-
console.log(
|
|
2310
|
+
console.log(" 1. mise run skills-sync # sync .agents/skills.json into local CLI dirs");
|
|
2311
|
+
console.log(" 2. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
2312
|
+
console.log(" 3. git add .claude/settings.json .agents/hooks .agents/skills.json && commit (codex/kimi/hermes are per-dev)");
|
|
2313
|
+
console.log(" 4. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
2314
|
+
console.log(" 5. Optional per-dev hook opt-out: copy .agents/local.example.json -> .agents/local.json");
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
// src/recipes/MiseOpInjectRecipe.ts
|
|
2319
|
+
var MiseOpInjectRecipe = class extends Recipe {
|
|
2320
|
+
constructor(context) {
|
|
2321
|
+
super(context);
|
|
2322
|
+
this.addIngredient(WireMiseOpInject);
|
|
2323
|
+
}
|
|
2324
|
+
printNextSteps() {
|
|
2325
|
+
console.log("\u{1F389} Wired up .env.op 1Password resolution via mise!");
|
|
2326
|
+
console.log(" Next steps:");
|
|
2327
|
+
console.log(" 1. Create .env.op with your op:// secret references");
|
|
2328
|
+
console.log(" 2. Run `mise run secrets-inject` or simply cd out and back in to trigger the hook");
|
|
1899
2329
|
}
|
|
1900
2330
|
};
|
|
1901
2331
|
|
|
@@ -1938,6 +2368,12 @@ var RECIPE_REGISTRY = {
|
|
|
1938
2368
|
description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
|
|
1939
2369
|
class: AgentHooksRecipe,
|
|
1940
2370
|
commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
|
|
2371
|
+
},
|
|
2372
|
+
"mise-op-inject": {
|
|
2373
|
+
name: "mise-op-inject",
|
|
2374
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2375
|
+
class: MiseOpInjectRecipe,
|
|
2376
|
+
commands: ["WireMiseOpInject"]
|
|
1941
2377
|
}
|
|
1942
2378
|
};
|
|
1943
2379
|
var COMMAND_REGISTRY = {
|
|
@@ -2006,6 +2442,12 @@ var COMMAND_REGISTRY = {
|
|
|
2006
2442
|
description: "Create .env.example file",
|
|
2007
2443
|
group: "environment",
|
|
2008
2444
|
class: AddDotenv
|
|
2445
|
+
},
|
|
2446
|
+
WireMiseOpInject: {
|
|
2447
|
+
name: "WireMiseOpInject",
|
|
2448
|
+
description: "Wire up op-inject script to mise.toml for 1Password secret resolution",
|
|
2449
|
+
group: "mise",
|
|
2450
|
+
class: WireMiseOpInject
|
|
2009
2451
|
}
|
|
2010
2452
|
};
|
|
2011
2453
|
function getRecipeNames() {
|
|
@@ -2021,15 +2463,15 @@ function createRecipe(name, context) {
|
|
|
2021
2463
|
}
|
|
2022
2464
|
|
|
2023
2465
|
// src/utils/version.ts
|
|
2024
|
-
import { readFileSync as
|
|
2025
|
-
import { dirname as dirname6, join as
|
|
2466
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2467
|
+
import { dirname as dirname6, join as join13 } from "node:path";
|
|
2026
2468
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2027
2469
|
var PJANGLER_VERSION = (() => {
|
|
2028
2470
|
try {
|
|
2029
2471
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2030
2472
|
for (let i = 0; i < 4; i++) {
|
|
2031
2473
|
try {
|
|
2032
|
-
const raw =
|
|
2474
|
+
const raw = readFileSync6(join13(dir, "package.json"), "utf8");
|
|
2033
2475
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
2034
2476
|
} catch {
|
|
2035
2477
|
const parent = dirname6(dir);
|
|
@@ -2043,29 +2485,39 @@ var PJANGLER_VERSION = (() => {
|
|
|
2043
2485
|
})();
|
|
2044
2486
|
|
|
2045
2487
|
// src/parity/index.ts
|
|
2046
|
-
import { existsSync as
|
|
2047
|
-
import { basename as basename3, dirname as dirname7, join as
|
|
2488
|
+
import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync6, readFileSync as readFileSync7, readlinkSync, readdirSync as readdirSync2, renameSync as renameSync2, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7, chmodSync as chmodSync4, copyFileSync, rmSync } from "node:fs";
|
|
2489
|
+
import { basename as basename3, dirname as dirname7, join as join14, relative, resolve as resolve2 } from "node:path";
|
|
2048
2490
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2049
2491
|
import { homedir as homedir5 } from "node:os";
|
|
2050
2492
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
2051
|
-
import
|
|
2493
|
+
import YAML3 from "yaml";
|
|
2052
2494
|
var LINK_AGENTFILES_SCRIPT = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'";
|
|
2053
2495
|
var OP_INJECT_SCRIPT = "op inject -i .env.op > .env";
|
|
2496
|
+
var SYNC_SKILLS_SCRIPT = "sync-skills.py --scope project";
|
|
2054
2497
|
var CODEGRAPH_SCRIPT = "[ -f '{{config_root}}/.mise/scripts/codegraph.sh' ] && '{{config_root}}/.mise/scripts/codegraph.sh' || true";
|
|
2498
|
+
var SKILLS_REGISTRY_URL = "https://github.com/delorenj/skillex.git";
|
|
2055
2499
|
var HOOKS_COMMENT_HEADER = `# This block will handle the linking of
|
|
2056
2500
|
# agent files to the main AGENTS.md file.
|
|
2057
2501
|
#
|
|
2058
2502
|
# TODO: Ensure this works for all levels of nesting.
|
|
2059
2503
|
# i.e. All linked agent files MUST be siblings at
|
|
2060
2504
|
# any given level of nesting.`;
|
|
2061
|
-
var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT];
|
|
2505
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [LINK_AGENTFILES_SCRIPT, OP_INJECT_SCRIPT, SYNC_SKILLS_SCRIPT];
|
|
2062
2506
|
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
2063
2507
|
patterns = ["AGENTS.md"]
|
|
2064
2508
|
task = "link-agentfiles"
|
|
2065
2509
|
|
|
2510
|
+
[[watch_files]]
|
|
2511
|
+
patterns = [".agents/skills.json"]
|
|
2512
|
+
task = "skills-sync"
|
|
2513
|
+
|
|
2066
2514
|
[tasks.link-agentfiles]
|
|
2067
2515
|
description = "Symlink all agent files to AGENTS.md"
|
|
2068
|
-
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2516
|
+
run = "'{{config_root}}/.mise/scripts/link-agentfiles.sh'"
|
|
2517
|
+
|
|
2518
|
+
[tasks.skills-sync]
|
|
2519
|
+
description = "Sync skills from manifest to local CLI dirs"
|
|
2520
|
+
run = "sync-skills.py --scope project"`;
|
|
2069
2521
|
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
2070
2522
|
[tasks."version"]
|
|
2071
2523
|
description = "Print the current version (vX.Y.Z)"
|
|
@@ -2095,7 +2547,7 @@ run = "'{{config_root}}/.mise/scripts/versioning.sh' sync"
|
|
|
2095
2547
|
function resolvePjanglerRoot2() {
|
|
2096
2548
|
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2097
2549
|
while (dir !== dirname7(dir)) {
|
|
2098
|
-
if (
|
|
2550
|
+
if (existsSync10(join14(dir, "package.json")) && existsSync10(join14(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2099
2551
|
return dir;
|
|
2100
2552
|
}
|
|
2101
2553
|
dir = dirname7(dir);
|
|
@@ -2106,17 +2558,17 @@ function normalizeNewlines(value) {
|
|
|
2106
2558
|
return value.replace(/\r\n/g, "\n");
|
|
2107
2559
|
}
|
|
2108
2560
|
function readText(path) {
|
|
2109
|
-
return normalizeNewlines(
|
|
2561
|
+
return normalizeNewlines(readFileSync7(path, "utf8"));
|
|
2110
2562
|
}
|
|
2111
2563
|
function safeReadText(path) {
|
|
2112
|
-
return
|
|
2564
|
+
return existsSync10(path) ? readText(path) : null;
|
|
2113
2565
|
}
|
|
2114
2566
|
function ensureParent(path) {
|
|
2115
2567
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2116
2568
|
}
|
|
2117
2569
|
function writeText(path, content) {
|
|
2118
2570
|
ensureParent(path);
|
|
2119
|
-
|
|
2571
|
+
writeFileSync7(path, content);
|
|
2120
2572
|
}
|
|
2121
2573
|
function tryParseJson(text2) {
|
|
2122
2574
|
if (!text2) return null;
|
|
@@ -2133,7 +2585,7 @@ function titleCaseSlug(slug) {
|
|
|
2133
2585
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
2134
2586
|
}
|
|
2135
2587
|
function readSymlinkTarget(path) {
|
|
2136
|
-
if (!
|
|
2588
|
+
if (!existsSync10(path)) return null;
|
|
2137
2589
|
try {
|
|
2138
2590
|
return readlinkSync(path);
|
|
2139
2591
|
} catch {
|
|
@@ -2141,7 +2593,7 @@ function readSymlinkTarget(path) {
|
|
|
2141
2593
|
}
|
|
2142
2594
|
}
|
|
2143
2595
|
function ensureSymlink(path, target, dryRun) {
|
|
2144
|
-
if (
|
|
2596
|
+
if (existsSync10(path)) {
|
|
2145
2597
|
const stat = lstatSync(path);
|
|
2146
2598
|
if (stat.isSymbolicLink()) {
|
|
2147
2599
|
const current = readSymlinkTarget(path);
|
|
@@ -2158,11 +2610,11 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
2158
2610
|
return { changed: true };
|
|
2159
2611
|
}
|
|
2160
2612
|
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
2161
|
-
const agentsPath =
|
|
2162
|
-
if (
|
|
2613
|
+
const agentsPath = join14(repoRoot, "AGENTS.md");
|
|
2614
|
+
if (existsSync10(agentsPath)) return { changedFiles: [], details: [] };
|
|
2163
2615
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2164
|
-
const source =
|
|
2165
|
-
if (!
|
|
2616
|
+
const source = join14(repoRoot, file);
|
|
2617
|
+
if (!existsSync10(source)) continue;
|
|
2166
2618
|
const stat = lstatSync(source);
|
|
2167
2619
|
if (stat.isSymbolicLink()) continue;
|
|
2168
2620
|
if (stat.isFile()) {
|
|
@@ -2171,8 +2623,8 @@ function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
|
2171
2623
|
}
|
|
2172
2624
|
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
2173
2625
|
}
|
|
2174
|
-
const readmePath =
|
|
2175
|
-
if (
|
|
2626
|
+
const readmePath = join14(repoRoot, "README.md");
|
|
2627
|
+
if (existsSync10(readmePath)) {
|
|
2176
2628
|
const stat = lstatSync(readmePath);
|
|
2177
2629
|
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
2178
2630
|
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
@@ -2211,12 +2663,12 @@ function yamlGet(text2, keyPath) {
|
|
|
2211
2663
|
return "";
|
|
2212
2664
|
}
|
|
2213
2665
|
function discoverRoles(repoRoot) {
|
|
2214
|
-
const rolesDir =
|
|
2215
|
-
if (!
|
|
2666
|
+
const rolesDir = join14(repoRoot, "agents", "hermes");
|
|
2667
|
+
if (!existsSync10(rolesDir)) return [];
|
|
2216
2668
|
return readdirSync2(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
2217
|
-
const roleDir =
|
|
2218
|
-
const roleYamlPath =
|
|
2219
|
-
if (!
|
|
2669
|
+
const roleDir = join14(rolesDir, entry.name);
|
|
2670
|
+
const roleYamlPath = join14(roleDir, "role.yaml");
|
|
2671
|
+
if (!existsSync10(roleYamlPath)) return null;
|
|
2220
2672
|
const text2 = readText(roleYamlPath);
|
|
2221
2673
|
const runtimeRepoRaw = yamlGet(text2, "runtime.github_repo");
|
|
2222
2674
|
return {
|
|
@@ -2244,7 +2696,7 @@ function discoverRoles(repoRoot) {
|
|
|
2244
2696
|
}).filter((value) => Boolean(value));
|
|
2245
2697
|
}
|
|
2246
2698
|
function registryPath(homeDir) {
|
|
2247
|
-
return
|
|
2699
|
+
return join14(homeDir, ".hermes", "agents-registry.yaml");
|
|
2248
2700
|
}
|
|
2249
2701
|
function systemctlUser(args) {
|
|
2250
2702
|
const result = spawnSync6("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -2255,8 +2707,8 @@ function systemctlUser(args) {
|
|
|
2255
2707
|
};
|
|
2256
2708
|
}
|
|
2257
2709
|
function templateScript(ctx, name) {
|
|
2258
|
-
const source =
|
|
2259
|
-
return
|
|
2710
|
+
const source = join14(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
2711
|
+
return existsSync10(source) ? readText(source) : void 0;
|
|
2260
2712
|
}
|
|
2261
2713
|
function templateVersioningScript(ctx) {
|
|
2262
2714
|
return templateScript(ctx, "versioning.sh");
|
|
@@ -2268,8 +2720,8 @@ function resolveAgentHooksLayer2(ctx) {
|
|
|
2268
2720
|
const override = process.env.PJ_AGENT_HOOKS_LAYER;
|
|
2269
2721
|
if (override === "0" || override === "false") return false;
|
|
2270
2722
|
if (override === "1" || override === "true") return true;
|
|
2271
|
-
if (
|
|
2272
|
-
return !
|
|
2723
|
+
if (existsSync10(join14(ctx.repoRoot, ".agents", "hooks", "sync.py"))) return true;
|
|
2724
|
+
return !existsSync10(join14(ctx.homeDir, ".agents", "hooks"));
|
|
2273
2725
|
}
|
|
2274
2726
|
function evaluateMiseConditionals(template, agentHooksLayer) {
|
|
2275
2727
|
const out = [];
|
|
@@ -2299,19 +2751,36 @@ function renderGeneratedProjectMiseToml(ctx, template) {
|
|
|
2299
2751
|
return evaluateMiseConditionals(template, resolveAgentHooksLayer2(ctx)).replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
2300
2752
|
}
|
|
2301
2753
|
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
2302
|
-
const targetPath =
|
|
2303
|
-
if (
|
|
2304
|
-
const sourcePath =
|
|
2305
|
-
if (!
|
|
2754
|
+
const targetPath = join14(ctx.repoRoot, "mise.toml");
|
|
2755
|
+
if (existsSync10(targetPath)) return false;
|
|
2756
|
+
const sourcePath = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
2757
|
+
if (!existsSync10(sourcePath)) return false;
|
|
2306
2758
|
changedFiles.push(targetPath);
|
|
2307
2759
|
if (!ctx.dryRun) {
|
|
2308
2760
|
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
2309
2761
|
}
|
|
2310
2762
|
return true;
|
|
2311
2763
|
}
|
|
2764
|
+
function templateCommonProjectText(ctx, rel) {
|
|
2765
|
+
const path = join14(ctx.pjanglerRoot, "templates", "commonproject", "template", rel);
|
|
2766
|
+
return existsSync10(path) ? readText(path) : void 0;
|
|
2767
|
+
}
|
|
2768
|
+
function canonicalSkillsManifest() {
|
|
2769
|
+
return `${JSON.stringify(
|
|
2770
|
+
{
|
|
2771
|
+
$schema: "https://raw.githubusercontent.com/skillex/schemas/main/skills.schema.json",
|
|
2772
|
+
inherit_global: true,
|
|
2773
|
+
registry: SKILLS_REGISTRY_URL,
|
|
2774
|
+
skills: []
|
|
2775
|
+
},
|
|
2776
|
+
null,
|
|
2777
|
+
2
|
|
2778
|
+
)}
|
|
2779
|
+
`;
|
|
2780
|
+
}
|
|
2312
2781
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
2313
|
-
const packageJson =
|
|
2314
|
-
return
|
|
2782
|
+
const packageJson = join14(repoRoot, "package.json");
|
|
2783
|
+
return existsSync10(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
2315
2784
|
}
|
|
2316
2785
|
function replaceOrAppendManagedBlock(text2, startMarker, block, beforePattern) {
|
|
2317
2786
|
if (startMarker.test(text2)) {
|
|
@@ -2335,7 +2804,7 @@ var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/herm
|
|
|
2335
2804
|
function requiredMisePathEntries(ctx) {
|
|
2336
2805
|
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
2337
2806
|
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
2338
|
-
if (
|
|
2807
|
+
if (existsSync10(join14(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
2339
2808
|
}
|
|
2340
2809
|
return required;
|
|
2341
2810
|
}
|
|
@@ -2436,6 +2905,9 @@ function stripTomlStringsAndComments(line) {
|
|
|
2436
2905
|
function isManagedHookEntry(value) {
|
|
2437
2906
|
const trimmed = value.trim();
|
|
2438
2907
|
if (trimmed === OP_INJECT_SCRIPT) return true;
|
|
2908
|
+
if (trimmed === SYNC_SKILLS_SCRIPT) return true;
|
|
2909
|
+
if (/link-project-skills-to-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2910
|
+
if (/unlink-project-skills-from-clis\.sh'?\s*$/.test(trimmed)) return true;
|
|
2439
2911
|
return /link-agentfiles\.sh'?\s*$/.test(trimmed);
|
|
2440
2912
|
}
|
|
2441
2913
|
function normalizeHookScript(script) {
|
|
@@ -2538,12 +3010,17 @@ function upsertLinkAgentfilesHooks(text2) {
|
|
|
2538
3010
|
function upsertLinkAgentfilesBlock(text2, ctx) {
|
|
2539
3011
|
const withPath = upsertMisePath(text2, requiredMisePathEntries(ctx));
|
|
2540
3012
|
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
3013
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-sync\]$/, void 0, { includePrecedingComments: false });
|
|
3014
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.link-project-skills-to-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3015
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.unlink-project-skills-from-clis\]$/, void 0, { includePrecedingComments: false });
|
|
3016
|
+
cleaned = removeTomlSection(cleaned, /^\[tasks\.skills-relink\]$/, void 0, { includePrecedingComments: false });
|
|
2541
3017
|
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
3018
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /\.agents\/skills\.json/, { includePrecedingComments: false });
|
|
2542
3019
|
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
2543
3020
|
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
2544
3021
|
}
|
|
2545
3022
|
function readProjectJson(ctx) {
|
|
2546
|
-
return tryParseJson(safeReadText(
|
|
3023
|
+
return tryParseJson(safeReadText(join14(ctx.repoRoot, ".project.json")));
|
|
2547
3024
|
}
|
|
2548
3025
|
function boolSetting(value, fallback) {
|
|
2549
3026
|
if (typeof value === "boolean") return value;
|
|
@@ -2618,12 +3095,12 @@ function canonicalProjectJson(ctx) {
|
|
|
2618
3095
|
};
|
|
2619
3096
|
}
|
|
2620
3097
|
function projectJsonFinding(ctx) {
|
|
2621
|
-
const projectPath =
|
|
2622
|
-
const planeJsonPath =
|
|
3098
|
+
const projectPath = join14(ctx.repoRoot, ".project.json");
|
|
3099
|
+
const planeJsonPath = join14(ctx.repoRoot, ".plane.json");
|
|
2623
3100
|
const details = [];
|
|
2624
3101
|
const data = readProjectJson(ctx);
|
|
2625
3102
|
const roles = discoverRoles(ctx.repoRoot);
|
|
2626
|
-
if (!
|
|
3103
|
+
if (!existsSync10(projectPath)) {
|
|
2627
3104
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
2628
3105
|
}
|
|
2629
3106
|
if (!data) {
|
|
@@ -2658,7 +3135,7 @@ function projectJsonFinding(ctx) {
|
|
|
2658
3135
|
for (const key of ["enabled", "grace_hours", "auto_review"]) {
|
|
2659
3136
|
if (!(key in reconcile)) details.push(`automation.reconcile.${key} missing`);
|
|
2660
3137
|
}
|
|
2661
|
-
if (
|
|
3138
|
+
if (existsSync10(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
2662
3139
|
return {
|
|
2663
3140
|
id: "sot.project-json",
|
|
2664
3141
|
title: "Canonical .project.json",
|
|
@@ -2739,17 +3216,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
2739
3216
|
`.replace(/\u0010/g, "$");
|
|
2740
3217
|
}
|
|
2741
3218
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
2742
|
-
if (!
|
|
3219
|
+
if (!existsSync10(sourceDir)) return;
|
|
2743
3220
|
mkdirSync6(targetDir, { recursive: true });
|
|
2744
3221
|
for (const entry of readdirSync2(sourceDir, { withFileTypes: true })) {
|
|
2745
|
-
const sourcePath =
|
|
3222
|
+
const sourcePath = join14(sourceDir, entry.name);
|
|
2746
3223
|
if (skip?.(sourcePath)) continue;
|
|
2747
|
-
const targetPath =
|
|
3224
|
+
const targetPath = join14(targetDir, entry.name);
|
|
2748
3225
|
if (entry.isDirectory()) {
|
|
2749
3226
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
2750
3227
|
continue;
|
|
2751
3228
|
}
|
|
2752
|
-
if (
|
|
3229
|
+
if (existsSync10(targetPath)) continue;
|
|
2753
3230
|
changedFiles.push(targetPath);
|
|
2754
3231
|
if (!dryRun) {
|
|
2755
3232
|
ensureParent(targetPath);
|
|
@@ -2758,7 +3235,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
2758
3235
|
}
|
|
2759
3236
|
}
|
|
2760
3237
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
2761
|
-
const gitmodulesPath =
|
|
3238
|
+
const gitmodulesPath = join14(repoRoot, ".gitmodules");
|
|
2762
3239
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
2763
3240
|
const owner = role.runtimeOwner || "delorenj";
|
|
2764
3241
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -2925,10 +3402,10 @@ function runBmadInstall(repoRoot) {
|
|
|
2925
3402
|
return { ok: true };
|
|
2926
3403
|
}
|
|
2927
3404
|
function readInstalledBmadVersion(repoRoot) {
|
|
2928
|
-
const raw = safeReadText(
|
|
3405
|
+
const raw = safeReadText(join14(repoRoot, "_bmad", "_config", "manifest.yaml"));
|
|
2929
3406
|
if (!raw) return void 0;
|
|
2930
3407
|
try {
|
|
2931
|
-
const parsed =
|
|
3408
|
+
const parsed = YAML3.parse(raw);
|
|
2932
3409
|
const version = parsed?.installation?.version;
|
|
2933
3410
|
return typeof version === "string" && version.trim() ? version.trim() : void 0;
|
|
2934
3411
|
} catch {
|
|
@@ -2936,8 +3413,8 @@ function readInstalledBmadVersion(repoRoot) {
|
|
|
2936
3413
|
}
|
|
2937
3414
|
}
|
|
2938
3415
|
function bmadCachePath(homeDir) {
|
|
2939
|
-
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() ||
|
|
2940
|
-
return
|
|
3416
|
+
const cacheRoot = process.env.XDG_CACHE_HOME?.trim() || join14(homeDir, ".cache");
|
|
3417
|
+
return join14(cacheRoot, "pjangler", "bmad-dist-tags.json");
|
|
2941
3418
|
}
|
|
2942
3419
|
function readBmadDistTagsCache(homeDir) {
|
|
2943
3420
|
const raw = safeReadText(bmadCachePath(homeDir));
|
|
@@ -2980,7 +3457,7 @@ function resolveBmadDistTags(homeDir) {
|
|
|
2980
3457
|
try {
|
|
2981
3458
|
const path = bmadCachePath(homeDir);
|
|
2982
3459
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
2983
|
-
|
|
3460
|
+
writeFileSync7(path, JSON.stringify({ fetchedAt: Date.now(), distTags: fetched }, null, 2));
|
|
2984
3461
|
} catch {
|
|
2985
3462
|
}
|
|
2986
3463
|
return { distTags: fetched, stale: false };
|
|
@@ -3025,14 +3502,14 @@ var RULES = [
|
|
|
3025
3502
|
id: "mise.config-root",
|
|
3026
3503
|
title: "mise config_root + AGENTS link hooks",
|
|
3027
3504
|
audit: (ctx) => {
|
|
3028
|
-
const misePath =
|
|
3029
|
-
if (!
|
|
3505
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3506
|
+
if (!existsSync10(misePath)) {
|
|
3030
3507
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
3031
3508
|
}
|
|
3032
3509
|
const text2 = readText(misePath);
|
|
3033
3510
|
const details = [];
|
|
3034
|
-
const linkAgentfilesPath =
|
|
3035
|
-
if (!
|
|
3511
|
+
const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3512
|
+
if (!existsSync10(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
3036
3513
|
const pathValues = [...(text2.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
3037
3514
|
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
3038
3515
|
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
@@ -3050,10 +3527,10 @@ var RULES = [
|
|
|
3050
3527
|
};
|
|
3051
3528
|
},
|
|
3052
3529
|
migrate: (ctx, finding) => {
|
|
3053
|
-
const path =
|
|
3530
|
+
const path = join14(ctx.repoRoot, "mise.toml");
|
|
3054
3531
|
const changedFiles = [];
|
|
3055
3532
|
const details = [];
|
|
3056
|
-
if (!
|
|
3533
|
+
if (!existsSync10(path)) {
|
|
3057
3534
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3058
3535
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
3059
3536
|
}
|
|
@@ -3069,7 +3546,7 @@ var RULES = [
|
|
|
3069
3546
|
if (!ctx.dryRun) writeText(path, next);
|
|
3070
3547
|
text2 = next;
|
|
3071
3548
|
}
|
|
3072
|
-
const linkAgentfilesPath =
|
|
3549
|
+
const linkAgentfilesPath = join14(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
3073
3550
|
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
3074
3551
|
if (expectedScript === void 0) {
|
|
3075
3552
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/link-agentfiles.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -3078,7 +3555,7 @@ var RULES = [
|
|
|
3078
3555
|
changedFiles.push(linkAgentfilesPath);
|
|
3079
3556
|
if (!ctx.dryRun) {
|
|
3080
3557
|
writeText(linkAgentfilesPath, expectedScript);
|
|
3081
|
-
|
|
3558
|
+
chmodSync4(linkAgentfilesPath, 493);
|
|
3082
3559
|
}
|
|
3083
3560
|
}
|
|
3084
3561
|
return {
|
|
@@ -3096,13 +3573,13 @@ var RULES = [
|
|
|
3096
3573
|
title: "managed mise versioning block",
|
|
3097
3574
|
audit: (ctx) => {
|
|
3098
3575
|
const details = [];
|
|
3099
|
-
const misePath =
|
|
3100
|
-
const versioningPath =
|
|
3101
|
-
const manifestPath =
|
|
3576
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3577
|
+
const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3578
|
+
const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3102
3579
|
const text2 = safeReadText(misePath);
|
|
3103
3580
|
if (!text2?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
3104
|
-
if (!
|
|
3105
|
-
if (!
|
|
3581
|
+
if (!existsSync10(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
3582
|
+
if (!existsSync10(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
3106
3583
|
return {
|
|
3107
3584
|
id: "mise.versioning",
|
|
3108
3585
|
title: "managed mise versioning block",
|
|
@@ -3115,8 +3592,8 @@ var RULES = [
|
|
|
3115
3592
|
migrate: (ctx, finding) => {
|
|
3116
3593
|
const changedFiles = [];
|
|
3117
3594
|
const details = [];
|
|
3118
|
-
const misePath =
|
|
3119
|
-
if (!
|
|
3595
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3596
|
+
if (!existsSync10(misePath)) {
|
|
3120
3597
|
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3121
3598
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
3122
3599
|
}
|
|
@@ -3140,7 +3617,7 @@ var RULES = [
|
|
|
3140
3617
|
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3141
3618
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3142
3619
|
}
|
|
3143
|
-
const versioningPath =
|
|
3620
|
+
const versioningPath = join14(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
3144
3621
|
const expectedScript = templateVersioningScript(ctx);
|
|
3145
3622
|
if (expectedScript === void 0) {
|
|
3146
3623
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "pjangler install is missing .mise/scripts/versioning.sh \u2014 update @delorenj/pjangler (broken package)", changedFiles, details: [] };
|
|
@@ -3149,10 +3626,10 @@ var RULES = [
|
|
|
3149
3626
|
changedFiles.push(versioningPath);
|
|
3150
3627
|
if (!ctx.dryRun) {
|
|
3151
3628
|
writeText(versioningPath, expectedScript);
|
|
3152
|
-
|
|
3629
|
+
chmodSync4(versioningPath, 493);
|
|
3153
3630
|
}
|
|
3154
3631
|
}
|
|
3155
|
-
const manifestPath =
|
|
3632
|
+
const manifestPath = join14(ctx.repoRoot, ".mise", "version-files.conf");
|
|
3156
3633
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
3157
3634
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3158
3635
|
changedFiles.push(manifestPath);
|
|
@@ -3168,13 +3645,125 @@ var RULES = [
|
|
|
3168
3645
|
};
|
|
3169
3646
|
}
|
|
3170
3647
|
},
|
|
3648
|
+
{
|
|
3649
|
+
id: "skills.project-manifest",
|
|
3650
|
+
title: "Skillex project skills manifest",
|
|
3651
|
+
audit: (ctx) => {
|
|
3652
|
+
const details = [];
|
|
3653
|
+
const manifestPath = join14(ctx.repoRoot, ".agents", "skills.json");
|
|
3654
|
+
const legacyDir = join14(ctx.repoRoot, ".agents", "skills");
|
|
3655
|
+
const localExamplePath = join14(ctx.repoRoot, ".agents", "local.example.json");
|
|
3656
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3657
|
+
let fixable = true;
|
|
3658
|
+
const manifest = tryParseJson(safeReadText(manifestPath));
|
|
3659
|
+
if (!manifest) {
|
|
3660
|
+
details.push(".agents/skills.json missing or invalid JSON");
|
|
3661
|
+
} else {
|
|
3662
|
+
if (manifest.inherit_global !== true) details.push(".agents/skills.json should set inherit_global: true");
|
|
3663
|
+
if (manifest.registry !== SKILLS_REGISTRY_URL) details.push(`.agents/skills.json should set registry to ${SKILLS_REGISTRY_URL}`);
|
|
3664
|
+
if (!Array.isArray(manifest.skills)) details.push(".agents/skills.json should define a skills array");
|
|
3665
|
+
}
|
|
3666
|
+
if (existsSync10(legacyDir)) {
|
|
3667
|
+
const entries = readdirSync2(legacyDir);
|
|
3668
|
+
if (entries.length > 0) {
|
|
3669
|
+
details.push(".agents/skills/ still contains legacy committed skills; map them into .agents/skills.json before migrating");
|
|
3670
|
+
fixable = false;
|
|
3671
|
+
} else {
|
|
3672
|
+
details.push(".agents/skills/ legacy directory should be removed");
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3676
|
+
if (existsSync10(join14(ctx.repoRoot, rel))) details.push(`${rel} is a legacy symlink-era script and should be removed`);
|
|
3677
|
+
}
|
|
3678
|
+
const localExample = tryParseJson(safeReadText(localExamplePath));
|
|
3679
|
+
if (localExample && Object.prototype.hasOwnProperty.call(localExample, "skills")) {
|
|
3680
|
+
details.push(".agents/local.example.json still documents legacy skills overrides; drop the skills section");
|
|
3681
|
+
}
|
|
3682
|
+
const mise = safeReadText(misePath);
|
|
3683
|
+
if (!mise?.includes(SYNC_SKILLS_SCRIPT)) details.push("mise.toml should run sync-skills.py --scope project on enter");
|
|
3684
|
+
if (!mise?.includes('patterns = [".agents/skills.json"]')) details.push("mise.toml should watch .agents/skills.json");
|
|
3685
|
+
if (!mise?.includes("[tasks.skills-sync]")) details.push("mise.toml should define a skills-sync task");
|
|
3686
|
+
if (mise?.includes("link-project-skills-to-clis.sh") || mise?.includes("unlink-project-skills-from-clis.sh") || mise?.includes("[tasks.skills-relink]")) {
|
|
3687
|
+
details.push("mise.toml still contains legacy skill-link wiring");
|
|
3688
|
+
}
|
|
3689
|
+
return {
|
|
3690
|
+
id: "skills.project-manifest",
|
|
3691
|
+
title: "Skillex project skills manifest",
|
|
3692
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
3693
|
+
summary: details.length === 0 ? "Skillex skills manifest parity verified" : `${details.length} Skillex migration issue(s) detected`,
|
|
3694
|
+
details,
|
|
3695
|
+
fixable
|
|
3696
|
+
};
|
|
3697
|
+
},
|
|
3698
|
+
migrate: (ctx, finding) => {
|
|
3699
|
+
const changedFiles = [];
|
|
3700
|
+
const details = [];
|
|
3701
|
+
const manifestPath = join14(ctx.repoRoot, ".agents", "skills.json");
|
|
3702
|
+
const legacyDir = join14(ctx.repoRoot, ".agents", "skills");
|
|
3703
|
+
const localExamplePath = join14(ctx.repoRoot, ".agents", "local.example.json");
|
|
3704
|
+
const misePath = join14(ctx.repoRoot, "mise.toml");
|
|
3705
|
+
if (existsSync10(legacyDir)) {
|
|
3706
|
+
const entries = readdirSync2(legacyDir);
|
|
3707
|
+
if (entries.length > 0) {
|
|
3708
|
+
return {
|
|
3709
|
+
id: finding.id,
|
|
3710
|
+
title: finding.title,
|
|
3711
|
+
status: "blocked",
|
|
3712
|
+
summary: "Legacy .agents/skills/ still contains committed skills; migrate them into .agents/skills.json manually first",
|
|
3713
|
+
changedFiles,
|
|
3714
|
+
details: entries.map((entry) => `.agents/skills/${entry}`)
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
changedFiles.push(legacyDir);
|
|
3718
|
+
if (!ctx.dryRun) rmSync(legacyDir, { recursive: true, force: true });
|
|
3719
|
+
}
|
|
3720
|
+
const expectedManifest = canonicalSkillsManifest();
|
|
3721
|
+
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
3722
|
+
changedFiles.push(manifestPath);
|
|
3723
|
+
if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
|
|
3724
|
+
details.push("Wrote canonical .agents/skills.json manifest");
|
|
3725
|
+
}
|
|
3726
|
+
for (const rel of [".mise/scripts/link-project-skills-to-clis.sh", ".mise/scripts/unlink-project-skills-from-clis.sh"]) {
|
|
3727
|
+
const path = join14(ctx.repoRoot, rel);
|
|
3728
|
+
if (existsSync10(path)) {
|
|
3729
|
+
changedFiles.push(path);
|
|
3730
|
+
if (!ctx.dryRun) unlinkSync3(path);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
const templateLocalExample = templateCommonProjectText(ctx, ".agents/local.example.json");
|
|
3734
|
+
const currentLocalExample = safeReadText(localExamplePath);
|
|
3735
|
+
if (templateLocalExample && currentLocalExample && currentLocalExample !== templateLocalExample) {
|
|
3736
|
+
changedFiles.push(localExamplePath);
|
|
3737
|
+
if (!ctx.dryRun) writeText(localExamplePath, templateLocalExample);
|
|
3738
|
+
}
|
|
3739
|
+
if (!existsSync10(misePath)) {
|
|
3740
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
3741
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details };
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
const currentMise = readText(misePath);
|
|
3745
|
+
const nextMise = upsertLinkAgentfilesBlock(currentMise, ctx);
|
|
3746
|
+
if (nextMise !== currentMise) {
|
|
3747
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
3748
|
+
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
3749
|
+
}
|
|
3750
|
+
return {
|
|
3751
|
+
id: finding.id,
|
|
3752
|
+
title: finding.title,
|
|
3753
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
3754
|
+
summary: changedFiles.length ? "Skillex skills manifest contract normalized" : "No changes required",
|
|
3755
|
+
changedFiles,
|
|
3756
|
+
details
|
|
3757
|
+
};
|
|
3758
|
+
}
|
|
3759
|
+
},
|
|
3171
3760
|
{
|
|
3172
3761
|
id: "sot.agent-symlinks",
|
|
3173
3762
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
3174
3763
|
audit: (ctx) => {
|
|
3175
|
-
const agentsPath =
|
|
3176
|
-
if (!
|
|
3177
|
-
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) =>
|
|
3764
|
+
const agentsPath = join14(ctx.repoRoot, "AGENTS.md");
|
|
3765
|
+
if (!existsSync10(agentsPath)) {
|
|
3766
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync10(join14(ctx.repoRoot, file)));
|
|
3178
3767
|
if (fallbackSources.length === 0) {
|
|
3179
3768
|
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
3180
3769
|
}
|
|
@@ -3189,7 +3778,7 @@ var RULES = [
|
|
|
3189
3778
|
}
|
|
3190
3779
|
const details = [];
|
|
3191
3780
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3192
|
-
const full =
|
|
3781
|
+
const full = join14(ctx.repoRoot, file);
|
|
3193
3782
|
const target = readSymlinkTarget(full);
|
|
3194
3783
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
3195
3784
|
}
|
|
@@ -3213,7 +3802,7 @@ var RULES = [
|
|
|
3213
3802
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
3214
3803
|
}
|
|
3215
3804
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
3216
|
-
const full =
|
|
3805
|
+
const full = join14(ctx.repoRoot, file);
|
|
3217
3806
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
3218
3807
|
if (result.blocked) blockedDetails.push(result.blocked);
|
|
3219
3808
|
if (result.changed) changedFiles.push(full);
|
|
@@ -3235,7 +3824,7 @@ var RULES = [
|
|
|
3235
3824
|
migrate: (ctx, finding) => {
|
|
3236
3825
|
const changedFiles = [];
|
|
3237
3826
|
const details = [];
|
|
3238
|
-
const path =
|
|
3827
|
+
const path = join14(ctx.repoRoot, ".project.json");
|
|
3239
3828
|
const existing = readProjectJson(ctx) ?? {};
|
|
3240
3829
|
const canonical = canonicalProjectJson(ctx);
|
|
3241
3830
|
const merged = { ...existing, ...canonical };
|
|
@@ -3245,10 +3834,10 @@ var RULES = [
|
|
|
3245
3834
|
changedFiles.push(path);
|
|
3246
3835
|
if (!ctx.dryRun) writeText(path, expected);
|
|
3247
3836
|
}
|
|
3248
|
-
const planeJson =
|
|
3249
|
-
if (
|
|
3837
|
+
const planeJson = join14(ctx.repoRoot, ".plane.json");
|
|
3838
|
+
if (existsSync10(planeJson)) {
|
|
3250
3839
|
const backup = `${planeJson}.migrated-backup`;
|
|
3251
|
-
if (
|
|
3840
|
+
if (existsSync10(backup)) {
|
|
3252
3841
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
3253
3842
|
} else {
|
|
3254
3843
|
changedFiles.push(backup);
|
|
@@ -3270,8 +3859,8 @@ var RULES = [
|
|
|
3270
3859
|
title: ".env.op + gitignore secrets contract",
|
|
3271
3860
|
audit: (ctx) => {
|
|
3272
3861
|
const details = [];
|
|
3273
|
-
const envOp = safeReadText(
|
|
3274
|
-
const gitignore = safeReadText(
|
|
3862
|
+
const envOp = safeReadText(join14(ctx.repoRoot, ".env.op"));
|
|
3863
|
+
const gitignore = safeReadText(join14(ctx.repoRoot, ".gitignore"));
|
|
3275
3864
|
if (!envOp) {
|
|
3276
3865
|
details.push(".env.op missing");
|
|
3277
3866
|
} else {
|
|
@@ -3297,12 +3886,12 @@ var RULES = [
|
|
|
3297
3886
|
migrate: (ctx, finding) => {
|
|
3298
3887
|
const changedFiles = [];
|
|
3299
3888
|
const details = [];
|
|
3300
|
-
const envOpPath =
|
|
3301
|
-
if (!
|
|
3889
|
+
const envOpPath = join14(ctx.repoRoot, ".env.op");
|
|
3890
|
+
if (!existsSync10(envOpPath)) {
|
|
3302
3891
|
changedFiles.push(envOpPath);
|
|
3303
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
3892
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join14(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
3304
3893
|
}
|
|
3305
|
-
const gitignorePath =
|
|
3894
|
+
const gitignorePath = join14(ctx.repoRoot, ".gitignore");
|
|
3306
3895
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
3307
3896
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
3308
3897
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -3329,7 +3918,7 @@ var RULES = [
|
|
|
3329
3918
|
title: ".copier-answers.yml provenance + drift report",
|
|
3330
3919
|
audit: (ctx) => {
|
|
3331
3920
|
const details = [];
|
|
3332
|
-
const path =
|
|
3921
|
+
const path = join14(ctx.repoRoot, ".copier-answers.yml");
|
|
3333
3922
|
const text2 = safeReadText(path);
|
|
3334
3923
|
const project = readProjectJson(ctx);
|
|
3335
3924
|
if (!text2) {
|
|
@@ -3360,12 +3949,12 @@ var RULES = [
|
|
|
3360
3949
|
const changedFiles = [];
|
|
3361
3950
|
const project = canonicalProjectJson(ctx);
|
|
3362
3951
|
const text2 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
3363
|
-
_src_path: ${
|
|
3952
|
+
_src_path: ${join14(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
3364
3953
|
project_description: ${String(project.project_description)}
|
|
3365
3954
|
project_name: ${String(project.project_name)}
|
|
3366
3955
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
3367
3956
|
`;
|
|
3368
|
-
const path =
|
|
3957
|
+
const path = join14(ctx.repoRoot, ".copier-answers.yml");
|
|
3369
3958
|
if (safeReadText(path) !== text2) {
|
|
3370
3959
|
changedFiles.push(path);
|
|
3371
3960
|
if (!ctx.dryRun) writeText(path, text2);
|
|
@@ -3384,14 +3973,14 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3384
3973
|
id: "bmad.scaffold",
|
|
3385
3974
|
title: "BMAD modules/docs scaffold",
|
|
3386
3975
|
audit: (ctx) => {
|
|
3387
|
-
const targetRoot =
|
|
3976
|
+
const targetRoot = join14(ctx.repoRoot, "_bmad");
|
|
3388
3977
|
const sentinels = [
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3978
|
+
join14("core", "config.yaml"),
|
|
3979
|
+
join14("config.toml"),
|
|
3980
|
+
join14("_config", "manifest.yaml"),
|
|
3981
|
+
join14("bmm", "config.yaml")
|
|
3393
3982
|
];
|
|
3394
|
-
const missing = sentinels.filter((file) => !
|
|
3983
|
+
const missing = sentinels.filter((file) => !existsSync10(join14(targetRoot, file)));
|
|
3395
3984
|
return {
|
|
3396
3985
|
id: "bmad.scaffold",
|
|
3397
3986
|
title: "BMAD modules/docs scaffold",
|
|
@@ -3405,7 +3994,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3405
3994
|
const changedFiles = [];
|
|
3406
3995
|
if (ctx.dryRun) {
|
|
3407
3996
|
for (const detail of finding.details) {
|
|
3408
|
-
changedFiles.push(
|
|
3997
|
+
changedFiles.push(join14(ctx.repoRoot, detail));
|
|
3409
3998
|
}
|
|
3410
3999
|
return {
|
|
3411
4000
|
id: finding.id,
|
|
@@ -3430,8 +4019,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3430
4019
|
};
|
|
3431
4020
|
}
|
|
3432
4021
|
for (const detail of finding.details) {
|
|
3433
|
-
if (
|
|
3434
|
-
changedFiles.push(
|
|
4022
|
+
if (existsSync10(join14(ctx.repoRoot, detail))) {
|
|
4023
|
+
changedFiles.push(join14(ctx.repoRoot, detail));
|
|
3435
4024
|
}
|
|
3436
4025
|
}
|
|
3437
4026
|
return {
|
|
@@ -3454,7 +4043,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3454
4043
|
id: "bmad.version",
|
|
3455
4044
|
title: "BMAD version currency",
|
|
3456
4045
|
status: "skip",
|
|
3457
|
-
summary:
|
|
4046
|
+
summary: existsSync10(join14(ctx.repoRoot, "_bmad")) ? "BMAD installed but version manifest unreadable" : "No BMAD install present",
|
|
3458
4047
|
details: [],
|
|
3459
4048
|
fixable: false
|
|
3460
4049
|
};
|
|
@@ -3509,7 +4098,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3509
4098
|
}
|
|
3510
4099
|
const installed = readInstalledBmadVersion(ctx.repoRoot);
|
|
3511
4100
|
const available = resolveBmadDistTags(ctx.homeDir)?.distTags?.[BMAD_TARGET_CHANNEL];
|
|
3512
|
-
const manifestPath =
|
|
4101
|
+
const manifestPath = join14(ctx.repoRoot, "_bmad", "_config", "manifest.yaml");
|
|
3513
4102
|
if (ctx.dryRun) {
|
|
3514
4103
|
return {
|
|
3515
4104
|
id: finding.id,
|
|
@@ -3556,11 +4145,11 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3556
4145
|
}
|
|
3557
4146
|
const details = [];
|
|
3558
4147
|
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
|
|
3559
|
-
if (!
|
|
4148
|
+
if (!existsSync10(join14(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join14(role.roleDir, rel))}`);
|
|
3560
4149
|
}
|
|
3561
|
-
const gitmodules = safeReadText(
|
|
4150
|
+
const gitmodules = safeReadText(join14(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
3562
4151
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
3563
|
-
if (!profileMetaInheritsDefault(
|
|
4152
|
+
if (!profileMetaInheritsDefault(join14(role.roleDir, "runtime", "profile.yaml"))) {
|
|
3564
4153
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
3565
4154
|
}
|
|
3566
4155
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -3581,21 +4170,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3581
4170
|
if (!role) {
|
|
3582
4171
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
3583
4172
|
}
|
|
3584
|
-
const templateRoleDir =
|
|
3585
|
-
writeIfDifferent(
|
|
3586
|
-
writeIfDifferent(
|
|
3587
|
-
writeIfDifferent(
|
|
3588
|
-
copyMissingRecursive(
|
|
3589
|
-
copyMissingRecursive(
|
|
3590
|
-
copyMissingRecursive(
|
|
3591
|
-
const promptSource =
|
|
3592
|
-
const promptTarget =
|
|
3593
|
-
if (
|
|
4173
|
+
const templateRoleDir = join14(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
4174
|
+
writeIfDifferent(join14(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
4175
|
+
writeIfDifferent(join14(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
4176
|
+
writeIfDifferent(join14(role.roleDir, ".gitignore"), readText(join14(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
4177
|
+
copyMissingRecursive(join14(templateRoleDir, ".runtime-scaffold"), join14(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
4178
|
+
copyMissingRecursive(join14(templateRoleDir, ".runtime-scaffold"), join14(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
4179
|
+
copyMissingRecursive(join14(templateRoleDir, ".scripts"), join14(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
4180
|
+
const promptSource = join14(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
4181
|
+
const promptTarget = join14(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
4182
|
+
if (existsSync10(promptSource) && !existsSync10(promptTarget)) {
|
|
3594
4183
|
const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo).replace(/\{\{ display_name \}\}/g, role.displayName || role.agentId);
|
|
3595
4184
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
3596
4185
|
}
|
|
3597
4186
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
3598
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
4187
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join14(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
3599
4188
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
3600
4189
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
3601
4190
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -3627,7 +4216,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3627
4216
|
const details = [];
|
|
3628
4217
|
for (const role of roles) {
|
|
3629
4218
|
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3630
|
-
const runtimeRelPath =
|
|
4219
|
+
const runtimeRelPath = join14(roleRelDir, "runtime");
|
|
3631
4220
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3632
4221
|
cwd: ctx.repoRoot,
|
|
3633
4222
|
encoding: "utf8"
|
|
@@ -3635,8 +4224,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3635
4224
|
if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
|
|
3636
4225
|
details.push(`submodule runtime is tracked in Git index at ${runtimeRelPath}`);
|
|
3637
4226
|
}
|
|
3638
|
-
const gitignorePath =
|
|
3639
|
-
if (
|
|
4227
|
+
const gitignorePath = join14(role.roleDir, ".gitignore");
|
|
4228
|
+
if (existsSync10(gitignorePath)) {
|
|
3640
4229
|
const content = safeReadText(gitignorePath) ?? "";
|
|
3641
4230
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3642
4231
|
if (!lines.includes("runtime/") && !lines.includes("runtime")) {
|
|
@@ -3661,7 +4250,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3661
4250
|
const details = [];
|
|
3662
4251
|
for (const role of roles) {
|
|
3663
4252
|
const roleRelDir = relative(ctx.repoRoot, role.roleDir);
|
|
3664
|
-
const runtimeRelPath =
|
|
4253
|
+
const runtimeRelPath = join14(roleRelDir, "runtime");
|
|
3665
4254
|
const lsResult = spawnSync6("git", ["ls-files", "--stage", runtimeRelPath], {
|
|
3666
4255
|
cwd: ctx.repoRoot,
|
|
3667
4256
|
encoding: "utf8"
|
|
@@ -3676,10 +4265,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3676
4265
|
});
|
|
3677
4266
|
}
|
|
3678
4267
|
}
|
|
3679
|
-
const gitignorePath =
|
|
4268
|
+
const gitignorePath = join14(role.roleDir, ".gitignore");
|
|
3680
4269
|
let content = "";
|
|
3681
4270
|
let isIgnored = false;
|
|
3682
|
-
if (
|
|
4271
|
+
if (existsSync10(gitignorePath)) {
|
|
3683
4272
|
content = safeReadText(gitignorePath) ?? "";
|
|
3684
4273
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
3685
4274
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
@@ -3746,9 +4335,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3746
4335
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
3747
4336
|
}
|
|
3748
4337
|
for (const role of roles) {
|
|
3749
|
-
const sysDir =
|
|
4338
|
+
const sysDir = join14(ctx.homeDir, ".config", "systemd", "user");
|
|
3750
4339
|
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
3751
|
-
const allUnitsPresent = units.every((unit) =>
|
|
4340
|
+
const allUnitsPresent = units.every((unit) => existsSync10(join14(sysDir, unit)));
|
|
3752
4341
|
if (allUnitsPresent) {
|
|
3753
4342
|
if (ctx.dryRun) {
|
|
3754
4343
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -3760,8 +4349,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
3760
4349
|
}
|
|
3761
4350
|
continue;
|
|
3762
4351
|
}
|
|
3763
|
-
for (const script of [
|
|
3764
|
-
if (!script || !
|
|
4352
|
+
for (const script of [join14(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
4353
|
+
if (!script || !existsSync10(script)) continue;
|
|
3765
4354
|
if (ctx.dryRun) {
|
|
3766
4355
|
details.push(`would run: bash ${script}`);
|
|
3767
4356
|
} else {
|
|
@@ -3788,7 +4377,7 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
3788
4377
|
changedFiles.push(path);
|
|
3789
4378
|
if (!dryRun) {
|
|
3790
4379
|
writeText(path, normalized);
|
|
3791
|
-
if (mode)
|
|
4380
|
+
if (mode) chmodSync4(path, mode);
|
|
3792
4381
|
}
|
|
3793
4382
|
}
|
|
3794
4383
|
function getParityRuleIds() {
|
|
@@ -3885,7 +4474,7 @@ var server = new McpServer({
|
|
|
3885
4474
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
3886
4475
|
function resolveTargetDir(targetDir) {
|
|
3887
4476
|
const dir = resolve3(targetDir ?? process.cwd());
|
|
3888
|
-
if (!
|
|
4477
|
+
if (!existsSync11(dir)) {
|
|
3889
4478
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
3890
4479
|
}
|
|
3891
4480
|
if (!statSync2(dir).isDirectory()) {
|
|
@@ -3896,7 +4485,7 @@ function resolveTargetDir(targetDir) {
|
|
|
3896
4485
|
function resolvePjanglerRoot3() {
|
|
3897
4486
|
let dir = dirname8(fileURLToPath5(import.meta.url));
|
|
3898
4487
|
while (dir !== dirname8(dir)) {
|
|
3899
|
-
if (
|
|
4488
|
+
if (existsSync11(join15(dir, "package.json")) && existsSync11(join15(dir, "templates", "commonproject", "copier.yml"))) {
|
|
3900
4489
|
return dir;
|
|
3901
4490
|
}
|
|
3902
4491
|
dir = dirname8(dir);
|
|
@@ -4098,8 +4687,8 @@ server.registerTool(
|
|
|
4098
4687
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
4099
4688
|
const projectSlug = input.projectSlug ?? slugify(input.projectName);
|
|
4100
4689
|
const parentDir = resolve3(input.parentDir ?? process.cwd());
|
|
4101
|
-
if (!
|
|
4102
|
-
const targetDir = resolve3(input.targetDir ??
|
|
4690
|
+
if (!existsSync11(parentDir) || !statSync2(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
4691
|
+
const targetDir = resolve3(input.targetDir ?? join15(parentDir, projectSlug));
|
|
4103
4692
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
4104
4693
|
const dryRun = input.dryRun ?? true;
|
|
4105
4694
|
const local = input.local ?? true;
|
|
@@ -4109,7 +4698,7 @@ server.registerTool(
|
|
|
4109
4698
|
if (!skipPlane && ticketProvider === "plane" && !boardId) {
|
|
4110
4699
|
throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
|
|
4111
4700
|
}
|
|
4112
|
-
if (!dryRun &&
|
|
4701
|
+
if (!dryRun && existsSync11(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
4113
4702
|
const plan = planProjectInit({
|
|
4114
4703
|
name: input.projectName,
|
|
4115
4704
|
description: input.projectDescription,
|
|
@@ -4135,7 +4724,7 @@ server.registerTool(
|
|
|
4135
4724
|
if (dryRun) {
|
|
4136
4725
|
return asText({ ...plan, guidance: parityGuidance() });
|
|
4137
4726
|
}
|
|
4138
|
-
const result = executeProjectInitPlan(plan);
|
|
4727
|
+
const result = await executeProjectInitPlan(plan);
|
|
4139
4728
|
if (!result.ok) return asText({ ...result, guidance: parityGuidance() });
|
|
4140
4729
|
let agentResult;
|
|
4141
4730
|
if (input.provisionAgent) {
|
|
@@ -4211,7 +4800,7 @@ server.registerTool(
|
|
|
4211
4800
|
overwrite: input.force ?? false
|
|
4212
4801
|
});
|
|
4213
4802
|
if (!input.apply) return asText(plan);
|
|
4214
|
-
return asText(executeProjectInitPlan(plan));
|
|
4803
|
+
return asText(await executeProjectInitPlan(plan));
|
|
4215
4804
|
} catch (err) {
|
|
4216
4805
|
return { isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] };
|
|
4217
4806
|
}
|