@delorenj/pjangler 1.1.4 → 1.2.1
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/.mise/scripts/link-agentfiles.sh +9 -0
- package/.mise/scripts/versioning.sh +236 -0
- package/dist/index.js +498 -277
- package/dist/mcp-server.js +1706 -163
- package/package.json +4 -2
- package/templates/commonproject/.mise/scripts/create-plane-project.sh +26 -0
- package/templates/commonproject/copier.yml +2 -0
- package/templates/commonproject/template/.agents/hooks/README.md +151 -0
- package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +63 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-journal-write.sh +327 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-recall.sh +180 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-retain.sh +74 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-session-end.sh +57 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-bank.sh +57 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-journal.sh +48 -0
- package/templates/commonproject/template/.agents/hooks/hooks.master.json +97 -0
- package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +30 -0
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +51 -0
- package/templates/commonproject/template/.agents/hooks/reminder-for-skill-check.sh +20 -0
- package/templates/commonproject/template/.agents/hooks/sync.py +618 -0
- package/templates/commonproject/template/.agents/local.example.json +15 -0
- package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +62 -0
- package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +110 -0
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +45 -0
- package/templates/commonproject/template/mise.toml.jinja +42 -0
- package/templates/hermes-agent/.codegraph/daemon.pid +6 -0
- package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +10 -0
- package/templates/hermes-agent/README.md +16 -14
- package/templates/hermes-agent/copier.yml +7 -25
- package/templates/hermes-agent/docs/architecture.md +16 -18
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +35 -0
- package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +253 -0
- package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +280 -0
- package/templates/hermes-agent/docs/fleet-control-plane/implementation-readiness-report-2026-06-27.md +118 -0
- package/templates/hermes-agent/docs/fleet-control-plane/prd.md +141 -0
- package/templates/hermes-agent/docs/operations.md +17 -19
- package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +17 -13
- package/templates/hermes-agent/docs/{scrum-master → sentinel}/README.md +47 -46
- package/templates/hermes-agent/docs/{scrum-master → sentinel}/architecture.md +39 -38
- package/templates/hermes-agent/docs/{scrum-master → sentinel}/development.md +63 -99
- package/templates/hermes-agent/docs/{scrum-master → sentinel}/providers.md +2 -2
- package/templates/hermes-agent/docs/sentinel.md +18 -0
- package/templates/hermes-agent/install-local.sh +22 -20
- package/templates/hermes-agent/runtime-scaffold/README.md +10 -9
- package/templates/hermes-agent/scripts/backfill-fleet-sot.sh +12 -21
- package/templates/hermes-agent/scripts/fleet-sync.sh +369 -0
- package/templates/hermes-agent/scripts/migrate-unify.sh +252 -0
- package/templates/hermes-agent/scripts/unify-pm.sh +148 -0
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +10 -9
- package/templates/hermes-agent/template/.scripts/01-config.sh +2 -3
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -2
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +47 -19
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +71 -27
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +3 -3
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +3 -3
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +48 -43
- package/templates/hermes-agent/template/.scripts/80-registry.sh +2 -2
- package/templates/hermes-agent/template/.scripts/99-summary.sh +3 -4
- package/templates/hermes-agent/template/.scripts/_lib.sh +4 -19
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +36 -0
- package/templates/hermes-agent/template/.scripts/config.example.toml +11 -5
- package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.sh → heartbeat.sh} +84 -31
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +1 -1
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -3
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/emit-event.py +5 -5
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-autonomous-review.sh +6 -6
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-close-gate.sh +2 -2
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/autonomous-delegated-review.md +7 -7
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/bloodbank-events.md +3 -3
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/continuous-ticket-orchestration.md +6 -6
- package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.prompt.md.jinja → sentinel.prompt.md.jinja} +5 -4
- package/templates/hermes-agent/template/SOUL.md.jinja +3 -6
- package/templates/hermes-agent/template/hermes.jinja +6 -15
- package/templates/hermes-agent/template/role.yaml.jinja +9 -8
- package/templates/commonproject/.claude/commands/bmad/bmb/agents/agent-builder.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmb/agents/module-builder.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmb/agents/workflow-builder.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmb/workflows/agent.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmb/workflows/module.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmb/workflows/workflow.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/analyst.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/architect.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/dev.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/pm.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/quick-flow-solo-dev.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/sm.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/tea.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/tech-writer.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/ux-designer.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/check-implementation-readiness.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/code-review.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/correct-course.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-architecture.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-epics-and-stories.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-dataflow.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-diagram.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-flowchart.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-wireframe.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-product-brief.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-story.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-ux-design.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/dev-story.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/document-project.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/generate-project-context.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/prd.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-dev.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-spec.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/research.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/retrospective.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-planning.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-status.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-atdd.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-automate.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-ci.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-framework.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-nfr.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-design.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-review.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-trace.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-init.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-status.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/cis/agents/brainstorming-coach.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/creative-problem-solver.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/design-thinking-coach.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/innovation-strategist.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/presentation-master.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/storyteller.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/workflows/design-thinking.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/cis/workflows/innovation-strategy.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/cis/workflows/problem-solving.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/cis/workflows/storytelling.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/core/agents/bmad-master.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/core/tasks/index-docs.md +0 -9
- package/templates/commonproject/.claude/commands/bmad/core/tasks/shard-doc.md +0 -9
- package/templates/commonproject/.claude/commands/bmad/core/workflows/brainstorming.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/core/workflows/party-mode.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/custom/workflows/ticket-lifecycle.md +0 -5
- package/templates/hermes-agent/docs/scrum-master-sentinel.md +0 -17
- package/templates/hermes-agent/template/.scripts/75-scrum-master.sh +0 -117
- package/templates/hermes-agent/template/.scripts/90-chain-scrum-master.sh +0 -45
package/dist/mcp-server.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
-
}) : x)(function(x) {
|
|
5
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
2
|
|
|
9
3
|
// src/mcp-server.ts
|
|
10
|
-
import {
|
|
11
|
-
import { existsSync as
|
|
4
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, statSync } from "node:fs";
|
|
6
|
+
import { basename as basename2, dirname as dirname7, join as join10, resolve as resolve2 } from "node:path";
|
|
7
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
12
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
10
|
import { z } from "zod";
|
|
15
11
|
|
|
16
12
|
// src/commands/Command.ts
|
|
13
|
+
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
14
|
+
import { join, dirname } from "path";
|
|
17
15
|
var Command = class {
|
|
18
16
|
context;
|
|
19
17
|
constructor(context) {
|
|
@@ -26,30 +24,24 @@ var Command = class {
|
|
|
26
24
|
return this.context.dryRun ? `[DRY RUN] ${message}` : message;
|
|
27
25
|
}
|
|
28
26
|
fileExists(filePath) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const fullPath = join7(this.context.targetDir, filePath);
|
|
32
|
-
return existsSync6(fullPath);
|
|
27
|
+
const fullPath = join(this.context.targetDir, filePath);
|
|
28
|
+
return existsSync(fullPath);
|
|
33
29
|
}
|
|
34
30
|
writeFile(filePath, content) {
|
|
35
31
|
if (this.context.dryRun) {
|
|
36
32
|
return;
|
|
37
33
|
}
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
mkdirSync3(dir, { recursive: true });
|
|
43
|
-
writeFileSync2(fullPath, content);
|
|
34
|
+
const fullPath = join(this.context.targetDir, filePath);
|
|
35
|
+
const dir = dirname(fullPath);
|
|
36
|
+
mkdirSync(dir, { recursive: true });
|
|
37
|
+
writeFileSync(fullPath, content);
|
|
44
38
|
}
|
|
45
39
|
createDirectory(dirPath) {
|
|
46
40
|
if (this.context.dryRun) {
|
|
47
41
|
return;
|
|
48
42
|
}
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const fullPath = join7(this.context.targetDir, dirPath);
|
|
52
|
-
mkdirSync3(fullPath, { recursive: true });
|
|
43
|
+
const fullPath = join(this.context.targetDir, dirPath);
|
|
44
|
+
mkdirSync(fullPath, { recursive: true });
|
|
53
45
|
}
|
|
54
46
|
};
|
|
55
47
|
|
|
@@ -460,32 +452,32 @@ var NodeRecipe = class extends Recipe {
|
|
|
460
452
|
|
|
461
453
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
462
454
|
import { homedir, platform } from "node:os";
|
|
463
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
464
|
-
import { join, dirname } from "node:path";
|
|
455
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
456
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
465
457
|
function resolveTemplateConfigPath() {
|
|
466
458
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
467
459
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
468
460
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
469
|
-
const base = xdg && xdg.length ? xdg :
|
|
470
|
-
return
|
|
461
|
+
const base = xdg && xdg.length ? xdg : join2(homedir(), ".config");
|
|
462
|
+
return join2(base, "hermes-agent-template", "config.toml");
|
|
471
463
|
}
|
|
472
464
|
function detectHermesBin(home) {
|
|
473
465
|
const candidates = [
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
466
|
+
join2(home, "code", "hermes-agent", "venv", "bin", "hermes"),
|
|
467
|
+
join2(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
|
|
468
|
+
join2(home, ".local", "bin", "hermes")
|
|
477
469
|
];
|
|
478
470
|
for (const c of candidates) {
|
|
479
|
-
if (
|
|
471
|
+
if (existsSync2(c)) return c;
|
|
480
472
|
}
|
|
481
473
|
return candidates[0];
|
|
482
474
|
}
|
|
483
475
|
function renderHostConfig() {
|
|
484
476
|
const home = homedir();
|
|
485
477
|
const hermesBin = detectHermesBin(home);
|
|
486
|
-
const hermesRepo =
|
|
487
|
-
const scaffoldDir =
|
|
488
|
-
const skillsDir =
|
|
478
|
+
const hermesRepo = join2(home, "code", "hermes-agent");
|
|
479
|
+
const scaffoldDir = join2(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
480
|
+
const skillsDir = join2(home, ".agents", "skills");
|
|
489
481
|
return `# hermes-agent-template \u2014 host configuration
|
|
490
482
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
491
483
|
#
|
|
@@ -529,7 +521,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
529
521
|
const ctx = this.context;
|
|
530
522
|
const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
|
|
531
523
|
const path = resolveTemplateConfigPath();
|
|
532
|
-
const exists =
|
|
524
|
+
const exists = existsSync2(path);
|
|
533
525
|
if (exists && !force) {
|
|
534
526
|
console.log(`\u2713 Config present: ${path}`);
|
|
535
527
|
return { success: true, message: "" };
|
|
@@ -539,8 +531,8 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
539
531
|
return { success: true, message: "" };
|
|
540
532
|
}
|
|
541
533
|
try {
|
|
542
|
-
|
|
543
|
-
|
|
534
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
535
|
+
writeFileSync2(path, renderHostConfig());
|
|
544
536
|
} catch (err) {
|
|
545
537
|
const msg = err instanceof Error ? err.message : String(err);
|
|
546
538
|
return { success: false, message: `\u2717 Failed to write ${path}: ${msg}` };
|
|
@@ -552,7 +544,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
552
544
|
};
|
|
553
545
|
|
|
554
546
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
555
|
-
import { basename, join as
|
|
547
|
+
import { basename, join as join3 } from "node:path";
|
|
556
548
|
import { readFileSync } from "node:fs";
|
|
557
549
|
import * as p from "@clack/prompts";
|
|
558
550
|
|
|
@@ -560,12 +552,7 @@ import * as p from "@clack/prompts";
|
|
|
560
552
|
var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
|
|
561
553
|
var SOUL_TONES = ["direct", "playful", "formal", "terse"];
|
|
562
554
|
var ROLE_CHOICES = [
|
|
563
|
-
{ value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship" },
|
|
564
|
-
{
|
|
565
|
-
value: "scrum-master",
|
|
566
|
-
label: "Scrum Master (Ticket Sentinel)",
|
|
567
|
-
hint: "continuous ticket sentinel + autonomous delegated review"
|
|
568
|
-
},
|
|
555
|
+
{ value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
|
|
569
556
|
{ value: "dev", label: "Developer (dev)", hint: "implements tickets" },
|
|
570
557
|
{ value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
|
|
571
558
|
{ value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
|
|
@@ -586,7 +573,7 @@ function deriveProfileName(repo, role) {
|
|
|
586
573
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
587
574
|
function detectTicketProvider(targetDir) {
|
|
588
575
|
try {
|
|
589
|
-
const t = JSON.parse(readFileSync(
|
|
576
|
+
const t = JSON.parse(readFileSync(join3(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
590
577
|
return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
|
|
591
578
|
} catch {
|
|
592
579
|
return void 0;
|
|
@@ -605,7 +592,6 @@ var PromptForAgentConfig = class extends Command {
|
|
|
605
592
|
ctx.modelProvider ??= "";
|
|
606
593
|
ctx.modelName ??= "";
|
|
607
594
|
ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
|
|
608
|
-
ctx.withScrumMaster ??= false;
|
|
609
595
|
ctx.skipTelegram ??= true;
|
|
610
596
|
ctx.skipEmail ??= true;
|
|
611
597
|
ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
|
|
@@ -651,14 +637,6 @@ var PromptForAgentConfig = class extends Command {
|
|
|
651
637
|
if (p.isCancel(answer)) return this.cancelled();
|
|
652
638
|
ctx.ticketProvider = answer;
|
|
653
639
|
}
|
|
654
|
-
if (ctx.role === "pm" && ctx.withScrumMaster === void 0) {
|
|
655
|
-
const answer = await p.confirm({
|
|
656
|
-
message: "Also provision the paired Scrum Master (Ticket Sentinel) for this repo?",
|
|
657
|
-
initialValue: true
|
|
658
|
-
});
|
|
659
|
-
if (p.isCancel(answer)) return this.cancelled();
|
|
660
|
-
ctx.withScrumMaster = answer === true;
|
|
661
|
-
}
|
|
662
640
|
if (!ctx.agentPurpose) {
|
|
663
641
|
const answer = await p.text({
|
|
664
642
|
message: "One-line purpose",
|
|
@@ -731,21 +709,21 @@ var PromptForAgentConfig = class extends Command {
|
|
|
731
709
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
732
710
|
import { spawnSync } from "node:child_process";
|
|
733
711
|
import { homedir as homedir2 } from "node:os";
|
|
734
|
-
import { join as
|
|
735
|
-
import { existsSync as
|
|
712
|
+
import { join as join4, dirname as dirname3 } from "node:path";
|
|
713
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
736
714
|
import { fileURLToPath } from "node:url";
|
|
737
715
|
import * as p2 from "@clack/prompts";
|
|
738
716
|
function resolveVendoredTemplate(name) {
|
|
739
717
|
let dir;
|
|
740
718
|
try {
|
|
741
|
-
dir =
|
|
719
|
+
dir = dirname3(fileURLToPath(import.meta.url));
|
|
742
720
|
} catch {
|
|
743
721
|
return void 0;
|
|
744
722
|
}
|
|
745
723
|
for (let i = 0; i < 8; i++) {
|
|
746
|
-
const candidate =
|
|
747
|
-
if (
|
|
748
|
-
const parent =
|
|
724
|
+
const candidate = join4(dir, "templates", name);
|
|
725
|
+
if (existsSync3(join4(candidate, "copier.yml"))) return candidate;
|
|
726
|
+
const parent = dirname3(dir);
|
|
749
727
|
if (parent === dir) break;
|
|
750
728
|
dir = parent;
|
|
751
729
|
}
|
|
@@ -757,14 +735,13 @@ var RunCopierTemplate = class extends Command {
|
|
|
757
735
|
const { targetRepo, role, agentPurpose, soulTone, modelProvider, modelName } = ctx;
|
|
758
736
|
const ticketProvider = ctx.ticketProvider ?? "plane";
|
|
759
737
|
const profileName = ctx.profileName ?? (targetRepo && role ? deriveProfileName(targetRepo, role) : void 0);
|
|
760
|
-
const withScrumMaster = role === "pm" && ctx.withScrumMaster === true;
|
|
761
738
|
if (!targetRepo || !role) {
|
|
762
739
|
return {
|
|
763
740
|
success: false,
|
|
764
741
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
765
742
|
};
|
|
766
743
|
}
|
|
767
|
-
const roleDir =
|
|
744
|
+
const roleDir = join4(ctx.targetDir, "agents", "hermes", role);
|
|
768
745
|
ctx.roleDir = roleDir;
|
|
769
746
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
770
747
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -774,7 +751,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
774
751
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
775
752
|
};
|
|
776
753
|
}
|
|
777
|
-
if (
|
|
754
|
+
if (existsSync3(join4(roleDir, "role.yaml")) && !ctx.force) {
|
|
778
755
|
if (ctx.yes) {
|
|
779
756
|
ctx.force = true;
|
|
780
757
|
} else {
|
|
@@ -801,9 +778,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
801
778
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
802
779
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
803
780
|
};
|
|
804
|
-
const LOCAL_TEMPLATE =
|
|
781
|
+
const LOCAL_TEMPLATE = join4(homedir2(), "code", "hermes-agent-template");
|
|
805
782
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
806
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (
|
|
783
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join4(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
807
784
|
const args = [
|
|
808
785
|
"copy",
|
|
809
786
|
templateSrc,
|
|
@@ -824,8 +801,6 @@ var RunCopierTemplate = class extends Command {
|
|
|
824
801
|
`soul_tone=${soulTone ?? "direct"}`,
|
|
825
802
|
"--data",
|
|
826
803
|
`ticket_provider=${ticketProvider}`,
|
|
827
|
-
"--data",
|
|
828
|
-
`with_scrum_master=${withScrumMaster}`,
|
|
829
804
|
"--trust",
|
|
830
805
|
"--vcs-ref=HEAD"
|
|
831
806
|
];
|
|
@@ -836,7 +811,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
836
811
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
837
812
|
};
|
|
838
813
|
}
|
|
839
|
-
|
|
814
|
+
mkdirSync3(join4(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
840
815
|
const spinner4 = p2.spinner();
|
|
841
816
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
842
817
|
const result = spawnSync("copier", args, {
|
|
@@ -861,8 +836,8 @@ var RunCopierTemplate = class extends Command {
|
|
|
861
836
|
|
|
862
837
|
// src/commands/hermes/WireTelegram.ts
|
|
863
838
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
864
|
-
import { join as
|
|
865
|
-
import { existsSync as
|
|
839
|
+
import { join as join5 } from "node:path";
|
|
840
|
+
import { existsSync as existsSync4, unlinkSync } from "node:fs";
|
|
866
841
|
import * as p3 from "@clack/prompts";
|
|
867
842
|
var WireTelegram = class extends Command {
|
|
868
843
|
async invoke() {
|
|
@@ -950,15 +925,15 @@ var WireTelegram = class extends Command {
|
|
|
950
925
|
if (p3.isCancel(allowedAnswer)) {
|
|
951
926
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
952
927
|
}
|
|
953
|
-
const script =
|
|
954
|
-
if (!
|
|
928
|
+
const script = join5(roleDir, ".scripts", "30-telegram.sh");
|
|
929
|
+
if (!existsSync4(script)) {
|
|
955
930
|
return {
|
|
956
931
|
success: false,
|
|
957
932
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
958
933
|
};
|
|
959
934
|
}
|
|
960
|
-
const marker =
|
|
961
|
-
if (
|
|
935
|
+
const marker = join5(roleDir, ".scripts", ".done-30-telegram");
|
|
936
|
+
if (existsSync4(marker)) unlinkSync(marker);
|
|
962
937
|
const spinner4 = p3.spinner();
|
|
963
938
|
spinner4.start("Verifying token + wiring profile");
|
|
964
939
|
const result = spawnSync2("bash", [script], {
|
|
@@ -985,8 +960,8 @@ function cap(s) {
|
|
|
985
960
|
|
|
986
961
|
// src/commands/hermes/WireEmail.ts
|
|
987
962
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
988
|
-
import { join as
|
|
989
|
-
import { existsSync as
|
|
963
|
+
import { join as join6 } from "node:path";
|
|
964
|
+
import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
990
965
|
import * as p4 from "@clack/prompts";
|
|
991
966
|
var WireEmail = class extends Command {
|
|
992
967
|
async invoke() {
|
|
@@ -1001,8 +976,8 @@ var WireEmail = class extends Command {
|
|
|
1001
976
|
if (!targetRepo || !role || !roleDir) {
|
|
1002
977
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
1003
978
|
}
|
|
1004
|
-
const script =
|
|
1005
|
-
if (!
|
|
979
|
+
const script = join6(roleDir, ".scripts", "50-email.sh");
|
|
980
|
+
if (!existsSync5(script)) {
|
|
1006
981
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1007
982
|
}
|
|
1008
983
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
@@ -1064,8 +1039,8 @@ var WireEmail = class extends Command {
|
|
|
1064
1039
|
}
|
|
1065
1040
|
}
|
|
1066
1041
|
}
|
|
1067
|
-
const marker =
|
|
1068
|
-
if (
|
|
1042
|
+
const marker = join6(roleDir, ".scripts", ".done-50-email");
|
|
1043
|
+
if (existsSync5(marker)) unlinkSync2(marker);
|
|
1069
1044
|
const spinner4 = p4.spinner();
|
|
1070
1045
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1071
1046
|
const result = spawnSync3("bash", [script], {
|
|
@@ -1094,7 +1069,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1094
1069
|
const email = `${targetRepo}-${role}@delo.sh`;
|
|
1095
1070
|
const gw = `hermes-${agentId}-gateway.service`;
|
|
1096
1071
|
const csm = `hermes-${agentId}-consumer.service`;
|
|
1097
|
-
const
|
|
1072
|
+
const hb = `hermes-${agentId}-heartbeat.timer`;
|
|
1098
1073
|
const lines = [];
|
|
1099
1074
|
lines.push(`agent_id ${agentId}`);
|
|
1100
1075
|
lines.push(`role dir ${ctx.roleDir}`);
|
|
@@ -1104,7 +1079,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1104
1079
|
lines.push("");
|
|
1105
1080
|
lines.push("Start daemons:");
|
|
1106
1081
|
lines.push(` systemctl --user start ${csm}`);
|
|
1107
|
-
lines.push(` systemctl --user start ${
|
|
1082
|
+
lines.push(` systemctl --user start ${hb}`);
|
|
1108
1083
|
if (!skipTelegram) {
|
|
1109
1084
|
lines.push(` systemctl --user start ${gw}`);
|
|
1110
1085
|
} else {
|
|
@@ -1151,6 +1126,193 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1151
1126
|
}
|
|
1152
1127
|
};
|
|
1153
1128
|
|
|
1129
|
+
// src/commands/AgentHooksCommands.ts
|
|
1130
|
+
import { homedir as homedir3 } from "node:os";
|
|
1131
|
+
import { join as join7, dirname as dirname4 } from "node:path";
|
|
1132
|
+
import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1133
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1134
|
+
function resolveTemplateRoot() {
|
|
1135
|
+
const candidates = [];
|
|
1136
|
+
if (process.env.PJANGLER_COMMONPROJECT_TEMPLATE) {
|
|
1137
|
+
candidates.push(process.env.PJANGLER_COMMONPROJECT_TEMPLATE);
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
1141
|
+
for (let i = 0; i < 8; i++) {
|
|
1142
|
+
candidates.push(join7(dir, "templates", "commonproject", "template"));
|
|
1143
|
+
const parent = dirname4(dir);
|
|
1144
|
+
if (parent === dir) break;
|
|
1145
|
+
dir = parent;
|
|
1146
|
+
}
|
|
1147
|
+
} catch {
|
|
1148
|
+
}
|
|
1149
|
+
candidates.push(join7(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1150
|
+
for (const c of candidates) {
|
|
1151
|
+
if (existsSync6(join7(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1152
|
+
}
|
|
1153
|
+
throw new Error(
|
|
1154
|
+
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
var CopyAgentHooksTree = class extends Command {
|
|
1158
|
+
async invoke() {
|
|
1159
|
+
let templateRoot;
|
|
1160
|
+
try {
|
|
1161
|
+
templateRoot = resolveTemplateRoot();
|
|
1162
|
+
} catch (e) {
|
|
1163
|
+
return { success: false, message: `\u26A0\uFE0F ${e.message}` };
|
|
1164
|
+
}
|
|
1165
|
+
const items = [
|
|
1166
|
+
{ rel: ".agents/hooks", dir: true },
|
|
1167
|
+
{ rel: ".agents/local.example.json", dir: false },
|
|
1168
|
+
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
1169
|
+
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
1170
|
+
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
1171
|
+
];
|
|
1172
|
+
const created = [];
|
|
1173
|
+
const skipped = [];
|
|
1174
|
+
for (const { rel, dir } of items) {
|
|
1175
|
+
const src = join7(templateRoot, rel);
|
|
1176
|
+
const dest = join7(this.context.targetDir, rel);
|
|
1177
|
+
if (!existsSync6(src)) continue;
|
|
1178
|
+
if (existsSync6(dest) && !this.context.force) {
|
|
1179
|
+
skipped.push(rel);
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
if (!this.context.dryRun) {
|
|
1183
|
+
mkdirSync4(dirname4(dest), { recursive: true });
|
|
1184
|
+
cpSync(src, dest, { recursive: dir, force: true });
|
|
1185
|
+
}
|
|
1186
|
+
created.push(rel);
|
|
1187
|
+
}
|
|
1188
|
+
const verb = this.context.dryRun ? "Would copy" : "Copied";
|
|
1189
|
+
const tail = skipped.length ? ` (${skipped.length} already present \u2014 use --force to overwrite)` : "";
|
|
1190
|
+
return {
|
|
1191
|
+
success: created.length > 0,
|
|
1192
|
+
message: this.formatMessage(`\u2705 ${verb} ${created.length} agent-hooks path(s)${tail}`)
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
1196
|
+
var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
1197
|
+
static MARKER = "# pjangler:agent-hooks";
|
|
1198
|
+
static CR = "{{config_root}}";
|
|
1199
|
+
// mise's own runtime var — emitted literally
|
|
1200
|
+
async invoke() {
|
|
1201
|
+
const misePath = join7(this.context.targetDir, "mise.toml");
|
|
1202
|
+
if (!existsSync6(misePath)) {
|
|
1203
|
+
return {
|
|
1204
|
+
success: false,
|
|
1205
|
+
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
let content = readFileSync2(misePath, "utf8");
|
|
1209
|
+
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1210
|
+
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1211
|
+
}
|
|
1212
|
+
const cr = _WireMiseAgentHooks.CR;
|
|
1213
|
+
const enterAdds = [
|
|
1214
|
+
` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
|
|
1215
|
+
` "${cr}/.agents/hooks/sync.py --install --quiet",`
|
|
1216
|
+
].join("\n");
|
|
1217
|
+
const leaveBlock = [
|
|
1218
|
+
"leave = [",
|
|
1219
|
+
` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
|
|
1220
|
+
` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
|
|
1221
|
+
"]"
|
|
1222
|
+
].join("\n");
|
|
1223
|
+
let wiredHooks = false;
|
|
1224
|
+
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
1225
|
+
if (enterRe.test(content)) {
|
|
1226
|
+
content = content.replace(enterRe, (_m, head, close) => {
|
|
1227
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
1228
|
+
return `${head}${sep}
|
|
1229
|
+
${enterAdds}${close}`;
|
|
1230
|
+
});
|
|
1231
|
+
const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
1232
|
+
if (leaveRe.test(content)) {
|
|
1233
|
+
content = content.replace(leaveRe, (_m, head, close) => {
|
|
1234
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
1235
|
+
return `${head}${sep}
|
|
1236
|
+
"${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
|
|
1237
|
+
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
1238
|
+
});
|
|
1239
|
+
} else {
|
|
1240
|
+
content = content.replace(enterRe, (m) => `${m}
|
|
1241
|
+
${leaveBlock}`);
|
|
1242
|
+
}
|
|
1243
|
+
wiredHooks = true;
|
|
1244
|
+
}
|
|
1245
|
+
const appended = [
|
|
1246
|
+
"",
|
|
1247
|
+
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
1248
|
+
"[[watch_files]]",
|
|
1249
|
+
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
1250
|
+
'task = "hooks-sync"',
|
|
1251
|
+
"",
|
|
1252
|
+
"[tasks.hooks-sync]",
|
|
1253
|
+
'description = "Fan out hooks.master.json to each agent CLI (claude/codex/kimi/hermes)"',
|
|
1254
|
+
`run = "${cr}/.agents/hooks/sync.py --install"`,
|
|
1255
|
+
"",
|
|
1256
|
+
"[tasks.hooks-check]",
|
|
1257
|
+
'description = "Drift gate: verify generated hook configs match hooks.master.json"',
|
|
1258
|
+
`run = "${cr}/.agents/hooks/sync.py --check"`,
|
|
1259
|
+
"",
|
|
1260
|
+
"[tasks.hooks-uninstall]",
|
|
1261
|
+
'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
|
|
1262
|
+
`run = "${cr}/.agents/hooks/sync.py --uninstall"`,
|
|
1263
|
+
"",
|
|
1264
|
+
"[tasks.link-project-skills-to-clis]",
|
|
1265
|
+
'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
|
|
1266
|
+
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1267
|
+
"",
|
|
1268
|
+
"[tasks.unlink-project-skills-from-clis]",
|
|
1269
|
+
'description = "Remove project skill symlinks from shared per-CLI dirs"',
|
|
1270
|
+
`run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
|
|
1271
|
+
"",
|
|
1272
|
+
"[tasks.skills-relink]",
|
|
1273
|
+
'description = "Re-fan the project skill set to all CLIs"',
|
|
1274
|
+
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1275
|
+
"",
|
|
1276
|
+
"[tasks.hindsight-setup]",
|
|
1277
|
+
`description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
|
|
1278
|
+
`run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
|
|
1279
|
+
"",
|
|
1280
|
+
_WireMiseAgentHooks.MARKER + ":end",
|
|
1281
|
+
""
|
|
1282
|
+
].join("\n");
|
|
1283
|
+
content = content.replace(/\n*$/, "\n") + appended;
|
|
1284
|
+
if (!this.context.dryRun) writeFileSync3(misePath, content);
|
|
1285
|
+
if (wiredHooks) {
|
|
1286
|
+
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
success: true,
|
|
1290
|
+
message: this.formatMessage(
|
|
1291
|
+
`\u2705 Added agent-hooks tasks to mise.toml.
|
|
1292
|
+
\u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
|
|
1293
|
+
enter += "${cr}/.mise/scripts/link-project-skills-to-clis.sh", "${cr}/.agents/hooks/sync.py --install --quiet"
|
|
1294
|
+
leave += "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh", "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
|
|
1295
|
+
)
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
// src/recipes/AgentHooksRecipe.ts
|
|
1301
|
+
var AgentHooksRecipe = class extends Recipe {
|
|
1302
|
+
constructor(context) {
|
|
1303
|
+
super(context);
|
|
1304
|
+
this.addIngredient(CopyAgentHooksTree).addIngredient(WireMiseAgentHooks);
|
|
1305
|
+
}
|
|
1306
|
+
printNextSteps() {
|
|
1307
|
+
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
1308
|
+
console.log(" Next steps:");
|
|
1309
|
+
console.log(" 1. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
1310
|
+
console.log(" 2. git add .claude/settings.json .agents/hooks && commit (codex/kimi/hermes are per-dev)");
|
|
1311
|
+
console.log(" 3. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
1312
|
+
console.log(` 4. If you run a global agent system: echo '{"skills":{"defer_to_global":true}}' > .agents/local.json`);
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
|
|
1154
1316
|
// src/utils/registry.ts
|
|
1155
1317
|
var RECIPE_REGISTRY = {
|
|
1156
1318
|
mise: {
|
|
@@ -1184,9 +1346,27 @@ var RECIPE_REGISTRY = {
|
|
|
1184
1346
|
"WireEmail",
|
|
1185
1347
|
"PrintHermesSummary"
|
|
1186
1348
|
]
|
|
1349
|
+
},
|
|
1350
|
+
"agent-hooks": {
|
|
1351
|
+
name: "agent-hooks",
|
|
1352
|
+
description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
|
|
1353
|
+
class: AgentHooksRecipe,
|
|
1354
|
+
commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
|
|
1187
1355
|
}
|
|
1188
1356
|
};
|
|
1189
1357
|
var COMMAND_REGISTRY = {
|
|
1358
|
+
CopyAgentHooksTree: {
|
|
1359
|
+
name: "CopyAgentHooksTree",
|
|
1360
|
+
description: "Copy the generic agent-hooks tree (hooks SSOT + sync engine + scripts) from the CommonProject template",
|
|
1361
|
+
group: "agent-hooks",
|
|
1362
|
+
class: CopyAgentHooksTree
|
|
1363
|
+
},
|
|
1364
|
+
WireMiseAgentHooks: {
|
|
1365
|
+
name: "WireMiseAgentHooks",
|
|
1366
|
+
description: "Merge agent-hooks enter/leave + tasks into an existing mise.toml (idempotent)",
|
|
1367
|
+
group: "agent-hooks",
|
|
1368
|
+
class: WireMiseAgentHooks
|
|
1369
|
+
},
|
|
1190
1370
|
AddDockerfile: {
|
|
1191
1371
|
name: "AddDockerfile",
|
|
1192
1372
|
description: "Create Dockerfile for containerization",
|
|
@@ -1249,18 +1429,18 @@ function createRecipe(name, context) {
|
|
|
1249
1429
|
}
|
|
1250
1430
|
|
|
1251
1431
|
// src/utils/version.ts
|
|
1252
|
-
import { readFileSync as
|
|
1253
|
-
import { dirname as
|
|
1254
|
-
import { fileURLToPath as
|
|
1432
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1433
|
+
import { dirname as dirname5, join as join8 } from "node:path";
|
|
1434
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1255
1435
|
var PJANGLER_VERSION = (() => {
|
|
1256
1436
|
try {
|
|
1257
|
-
let dir =
|
|
1437
|
+
let dir = dirname5(fileURLToPath3(import.meta.url));
|
|
1258
1438
|
for (let i = 0; i < 4; i++) {
|
|
1259
1439
|
try {
|
|
1260
|
-
const raw =
|
|
1440
|
+
const raw = readFileSync3(join8(dir, "package.json"), "utf8");
|
|
1261
1441
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
1262
1442
|
} catch {
|
|
1263
|
-
const parent =
|
|
1443
|
+
const parent = dirname5(dir);
|
|
1264
1444
|
if (parent === dir) break;
|
|
1265
1445
|
dir = parent;
|
|
1266
1446
|
}
|
|
@@ -1270,14 +1450,1150 @@ var PJANGLER_VERSION = (() => {
|
|
|
1270
1450
|
return "0.0.0";
|
|
1271
1451
|
})();
|
|
1272
1452
|
|
|
1453
|
+
// src/parity/index.ts
|
|
1454
|
+
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, copyFileSync } from "node:fs";
|
|
1455
|
+
import { dirname as dirname6, join as join9, relative, resolve } from "node:path";
|
|
1456
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
1457
|
+
import { homedir as homedir4 } from "node:os";
|
|
1458
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1459
|
+
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
1460
|
+
# agent files to the main AGENTS.md file.
|
|
1461
|
+
#
|
|
1462
|
+
# TODO: Ensure this works for all levels of nesting.
|
|
1463
|
+
# i.e. All linked agent files MUST be siblings at
|
|
1464
|
+
# any given level of nesting.
|
|
1465
|
+
[hooks]
|
|
1466
|
+
enter = [
|
|
1467
|
+
"{{config_root}}/.mise/scripts/link-agentfiles.sh",
|
|
1468
|
+
"op inject -i .env.op > .env",
|
|
1469
|
+
]
|
|
1470
|
+
|
|
1471
|
+
[[watch_files]]
|
|
1472
|
+
patterns = ["AGENTS.md"]
|
|
1473
|
+
task = "link-agentfiles"
|
|
1474
|
+
|
|
1475
|
+
[tasks.link-agentfiles]
|
|
1476
|
+
description = "Symlink all agent files to AGENTS.md"
|
|
1477
|
+
run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
|
|
1478
|
+
var VERSIONING_BLOCK = `# >>> mise-versioning >>> (managed block \u2014 do not edit by hand; re-run init to update)
|
|
1479
|
+
[tasks."version"]
|
|
1480
|
+
description = "Print the current version (vX.Y.Z)"
|
|
1481
|
+
run = "{{config_root}}/.mise/scripts/versioning.sh current"
|
|
1482
|
+
|
|
1483
|
+
[tasks."version:bump"]
|
|
1484
|
+
description = "Bump patch version: vX.Y.Z -> vX.Y.(Z+1)"
|
|
1485
|
+
alias = "version:bump-patch"
|
|
1486
|
+
run = "{{config_root}}/.mise/scripts/versioning.sh bump patch"
|
|
1487
|
+
|
|
1488
|
+
[tasks."version:bump-minor"]
|
|
1489
|
+
description = "Bump minor version: vX.Y.Z -> vX.(Y+1).0"
|
|
1490
|
+
run = "{{config_root}}/.mise/scripts/versioning.sh bump minor"
|
|
1491
|
+
|
|
1492
|
+
[tasks."version:bump-major"]
|
|
1493
|
+
description = "Bump major version: vX.Y.Z -> v(X+1).0.0"
|
|
1494
|
+
run = "{{config_root}}/.mise/scripts/versioning.sh bump major"
|
|
1495
|
+
|
|
1496
|
+
[tasks."version:check"]
|
|
1497
|
+
description = "Verify every versioned file is in parity"
|
|
1498
|
+
run = "{{config_root}}/.mise/scripts/versioning.sh check"
|
|
1499
|
+
|
|
1500
|
+
[tasks."version:sync"]
|
|
1501
|
+
description = "Force every versioned file up to the highest version"
|
|
1502
|
+
run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
1503
|
+
# <<< mise-versioning <<<`;
|
|
1504
|
+
function resolvePjanglerRoot() {
|
|
1505
|
+
let dir = dirname6(fileURLToPath4(import.meta.url));
|
|
1506
|
+
while (dir !== dirname6(dir)) {
|
|
1507
|
+
if (existsSync7(join9(dir, "package.json")) && existsSync7(join9(dir, "templates", "commonproject", "copier.yml"))) {
|
|
1508
|
+
return dir;
|
|
1509
|
+
}
|
|
1510
|
+
dir = dirname6(dir);
|
|
1511
|
+
}
|
|
1512
|
+
throw new Error("Unable to resolve pjangler root");
|
|
1513
|
+
}
|
|
1514
|
+
function normalizeNewlines(value) {
|
|
1515
|
+
return value.replace(/\r\n/g, "\n");
|
|
1516
|
+
}
|
|
1517
|
+
function readText(path) {
|
|
1518
|
+
return normalizeNewlines(readFileSync4(path, "utf8"));
|
|
1519
|
+
}
|
|
1520
|
+
function safeReadText(path) {
|
|
1521
|
+
return existsSync7(path) ? readText(path) : null;
|
|
1522
|
+
}
|
|
1523
|
+
function ensureParent(path) {
|
|
1524
|
+
mkdirSync5(dirname6(path), { recursive: true });
|
|
1525
|
+
}
|
|
1526
|
+
function writeText(path, content) {
|
|
1527
|
+
ensureParent(path);
|
|
1528
|
+
writeFileSync4(path, content);
|
|
1529
|
+
}
|
|
1530
|
+
function tryParseJson(text3) {
|
|
1531
|
+
if (!text3) return null;
|
|
1532
|
+
try {
|
|
1533
|
+
return JSON.parse(text3);
|
|
1534
|
+
} catch {
|
|
1535
|
+
return null;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function slugifyRepoName(name) {
|
|
1539
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
1540
|
+
}
|
|
1541
|
+
function titleCaseSlug(slug) {
|
|
1542
|
+
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1543
|
+
}
|
|
1544
|
+
function readSymlinkTarget(path) {
|
|
1545
|
+
if (!existsSync7(path)) return null;
|
|
1546
|
+
try {
|
|
1547
|
+
return readlinkSync(path);
|
|
1548
|
+
} catch {
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
function ensureSymlink(path, target, dryRun) {
|
|
1553
|
+
if (existsSync7(path)) {
|
|
1554
|
+
const stat = lstatSync(path);
|
|
1555
|
+
if (stat.isSymbolicLink()) {
|
|
1556
|
+
const current = readSymlinkTarget(path);
|
|
1557
|
+
if (current === target) return { changed: false };
|
|
1558
|
+
if (!dryRun) {
|
|
1559
|
+
unlinkSync3(path);
|
|
1560
|
+
symlinkSync(target, path);
|
|
1561
|
+
}
|
|
1562
|
+
return { changed: true };
|
|
1563
|
+
}
|
|
1564
|
+
return { changed: false, blocked: `${relative(process.cwd(), path) || path} exists and is not a symlink` };
|
|
1565
|
+
}
|
|
1566
|
+
if (!dryRun) symlinkSync(target, path);
|
|
1567
|
+
return { changed: true };
|
|
1568
|
+
}
|
|
1569
|
+
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
1570
|
+
const agentsPath = join9(repoRoot, "AGENTS.md");
|
|
1571
|
+
if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
|
|
1572
|
+
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1573
|
+
const source = join9(repoRoot, file);
|
|
1574
|
+
if (!existsSync7(source)) continue;
|
|
1575
|
+
const stat = lstatSync(source);
|
|
1576
|
+
if (stat.isSymbolicLink()) continue;
|
|
1577
|
+
if (stat.isFile()) {
|
|
1578
|
+
if (!dryRun) renameSync(source, agentsPath);
|
|
1579
|
+
return { changedFiles: [agentsPath], details: [`Moved ${file} to AGENTS.md before wiring agent-file symlinks`] };
|
|
1580
|
+
}
|
|
1581
|
+
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
1582
|
+
}
|
|
1583
|
+
const readmePath = join9(repoRoot, "README.md");
|
|
1584
|
+
if (existsSync7(readmePath)) {
|
|
1585
|
+
const stat = lstatSync(readmePath);
|
|
1586
|
+
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
1587
|
+
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
1588
|
+
return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
|
|
1589
|
+
}
|
|
1590
|
+
return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
|
|
1591
|
+
}
|
|
1592
|
+
function yamlGet(text3, keyPath) {
|
|
1593
|
+
const parts = keyPath.split(".");
|
|
1594
|
+
const lines = text3.split("\n");
|
|
1595
|
+
let start = 0;
|
|
1596
|
+
let indent = 0;
|
|
1597
|
+
for (let idx = 0; idx < parts.length; idx += 1) {
|
|
1598
|
+
const key = parts[idx];
|
|
1599
|
+
let found = false;
|
|
1600
|
+
for (let i = start; i < lines.length; i += 1) {
|
|
1601
|
+
const line = lines[i];
|
|
1602
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
1603
|
+
const match = line.match(/^(\s*)([^:#]+):\s*(.*)$/);
|
|
1604
|
+
if (!match) continue;
|
|
1605
|
+
const currentIndent = match[1].length;
|
|
1606
|
+
const currentKey = match[2].trim();
|
|
1607
|
+
const rest = match[3].trim();
|
|
1608
|
+
if (idx > 0 && currentIndent < indent) break;
|
|
1609
|
+
if (currentIndent !== indent || currentKey !== key) continue;
|
|
1610
|
+
found = true;
|
|
1611
|
+
if (idx === parts.length - 1) {
|
|
1612
|
+
return rest.replace(/^['"]|['"]$/g, "").trim();
|
|
1613
|
+
}
|
|
1614
|
+
start = i + 1;
|
|
1615
|
+
indent = currentIndent + 2;
|
|
1616
|
+
break;
|
|
1617
|
+
}
|
|
1618
|
+
if (!found) return "";
|
|
1619
|
+
}
|
|
1620
|
+
return "";
|
|
1621
|
+
}
|
|
1622
|
+
function discoverRoles(repoRoot) {
|
|
1623
|
+
const rolesDir = join9(repoRoot, "agents", "hermes");
|
|
1624
|
+
if (!existsSync7(rolesDir)) return [];
|
|
1625
|
+
return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
1626
|
+
const roleDir = join9(rolesDir, entry.name);
|
|
1627
|
+
const roleYamlPath = join9(roleDir, "role.yaml");
|
|
1628
|
+
if (!existsSync7(roleYamlPath)) return null;
|
|
1629
|
+
const text3 = readText(roleYamlPath);
|
|
1630
|
+
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
1631
|
+
return {
|
|
1632
|
+
role: yamlGet(text3, "role") || entry.name,
|
|
1633
|
+
roleDir,
|
|
1634
|
+
roleYamlPath,
|
|
1635
|
+
repo: yamlGet(text3, "repo"),
|
|
1636
|
+
agentId: yamlGet(text3, "agent_id"),
|
|
1637
|
+
profileName: yamlGet(text3, "profile") || yamlGet(text3, "agent_id"),
|
|
1638
|
+
displayName: yamlGet(text3, "display_name"),
|
|
1639
|
+
purpose: yamlGet(text3, "purpose"),
|
|
1640
|
+
botHandle: yamlGet(text3, "telegram.bot_username"),
|
|
1641
|
+
runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
|
|
1642
|
+
runtimeOwner: yamlGet(text3, "runtime.github_owner"),
|
|
1643
|
+
planeWorkspace: yamlGet(text3, "ticket_provider.workspace") || yamlGet(text3, "plane.workspace"),
|
|
1644
|
+
ticketProviderName: yamlGet(text3, "ticket_provider.name"),
|
|
1645
|
+
ticketProviderBoardId: yamlGet(text3, "ticket_provider.board_id"),
|
|
1646
|
+
ticketProviderBoardUrl: yamlGet(text3, "ticket_provider.board_url"),
|
|
1647
|
+
ticketProviderIdentifier: yamlGet(text3, "plane.identifier")
|
|
1648
|
+
};
|
|
1649
|
+
}).filter((value) => Boolean(value));
|
|
1650
|
+
}
|
|
1651
|
+
function registryPath(homeDir) {
|
|
1652
|
+
return join9(homeDir, ".hermes", "agents-registry.yaml");
|
|
1653
|
+
}
|
|
1654
|
+
function systemctlUser(args) {
|
|
1655
|
+
const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
1656
|
+
return {
|
|
1657
|
+
ok: result.status === 0,
|
|
1658
|
+
stdout: result.stdout.trim(),
|
|
1659
|
+
stderr: result.stderr.trim()
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
function templateScript(ctx, name) {
|
|
1663
|
+
const source = join9(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
1664
|
+
return existsSync7(source) ? readText(source) : void 0;
|
|
1665
|
+
}
|
|
1666
|
+
function templateVersioningScript(ctx) {
|
|
1667
|
+
return templateScript(ctx, "versioning.sh");
|
|
1668
|
+
}
|
|
1669
|
+
function templateLinkAgentfilesScript(ctx) {
|
|
1670
|
+
return templateScript(ctx, "link-agentfiles.sh");
|
|
1671
|
+
}
|
|
1672
|
+
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1673
|
+
const packageJson = join9(repoRoot, "package.json");
|
|
1674
|
+
return existsSync7(packageJson) ? "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\njson package.json\ngittag .\n" : "# mise-versioning manifest: <type> <path>\n# types: json toml cargo csproj gradle plain gittag\ngittag .\n";
|
|
1675
|
+
}
|
|
1676
|
+
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
1677
|
+
if (startMarker.test(text3)) {
|
|
1678
|
+
return text3.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
|
|
1679
|
+
}
|
|
1680
|
+
if (beforePattern) {
|
|
1681
|
+
const match = text3.match(beforePattern);
|
|
1682
|
+
if (match && typeof match.index === "number") {
|
|
1683
|
+
return `${text3.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
1684
|
+
|
|
1685
|
+
${text3.slice(match.index)}`;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return `${text3.replace(/\s*$/, "")}
|
|
1689
|
+
|
|
1690
|
+
${block}
|
|
1691
|
+
`;
|
|
1692
|
+
}
|
|
1693
|
+
var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
|
|
1694
|
+
var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/hermes"];
|
|
1695
|
+
function requiredMisePathEntries(ctx) {
|
|
1696
|
+
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
1697
|
+
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
1698
|
+
if (existsSync7(join9(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1699
|
+
}
|
|
1700
|
+
return required;
|
|
1701
|
+
}
|
|
1702
|
+
function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
|
|
1703
|
+
const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
1704
|
+
const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
|
|
1705
|
+
if (!envMatch || typeof envMatch.index !== "number") {
|
|
1706
|
+
return `[env]
|
|
1707
|
+
${render(required)}
|
|
1708
|
+
|
|
1709
|
+
${text3.replace(/^\s+/, "")}`;
|
|
1710
|
+
}
|
|
1711
|
+
const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
|
|
1712
|
+
const section = envMatch[2];
|
|
1713
|
+
const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
|
|
1714
|
+
const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
|
|
1715
|
+
if (!pathLine) {
|
|
1716
|
+
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
1717
|
+
}
|
|
1718
|
+
const current = [...pathLine[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
1719
|
+
const merged = [...current];
|
|
1720
|
+
for (const value of required) {
|
|
1721
|
+
if (!merged.includes(value)) merged.push(value);
|
|
1722
|
+
}
|
|
1723
|
+
const nextLine = render(merged);
|
|
1724
|
+
if (pathLine[0] === nextLine) return text3;
|
|
1725
|
+
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
1726
|
+
}
|
|
1727
|
+
function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
1728
|
+
const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
|
|
1729
|
+
const existing = /# This block will handle the linking of[\s\S]*?\[tasks\.link-agentfiles\][\s\S]*?run = "\{\{config_root\}\}\/\.mise\/scripts\/link-agentfiles\.sh"/;
|
|
1730
|
+
if (existing.test(withPath)) {
|
|
1731
|
+
return withPath.replace(existing, LINK_AGENTFILES_BLOCK);
|
|
1732
|
+
}
|
|
1733
|
+
const versioningIndex = withPath.indexOf("# >>> mise-versioning >>>");
|
|
1734
|
+
if (versioningIndex >= 0) {
|
|
1735
|
+
return `${withPath.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${LINK_AGENTFILES_BLOCK}
|
|
1736
|
+
|
|
1737
|
+
${withPath.slice(versioningIndex)}`;
|
|
1738
|
+
}
|
|
1739
|
+
return `${withPath.replace(/\s*$/, "")}
|
|
1740
|
+
|
|
1741
|
+
${LINK_AGENTFILES_BLOCK}
|
|
1742
|
+
`;
|
|
1743
|
+
}
|
|
1744
|
+
function readProjectJson(ctx) {
|
|
1745
|
+
return tryParseJson(safeReadText(join9(ctx.repoRoot, ".project.json")));
|
|
1746
|
+
}
|
|
1747
|
+
function canonicalProjectJson(ctx) {
|
|
1748
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
1749
|
+
const existing = readProjectJson(ctx) ?? {};
|
|
1750
|
+
const slug = String(existing.project_slug ?? slugifyRepoName(dirname6(ctx.repoRoot) === ctx.repoRoot ? ctx.repoRoot.split("/").pop() ?? "project" : ctx.repoRoot.split("/").pop() ?? "project"));
|
|
1751
|
+
const firstRole = roles[0];
|
|
1752
|
+
const ticketProvider = {
|
|
1753
|
+
type: String((existing.ticket_provider?.type ?? firstRole?.ticketProviderName ?? "plane") || "plane"),
|
|
1754
|
+
workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
|
|
1755
|
+
identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
|
|
1756
|
+
board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
|
|
1757
|
+
board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || "")
|
|
1758
|
+
};
|
|
1759
|
+
return {
|
|
1760
|
+
project_name: String(existing.project_name ?? titleCaseSlug(slug)),
|
|
1761
|
+
project_description: String(existing.project_description ?? ""),
|
|
1762
|
+
project_slug: slug,
|
|
1763
|
+
repo_path: ctx.repoRoot,
|
|
1764
|
+
ticket_provider: ticketProvider,
|
|
1765
|
+
agents: Object.fromEntries(
|
|
1766
|
+
roles.map((role) => [
|
|
1767
|
+
role.agentId || `${slug}-${role.role}`,
|
|
1768
|
+
{
|
|
1769
|
+
role: role.role,
|
|
1770
|
+
role_dir: relative(ctx.repoRoot, role.roleDir)
|
|
1771
|
+
}
|
|
1772
|
+
])
|
|
1773
|
+
)
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function projectJsonFinding(ctx) {
|
|
1777
|
+
const projectPath = join9(ctx.repoRoot, ".project.json");
|
|
1778
|
+
const planeJsonPath = join9(ctx.repoRoot, ".plane.json");
|
|
1779
|
+
const details = [];
|
|
1780
|
+
const data = readProjectJson(ctx);
|
|
1781
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
1782
|
+
if (!existsSync7(projectPath)) {
|
|
1783
|
+
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
1784
|
+
}
|
|
1785
|
+
if (!data) {
|
|
1786
|
+
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json is not valid JSON", details: [], fixable: true };
|
|
1787
|
+
}
|
|
1788
|
+
for (const key of ["project_name", "project_description", "project_slug", "repo_path", "ticket_provider", "agents"]) {
|
|
1789
|
+
if (!(key in data)) details.push(`missing key: ${key}`);
|
|
1790
|
+
}
|
|
1791
|
+
if (data.repo_path !== ctx.repoRoot) details.push(`repo_path should be ${ctx.repoRoot}`);
|
|
1792
|
+
const agents = data.agents ?? {};
|
|
1793
|
+
for (const role of roles) {
|
|
1794
|
+
const agent = agents[role.agentId];
|
|
1795
|
+
if (!agent) {
|
|
1796
|
+
details.push(`agents.${role.agentId} missing`);
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
if (agent.role !== role.role) details.push(`agents.${role.agentId}.role should be ${role.role}`);
|
|
1800
|
+
if (agent.role_dir !== relative(ctx.repoRoot, role.roleDir)) {
|
|
1801
|
+
details.push(`agents.${role.agentId}.role_dir should be ${relative(ctx.repoRoot, role.roleDir)}`);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
const ticketProvider = data.ticket_provider ?? {};
|
|
1805
|
+
for (const key of ["type", "workspace", "identifier", "board_id", "board_url"]) {
|
|
1806
|
+
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
1807
|
+
}
|
|
1808
|
+
if (existsSync7(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
1809
|
+
return {
|
|
1810
|
+
id: "sot.project-json",
|
|
1811
|
+
title: "Canonical .project.json",
|
|
1812
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
1813
|
+
summary: details.length === 0 ? ".project.json matches canonical parity contract" : `${details.length} parity issue(s) detected`,
|
|
1814
|
+
details,
|
|
1815
|
+
fixable: true
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
function renderSoul(role) {
|
|
1819
|
+
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
1820
|
+
const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` : "Direct and brief.";
|
|
1821
|
+
const roleSpecific = role.role === "pm" ? `You are the project manager. You triage incoming work, create or refine tickets, and delegate implementation. You do not ship product code. A systemd heartbeat checkpoints your runtime; when this repo opts into reconciliation (\`reconcile.enabled\` in role.yaml), the same heartbeat also runs your continuous board-reconciliation pass out-of-band (\`.scripts/sentinel.prompt.md\`, \`--source cron\`), kept separate from your interactive session memory.` : `You operate as the ${role.role} agent for this repo.`;
|
|
1822
|
+
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
1823
|
+
return `# ${role.displayName || role.agentId}
|
|
1824
|
+
|
|
1825
|
+
You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisioned to work inside the
|
|
1826
|
+
\`${role.repo}\` repository.
|
|
1827
|
+
|
|
1828
|
+
## Identity
|
|
1829
|
+
|
|
1830
|
+
| | |
|
|
1831
|
+
| --- | --- |
|
|
1832
|
+
| Agent ID | \`${role.agentId}\` |
|
|
1833
|
+
| Profile | \`${role.profileName || role.agentId}\` |
|
|
1834
|
+
| Repo | \`${role.repo}\` |
|
|
1835
|
+
| Role | \`${role.role}\` |
|
|
1836
|
+
| Telegram | \`${telegram}\` |
|
|
1837
|
+
| Purpose | ${role.purpose || `${role.role} agent for ${role.repo}`} |
|
|
1838
|
+
|
|
1839
|
+
## Scope
|
|
1840
|
+
|
|
1841
|
+
You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME is the runtime submodule at \`./runtime/\` (repo \`${runtimeOwner}/${role.runtimeRepo}\`), which \`~/.hermes/profiles/${role.profileName || role.agentId}\` symlinks to (so \`--profile\` invocations resolve here too); Hermes loads its \`config.yaml\` directly. Secrets, SOUL, memories, skills, sessions, gateway state, and runtime files all live local to that runtime.
|
|
1842
|
+
|
|
1843
|
+
## Tone
|
|
1844
|
+
|
|
1845
|
+
${tone}
|
|
1846
|
+
|
|
1847
|
+
## Role-specific behavior
|
|
1848
|
+
|
|
1849
|
+
${roleSpecific}
|
|
1850
|
+
|
|
1851
|
+
## Memory hygiene
|
|
1852
|
+
|
|
1853
|
+
Your memory is the submodule at \`./runtime/memories/\`. Use durable memory deliberately and keep \`memories/MEMORY.md\` current.
|
|
1854
|
+
`;
|
|
1855
|
+
}
|
|
1856
|
+
function renderHermesWrapper(role) {
|
|
1857
|
+
return `#!/usr/bin/env bash
|
|
1858
|
+
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime submodule.
|
|
1859
|
+
|
|
1860
|
+
set -euo pipefail
|
|
1861
|
+
|
|
1862
|
+
ROLE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
1863
|
+
RUNTIME_HOME="$ROLE_DIR/runtime"
|
|
1864
|
+
|
|
1865
|
+
FLEET_ENV="{HERMES_FLEET_ENV:-$HOME/.hermes/fleet.env}"
|
|
1866
|
+
if [[ -f "$FLEET_ENV" ]]; then
|
|
1867
|
+
# shellcheck disable=SC1090
|
|
1868
|
+
source "$FLEET_ENV"
|
|
1869
|
+
fi
|
|
1870
|
+
|
|
1871
|
+
HERMES_BIN="{HERMES_BIN:-{HERMES_FLEET_BIN:-/home/delorenj/code/hermes-agent/.venv/bin/hermes}}"
|
|
1872
|
+
HERMES_OAUTH_FILE="{HERMES_OAUTH_FILE:-{HERMES_FLEET_OAUTH_FILE:-$HOME/.hermes/auth.json}}"
|
|
1873
|
+
CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
|
|
1874
|
+
|
|
1875
|
+
FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
1876
|
+
PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
|
|
1877
|
+
HERMES_HOME="$RUNTIME_HOME"
|
|
1878
|
+
|
|
1879
|
+
if [[ ! -d "$RUNTIME_HOME" ]]; then
|
|
1880
|
+
echo "hermes: runtime submodule not initialized at $RUNTIME_HOME" >&2
|
|
1881
|
+
echo " fix: git submodule update --init --recursive" >&2
|
|
1882
|
+
exit 1
|
|
1883
|
+
fi
|
|
1884
|
+
|
|
1885
|
+
exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH_FILE="$HERMES_OAUTH_FILE" CODEX_HOME="$CODEX_HOME" "$HERMES_BIN" "$@"
|
|
1886
|
+
`.replace(/\u0010/g, "$");
|
|
1887
|
+
}
|
|
1888
|
+
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
1889
|
+
if (!existsSync7(sourceDir)) return;
|
|
1890
|
+
mkdirSync5(targetDir, { recursive: true });
|
|
1891
|
+
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
1892
|
+
const sourcePath = join9(sourceDir, entry.name);
|
|
1893
|
+
if (skip?.(sourcePath)) continue;
|
|
1894
|
+
const targetPath = join9(targetDir, entry.name);
|
|
1895
|
+
if (entry.isDirectory()) {
|
|
1896
|
+
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
if (existsSync7(targetPath)) continue;
|
|
1900
|
+
changedFiles.push(targetPath);
|
|
1901
|
+
if (!dryRun) {
|
|
1902
|
+
ensureParent(targetPath);
|
|
1903
|
+
copyFileSync(sourcePath, targetPath);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
1908
|
+
const gitmodulesPath = join9(repoRoot, ".gitmodules");
|
|
1909
|
+
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
1910
|
+
const owner = role.runtimeOwner || "delorenj";
|
|
1911
|
+
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
1912
|
+
path = agents/hermes/${role.role}/runtime
|
|
1913
|
+
url = git@github.com:${owner}/${repoName}.git
|
|
1914
|
+
`;
|
|
1915
|
+
const current = safeReadText(gitmodulesPath) ?? "";
|
|
1916
|
+
const header = `[submodule "agents/hermes/${role.role}/runtime"]`;
|
|
1917
|
+
if (current.includes(header)) return [];
|
|
1918
|
+
changedFiles.push(gitmodulesPath);
|
|
1919
|
+
if (!dryRun) writeText(gitmodulesPath, `${current.replace(/\s*$/, "")}${current.trim() ? "\n" : ""}${block}`);
|
|
1920
|
+
return [gitmodulesPath];
|
|
1921
|
+
}
|
|
1922
|
+
function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
1923
|
+
const path = registryPath(homeDir);
|
|
1924
|
+
const current = safeReadText(path) ?? "# Hermes agent fleet registry.\n# One entry per provisioned agent. Managed by hermes-agent-template/.scripts/80-registry.sh.\nschema_version: 1\nagents: {}\n";
|
|
1925
|
+
if (current.includes(`${role.agentId}:`)) return null;
|
|
1926
|
+
const block = ` ${role.agentId}:
|
|
1927
|
+
repo: ${role.repo}
|
|
1928
|
+
role: ${role.role}
|
|
1929
|
+
display_name: ${JSON.stringify(role.displayName || role.agentId)}
|
|
1930
|
+
project_path: ${ctxEscape(role.roleDir ? dirname6(dirname6(dirname6(role.roleDir))) : "")}
|
|
1931
|
+
role_dir: ${ctxEscape(role.roleDir)}
|
|
1932
|
+
profile_name: ${role.profileName || role.agentId}
|
|
1933
|
+
telegram:
|
|
1934
|
+
bot_username: ${ctxEscape(role.botHandle)}
|
|
1935
|
+
plane:
|
|
1936
|
+
workspace: ${ctxEscape(role.planeWorkspace)}
|
|
1937
|
+
project_id: ${ctxEscape(role.ticketProviderBoardId)}
|
|
1938
|
+
identifier: ${ctxEscape(role.ticketProviderIdentifier)}
|
|
1939
|
+
runtime_repo: ${ctxEscape(role.runtimeRepo)}
|
|
1940
|
+
systemd:
|
|
1941
|
+
gateway_unit: hermes-${role.agentId}-gateway.service
|
|
1942
|
+
consumer_unit: hermes-${role.agentId}-consumer.service
|
|
1943
|
+
heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
|
|
1944
|
+
`;
|
|
1945
|
+
const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
|
|
1946
|
+
${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
|
|
1947
|
+
changedFiles.push(path);
|
|
1948
|
+
if (!dryRun) writeText(path, next);
|
|
1949
|
+
return path;
|
|
1950
|
+
}
|
|
1951
|
+
function profileMetaInheritsDefault(path) {
|
|
1952
|
+
const text3 = safeReadText(path);
|
|
1953
|
+
return Boolean(
|
|
1954
|
+
text3 && /^config:\s*$/m.test(text3) && /^\s+inherit_from:\s*default\s*$/m.test(text3) && /^\s+save_mode:\s*delta\s*$/m.test(text3)
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
|
|
1958
|
+
const current = safeReadText(path) ?? "";
|
|
1959
|
+
const lines = current.split("\n");
|
|
1960
|
+
let next;
|
|
1961
|
+
const start = lines.findIndex((line) => /^config:\s*$/.test(line));
|
|
1962
|
+
if (!current.trim()) {
|
|
1963
|
+
next = "config:\n inherit_from: default\n save_mode: delta\n";
|
|
1964
|
+
} else if (start === -1) {
|
|
1965
|
+
next = `${current.replace(/\s*$/, "\n")}config:
|
|
1966
|
+
inherit_from: default
|
|
1967
|
+
save_mode: delta
|
|
1968
|
+
`;
|
|
1969
|
+
} else {
|
|
1970
|
+
let end = start + 1;
|
|
1971
|
+
while (end < lines.length && !/^[^#\s][^:]*:\s*/.test(lines[end] ?? "")) end++;
|
|
1972
|
+
let hasInherit = false;
|
|
1973
|
+
let hasSave = false;
|
|
1974
|
+
for (let idx = start + 1; idx < end; idx++) {
|
|
1975
|
+
if (/^\s+inherit_from:\s*/.test(lines[idx] ?? "")) {
|
|
1976
|
+
lines[idx] = " inherit_from: default";
|
|
1977
|
+
hasInherit = true;
|
|
1978
|
+
} else if (/^\s+save_mode:\s*/.test(lines[idx] ?? "")) {
|
|
1979
|
+
lines[idx] = " save_mode: delta";
|
|
1980
|
+
hasSave = true;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const inserts = [];
|
|
1984
|
+
if (!hasInherit) inserts.push(" inherit_from: default");
|
|
1985
|
+
if (!hasSave) inserts.push(" save_mode: delta");
|
|
1986
|
+
if (inserts.length) lines.splice(end, 0, ...inserts);
|
|
1987
|
+
next = lines.join("\n");
|
|
1988
|
+
if (!next.endsWith("\n")) next += "\n";
|
|
1989
|
+
}
|
|
1990
|
+
if (next === current) return null;
|
|
1991
|
+
changedFiles.push(path);
|
|
1992
|
+
if (!dryRun) writeText(path, next);
|
|
1993
|
+
return path;
|
|
1994
|
+
}
|
|
1995
|
+
function ctxEscape(value) {
|
|
1996
|
+
return JSON.stringify(value || "");
|
|
1997
|
+
}
|
|
1998
|
+
function checkUnit(unit) {
|
|
1999
|
+
const enabled = systemctlUser(["is-enabled", unit]).ok;
|
|
2000
|
+
const active = systemctlUser(["is-active", unit]).ok;
|
|
2001
|
+
return { enabled, active };
|
|
2002
|
+
}
|
|
2003
|
+
var RULES = [
|
|
2004
|
+
{
|
|
2005
|
+
id: "mise.config-root",
|
|
2006
|
+
title: "mise config_root + AGENTS link hooks",
|
|
2007
|
+
audit: (ctx) => {
|
|
2008
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2009
|
+
if (!existsSync7(misePath)) {
|
|
2010
|
+
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
2011
|
+
}
|
|
2012
|
+
const text3 = readText(misePath);
|
|
2013
|
+
const details = [];
|
|
2014
|
+
const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2015
|
+
if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2016
|
+
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2017
|
+
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2018
|
+
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
2019
|
+
if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
|
|
2020
|
+
if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
|
|
2021
|
+
if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
|
|
2022
|
+
if (!text3.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
|
|
2023
|
+
return {
|
|
2024
|
+
id: "mise.config-root",
|
|
2025
|
+
title: "mise config_root + AGENTS link hooks",
|
|
2026
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
2027
|
+
summary: details.length === 0 ? "mise AGENTS-linking parity verified" : `${details.length} issue(s) detected in mise AGENTS-linking contract`,
|
|
2028
|
+
details,
|
|
2029
|
+
fixable: true
|
|
2030
|
+
};
|
|
2031
|
+
},
|
|
2032
|
+
migrate: (ctx, finding) => {
|
|
2033
|
+
const path = join9(ctx.repoRoot, "mise.toml");
|
|
2034
|
+
const changedFiles = [];
|
|
2035
|
+
if (!existsSync7(path)) {
|
|
2036
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; initialize mise first", changedFiles, details: [] };
|
|
2037
|
+
}
|
|
2038
|
+
let text3 = readText(path);
|
|
2039
|
+
const next = upsertLinkAgentfilesBlock(text3, ctx);
|
|
2040
|
+
if (next !== text3) {
|
|
2041
|
+
changedFiles.push(path);
|
|
2042
|
+
if (!ctx.dryRun) writeText(path, next);
|
|
2043
|
+
text3 = next;
|
|
2044
|
+
}
|
|
2045
|
+
const linkAgentfilesPath = join9(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2046
|
+
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2047
|
+
if (expectedScript === void 0) {
|
|
2048
|
+
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: [] };
|
|
2049
|
+
}
|
|
2050
|
+
if (safeReadText(linkAgentfilesPath) !== expectedScript) {
|
|
2051
|
+
changedFiles.push(linkAgentfilesPath);
|
|
2052
|
+
if (!ctx.dryRun) {
|
|
2053
|
+
writeText(linkAgentfilesPath, expectedScript);
|
|
2054
|
+
chmodSync(linkAgentfilesPath, 493);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
return {
|
|
2058
|
+
id: finding.id,
|
|
2059
|
+
title: finding.title,
|
|
2060
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
2061
|
+
summary: changedFiles.length ? "Updated mise AGENTS-linking contract" : "No changes required",
|
|
2062
|
+
changedFiles,
|
|
2063
|
+
details: changedFiles.length ? ["Normalized hooks/watch_files/tasks.link-agentfiles block and script"] : []
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
},
|
|
2067
|
+
{
|
|
2068
|
+
id: "mise.versioning",
|
|
2069
|
+
title: "managed mise versioning block",
|
|
2070
|
+
audit: (ctx) => {
|
|
2071
|
+
const details = [];
|
|
2072
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2073
|
+
const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2074
|
+
const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2075
|
+
const text3 = safeReadText(misePath);
|
|
2076
|
+
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2077
|
+
if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2078
|
+
if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
2079
|
+
return {
|
|
2080
|
+
id: "mise.versioning",
|
|
2081
|
+
title: "managed mise versioning block",
|
|
2082
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
2083
|
+
summary: details.length === 0 ? "mise versioning parity verified" : `${details.length} versioning issue(s) detected`,
|
|
2084
|
+
details,
|
|
2085
|
+
fixable: true
|
|
2086
|
+
};
|
|
2087
|
+
},
|
|
2088
|
+
migrate: (ctx, finding) => {
|
|
2089
|
+
const changedFiles = [];
|
|
2090
|
+
const misePath = join9(ctx.repoRoot, "mise.toml");
|
|
2091
|
+
if (!existsSync7(misePath)) {
|
|
2092
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; cannot inject versioning block", changedFiles, details: [] };
|
|
2093
|
+
}
|
|
2094
|
+
const currentMise = readText(misePath);
|
|
2095
|
+
const nextMise = replaceOrAppendManagedBlock(currentMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
2096
|
+
if (nextMise !== currentMise) {
|
|
2097
|
+
changedFiles.push(misePath);
|
|
2098
|
+
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
2099
|
+
}
|
|
2100
|
+
const versioningPath = join9(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2101
|
+
const expectedScript = templateVersioningScript(ctx);
|
|
2102
|
+
if (expectedScript === void 0) {
|
|
2103
|
+
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: [] };
|
|
2104
|
+
}
|
|
2105
|
+
if (safeReadText(versioningPath) !== expectedScript) {
|
|
2106
|
+
changedFiles.push(versioningPath);
|
|
2107
|
+
if (!ctx.dryRun) {
|
|
2108
|
+
writeText(versioningPath, expectedScript);
|
|
2109
|
+
chmodSync(versioningPath, 493);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
const manifestPath = join9(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2113
|
+
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
2114
|
+
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
2115
|
+
changedFiles.push(manifestPath);
|
|
2116
|
+
if (!ctx.dryRun) writeText(manifestPath, expectedManifest);
|
|
2117
|
+
}
|
|
2118
|
+
return {
|
|
2119
|
+
id: finding.id,
|
|
2120
|
+
title: finding.title,
|
|
2121
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
2122
|
+
summary: changedFiles.length ? "Versioning block/script/manifest normalized" : "No changes required",
|
|
2123
|
+
changedFiles,
|
|
2124
|
+
details: []
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
{
|
|
2129
|
+
id: "sot.agent-symlinks",
|
|
2130
|
+
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2131
|
+
audit: (ctx) => {
|
|
2132
|
+
const agentsPath = join9(ctx.repoRoot, "AGENTS.md");
|
|
2133
|
+
if (!existsSync7(agentsPath)) {
|
|
2134
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join9(ctx.repoRoot, file)));
|
|
2135
|
+
if (fallbackSources.length === 0) {
|
|
2136
|
+
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2137
|
+
}
|
|
2138
|
+
return {
|
|
2139
|
+
id: "sot.agent-symlinks",
|
|
2140
|
+
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2141
|
+
status: "fail",
|
|
2142
|
+
summary: "AGENTS.md missing but can be derived from existing project documentation",
|
|
2143
|
+
details: [`AGENTS.md can be created from ${fallbackSources[0]}`],
|
|
2144
|
+
fixable: true
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
const details = [];
|
|
2148
|
+
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2149
|
+
const full = join9(ctx.repoRoot, file);
|
|
2150
|
+
const target = readSymlinkTarget(full);
|
|
2151
|
+
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
2152
|
+
}
|
|
2153
|
+
return {
|
|
2154
|
+
id: "sot.agent-symlinks",
|
|
2155
|
+
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2156
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
2157
|
+
summary: details.length === 0 ? "Agent documentation symlinks are in parity" : `${details.length} symlink issue(s) detected`,
|
|
2158
|
+
details,
|
|
2159
|
+
fixable: true
|
|
2160
|
+
};
|
|
2161
|
+
},
|
|
2162
|
+
migrate: (ctx, finding) => {
|
|
2163
|
+
const changedFiles = [];
|
|
2164
|
+
const details = [];
|
|
2165
|
+
const blockedDetails = [];
|
|
2166
|
+
const bootstrap = bootstrapAgentsFile(ctx.repoRoot, ctx.dryRun);
|
|
2167
|
+
changedFiles.push(...bootstrap.changedFiles);
|
|
2168
|
+
details.push(...bootstrap.details);
|
|
2169
|
+
if (bootstrap.blocked) {
|
|
2170
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
2171
|
+
}
|
|
2172
|
+
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
2173
|
+
const full = join9(ctx.repoRoot, file);
|
|
2174
|
+
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
2175
|
+
if (result.blocked) blockedDetails.push(result.blocked);
|
|
2176
|
+
if (result.changed) changedFiles.push(full);
|
|
2177
|
+
}
|
|
2178
|
+
return {
|
|
2179
|
+
id: finding.id,
|
|
2180
|
+
title: finding.title,
|
|
2181
|
+
status: blockedDetails.length ? "blocked" : changedFiles.length ? "applied" : "noop",
|
|
2182
|
+
summary: blockedDetails.length ? "One or more files could not be replaced safely" : changedFiles.length ? "Symlink contract repaired" : "No changes required",
|
|
2183
|
+
changedFiles,
|
|
2184
|
+
details: [...details, ...blockedDetails]
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
},
|
|
2188
|
+
{
|
|
2189
|
+
id: "sot.project-json",
|
|
2190
|
+
title: "Canonical .project.json",
|
|
2191
|
+
audit: projectJsonFinding,
|
|
2192
|
+
migrate: (ctx, finding) => {
|
|
2193
|
+
const changedFiles = [];
|
|
2194
|
+
const details = [];
|
|
2195
|
+
const path = join9(ctx.repoRoot, ".project.json");
|
|
2196
|
+
const existing = readProjectJson(ctx) ?? {};
|
|
2197
|
+
const canonical = canonicalProjectJson(ctx);
|
|
2198
|
+
const merged = { ...existing, ...canonical };
|
|
2199
|
+
const expected = `${JSON.stringify(merged, null, 2)}
|
|
2200
|
+
`;
|
|
2201
|
+
if (safeReadText(path) !== expected) {
|
|
2202
|
+
changedFiles.push(path);
|
|
2203
|
+
if (!ctx.dryRun) writeText(path, expected);
|
|
2204
|
+
}
|
|
2205
|
+
const planeJson = join9(ctx.repoRoot, ".plane.json");
|
|
2206
|
+
if (existsSync7(planeJson)) {
|
|
2207
|
+
const backup = `${planeJson}.migrated-backup`;
|
|
2208
|
+
if (existsSync7(backup)) {
|
|
2209
|
+
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
2210
|
+
} else {
|
|
2211
|
+
changedFiles.push(backup);
|
|
2212
|
+
if (!ctx.dryRun) renameSync(planeJson, backup);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return {
|
|
2216
|
+
id: finding.id,
|
|
2217
|
+
title: finding.title,
|
|
2218
|
+
status: details.length ? "blocked" : changedFiles.length ? "applied" : "noop",
|
|
2219
|
+
summary: details.length ? "Project SOT partially blocked" : changedFiles.length ? "Canonical .project.json written" : "No changes required",
|
|
2220
|
+
changedFiles,
|
|
2221
|
+
details
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
},
|
|
2225
|
+
{
|
|
2226
|
+
id: "secrets.env-op",
|
|
2227
|
+
title: ".env.op + gitignore secrets contract",
|
|
2228
|
+
audit: (ctx) => {
|
|
2229
|
+
const details = [];
|
|
2230
|
+
const envOp = safeReadText(join9(ctx.repoRoot, ".env.op"));
|
|
2231
|
+
const gitignore = safeReadText(join9(ctx.repoRoot, ".gitignore"));
|
|
2232
|
+
if (!envOp) {
|
|
2233
|
+
details.push(".env.op missing");
|
|
2234
|
+
} else {
|
|
2235
|
+
const invalidLines = envOp.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).filter((line) => {
|
|
2236
|
+
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
2237
|
+
return !value.startsWith("op://") && !/^https?:\/\//.test(value) && !/^[A-Za-z0-9_.:-]+$/.test(value);
|
|
2238
|
+
});
|
|
2239
|
+
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals: ${invalidLines.join(", ")}`);
|
|
2240
|
+
}
|
|
2241
|
+
if (!gitignore?.includes(".env\n") && !gitignore?.includes(".env\r\n")) details.push(".gitignore should ignore .env");
|
|
2242
|
+
if (!gitignore?.includes(".env.*")) details.push(".gitignore should ignore .env.*");
|
|
2243
|
+
if (!gitignore?.includes("!.env.op")) details.push(".gitignore should unignore .env.op");
|
|
2244
|
+
return {
|
|
2245
|
+
id: "secrets.env-op",
|
|
2246
|
+
title: ".env.op + gitignore secrets contract",
|
|
2247
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
2248
|
+
summary: details.length === 0 ? "Secret reference file and ignore rules are in parity" : `${details.length} env parity issue(s) detected`,
|
|
2249
|
+
details,
|
|
2250
|
+
fixable: true
|
|
2251
|
+
};
|
|
2252
|
+
},
|
|
2253
|
+
migrate: (ctx, finding) => {
|
|
2254
|
+
const changedFiles = [];
|
|
2255
|
+
const details = [];
|
|
2256
|
+
const envOpPath = join9(ctx.repoRoot, ".env.op");
|
|
2257
|
+
if (!existsSync7(envOpPath)) {
|
|
2258
|
+
changedFiles.push(envOpPath);
|
|
2259
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join9(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
2260
|
+
}
|
|
2261
|
+
const gitignorePath = join9(ctx.repoRoot, ".gitignore");
|
|
2262
|
+
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
2263
|
+
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
2264
|
+
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
2265
|
+
.env
|
|
2266
|
+
.env.*
|
|
2267
|
+
!.env.op
|
|
2268
|
+
`;
|
|
2269
|
+
if (!gitignore.includes("!.env.op") || !gitignore.includes(".env.*")) {
|
|
2270
|
+
changedFiles.push(gitignorePath);
|
|
2271
|
+
if (!ctx.dryRun) writeText(gitignorePath, `${gitignore.replace(/\s*$/, "")}${gitignore.trim() ? "\n\n" : ""}${requiredBlock}`);
|
|
2272
|
+
}
|
|
2273
|
+
return {
|
|
2274
|
+
id: finding.id,
|
|
2275
|
+
title: finding.title,
|
|
2276
|
+
status: details.length ? "blocked" : changedFiles.length ? "applied" : "noop",
|
|
2277
|
+
summary: details.length ? "Manual cleanup still required" : changedFiles.length ? "Wrote .env.op/gitignore parity files" : "No changes required",
|
|
2278
|
+
changedFiles,
|
|
2279
|
+
details
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
id: "provenance.copier",
|
|
2285
|
+
title: ".copier-answers.yml provenance + drift report",
|
|
2286
|
+
audit: (ctx) => {
|
|
2287
|
+
const details = [];
|
|
2288
|
+
const path = join9(ctx.repoRoot, ".copier-answers.yml");
|
|
2289
|
+
const text3 = safeReadText(path);
|
|
2290
|
+
const project = readProjectJson(ctx);
|
|
2291
|
+
if (!text3) {
|
|
2292
|
+
details.push(".copier-answers.yml missing");
|
|
2293
|
+
} else {
|
|
2294
|
+
if (!text3.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
|
|
2295
|
+
if (!text3.includes("_src_path:")) details.push("_src_path missing");
|
|
2296
|
+
if (project?.project_name) {
|
|
2297
|
+
const nameMatch = text3.match(/project_name:\s*(.+)/);
|
|
2298
|
+
if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
|
|
2299
|
+
}
|
|
2300
|
+
if (project?.project_description) {
|
|
2301
|
+
const descMatch = text3.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
|
|
2302
|
+
const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
|
|
2303
|
+
if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
return {
|
|
2307
|
+
id: "provenance.copier",
|
|
2308
|
+
title: ".copier-answers.yml provenance + drift report",
|
|
2309
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
2310
|
+
summary: details.length === 0 ? "Copier provenance is in parity" : `${details.length} provenance issue(s) detected`,
|
|
2311
|
+
details,
|
|
2312
|
+
fixable: true
|
|
2313
|
+
};
|
|
2314
|
+
},
|
|
2315
|
+
migrate: (ctx, finding) => {
|
|
2316
|
+
const changedFiles = [];
|
|
2317
|
+
const project = canonicalProjectJson(ctx);
|
|
2318
|
+
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2319
|
+
_src_path: ${join9(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2320
|
+
project_description: ${String(project.project_description)}
|
|
2321
|
+
project_name: ${String(project.project_name)}
|
|
2322
|
+
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2323
|
+
`;
|
|
2324
|
+
const path = join9(ctx.repoRoot, ".copier-answers.yml");
|
|
2325
|
+
if (safeReadText(path) !== text3) {
|
|
2326
|
+
changedFiles.push(path);
|
|
2327
|
+
if (!ctx.dryRun) writeText(path, text3);
|
|
2328
|
+
}
|
|
2329
|
+
return {
|
|
2330
|
+
id: finding.id,
|
|
2331
|
+
title: finding.title,
|
|
2332
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
2333
|
+
summary: changedFiles.length ? "Copier provenance file refreshed" : "No changes required",
|
|
2334
|
+
changedFiles,
|
|
2335
|
+
details: []
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
},
|
|
2339
|
+
{
|
|
2340
|
+
id: "bmad.scaffold",
|
|
2341
|
+
title: "BMAD modules/docs scaffold",
|
|
2342
|
+
audit: (ctx) => {
|
|
2343
|
+
const sourceRoot = join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2344
|
+
const targetRoot = join9(ctx.repoRoot, "_bmad");
|
|
2345
|
+
const sentinels = [
|
|
2346
|
+
join9("core", "config.yaml"),
|
|
2347
|
+
join9("custom", "config.yaml"),
|
|
2348
|
+
join9("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
2349
|
+
join9("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
2350
|
+
];
|
|
2351
|
+
const missing = sentinels.filter((file) => existsSync7(join9(sourceRoot, file)) && !existsSync7(join9(targetRoot, file)));
|
|
2352
|
+
return {
|
|
2353
|
+
id: "bmad.scaffold",
|
|
2354
|
+
title: "BMAD modules/docs scaffold",
|
|
2355
|
+
status: missing.length === 0 ? "pass" : "fail",
|
|
2356
|
+
summary: missing.length === 0 ? "BMAD scaffold parity verified" : `${missing.length} BMAD sentinel file(s) missing`,
|
|
2357
|
+
details: missing.map((file) => `_bmad/${file}`),
|
|
2358
|
+
fixable: true
|
|
2359
|
+
};
|
|
2360
|
+
},
|
|
2361
|
+
migrate: (ctx, finding) => {
|
|
2362
|
+
const changedFiles = [];
|
|
2363
|
+
copyMissingRecursive(join9(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join9(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2364
|
+
return {
|
|
2365
|
+
id: finding.id,
|
|
2366
|
+
title: finding.title,
|
|
2367
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
2368
|
+
summary: changedFiles.length ? "Copied missing BMAD scaffold files" : "No changes required",
|
|
2369
|
+
changedFiles,
|
|
2370
|
+
details: []
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
},
|
|
2374
|
+
{
|
|
2375
|
+
id: "hermes.pm-scaffold",
|
|
2376
|
+
title: "Hermes PM scaffold parity",
|
|
2377
|
+
audit: (ctx) => {
|
|
2378
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
2379
|
+
const role = roles.find((item) => item.role === "pm");
|
|
2380
|
+
if (!role) {
|
|
2381
|
+
return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
|
|
2382
|
+
}
|
|
2383
|
+
const details = [];
|
|
2384
|
+
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".scripts/heartbeat.sh", ".scripts/checkpoint.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
|
|
2385
|
+
if (!existsSync7(join9(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join9(role.roleDir, rel))}`);
|
|
2386
|
+
}
|
|
2387
|
+
const gitmodules = safeReadText(join9(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2388
|
+
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2389
|
+
if (!profileMetaInheritsDefault(join9(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2390
|
+
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2391
|
+
}
|
|
2392
|
+
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
2393
|
+
if (!registry?.includes(`${role.agentId}:`)) details.push(`fleet registry missing ${role.agentId}`);
|
|
2394
|
+
return {
|
|
2395
|
+
id: "hermes.pm-scaffold",
|
|
2396
|
+
title: "Hermes PM scaffold parity",
|
|
2397
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
2398
|
+
summary: details.length === 0 ? "PM scaffold parity verified" : `${details.length} PM scaffold issue(s) detected`,
|
|
2399
|
+
details,
|
|
2400
|
+
fixable: true
|
|
2401
|
+
};
|
|
2402
|
+
},
|
|
2403
|
+
migrate: (ctx, finding) => {
|
|
2404
|
+
const role = discoverRoles(ctx.repoRoot).find((item) => item.role === "pm");
|
|
2405
|
+
const changedFiles = [];
|
|
2406
|
+
const details = [];
|
|
2407
|
+
if (!role) {
|
|
2408
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2409
|
+
}
|
|
2410
|
+
const templateRoleDir = join9(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
2411
|
+
writeIfDifferent(join9(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
2412
|
+
writeIfDifferent(join9(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
2413
|
+
writeIfDifferent(join9(role.roleDir, ".gitignore"), readText(join9(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
2414
|
+
copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
2415
|
+
copyMissingRecursive(join9(templateRoleDir, ".runtime-scaffold"), join9(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
2416
|
+
copyMissingRecursive(join9(templateRoleDir, ".scripts"), join9(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
2417
|
+
const promptSource = join9(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
2418
|
+
const promptTarget = join9(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
2419
|
+
if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
|
|
2420
|
+
const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo).replace(/\{\{ display_name \}\}/g, role.displayName || role.agentId);
|
|
2421
|
+
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2422
|
+
}
|
|
2423
|
+
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2424
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join9(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2425
|
+
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2426
|
+
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2427
|
+
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
2428
|
+
return {
|
|
2429
|
+
id: finding.id,
|
|
2430
|
+
title: finding.title,
|
|
2431
|
+
status: changedFiles.length ? "applied" : "noop",
|
|
2432
|
+
summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
|
|
2433
|
+
changedFiles,
|
|
2434
|
+
details
|
|
2435
|
+
};
|
|
2436
|
+
}
|
|
2437
|
+
},
|
|
2438
|
+
{
|
|
2439
|
+
id: "systemd.sentinel",
|
|
2440
|
+
title: "Hermes systemd/sentinel units enabled + active",
|
|
2441
|
+
audit: (ctx) => {
|
|
2442
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
2443
|
+
if (!roles.length) {
|
|
2444
|
+
return { id: "systemd.sentinel", title: "Hermes systemd/sentinel units enabled + active", status: "skip", summary: "No Hermes roles present", details: [], fixable: false };
|
|
2445
|
+
}
|
|
2446
|
+
const probe = systemctlUser(["is-system-running"]);
|
|
2447
|
+
if (!probe.ok && !/running|degraded|starting|maintenance/.test(`${probe.stdout} ${probe.stderr}`)) {
|
|
2448
|
+
return { id: "systemd.sentinel", title: "Hermes systemd/sentinel units enabled + active", status: "warn", summary: "systemd --user unavailable; unit state not auditable here", details: [], fixable: false };
|
|
2449
|
+
}
|
|
2450
|
+
const details = [];
|
|
2451
|
+
for (const role of roles) {
|
|
2452
|
+
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
|
|
2453
|
+
const state = checkUnit(unit);
|
|
2454
|
+
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
return {
|
|
2458
|
+
id: "systemd.sentinel",
|
|
2459
|
+
title: "Hermes systemd/sentinel units enabled + active",
|
|
2460
|
+
status: details.length === 0 ? "pass" : "fail",
|
|
2461
|
+
summary: details.length === 0 ? "Hermes user units are enabled and active" : `${details.length} systemd parity issue(s) detected`,
|
|
2462
|
+
details,
|
|
2463
|
+
fixable: true
|
|
2464
|
+
};
|
|
2465
|
+
},
|
|
2466
|
+
migrate: (ctx, finding) => {
|
|
2467
|
+
const roles = discoverRoles(ctx.repoRoot);
|
|
2468
|
+
const changedFiles = [];
|
|
2469
|
+
const details = [];
|
|
2470
|
+
if (!roles.length) {
|
|
2471
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "No Hermes roles present", changedFiles, details };
|
|
2472
|
+
}
|
|
2473
|
+
const probe = systemctlUser(["is-system-running"]);
|
|
2474
|
+
if (!probe.ok && !/running|degraded|starting|maintenance/.test(`${probe.stdout} ${probe.stderr}`)) {
|
|
2475
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2476
|
+
}
|
|
2477
|
+
for (const role of roles) {
|
|
2478
|
+
const sysDir = join9(ctx.homeDir, ".config", "systemd", "user");
|
|
2479
|
+
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
2480
|
+
const allUnitsPresent = units.every((unit) => existsSync7(join9(sysDir, unit)));
|
|
2481
|
+
if (allUnitsPresent) {
|
|
2482
|
+
if (ctx.dryRun) {
|
|
2483
|
+
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
2484
|
+
} else {
|
|
2485
|
+
systemctlUser(["daemon-reload"]);
|
|
2486
|
+
for (const unit of units) {
|
|
2487
|
+
systemctlUser(["enable", "--now", unit]);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
for (const script of [join9(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
2493
|
+
if (!script || !existsSync7(script)) continue;
|
|
2494
|
+
if (ctx.dryRun) {
|
|
2495
|
+
details.push(`would run: bash ${script}`);
|
|
2496
|
+
} else {
|
|
2497
|
+
const result = spawnSync4("bash", [script], { cwd: role.roleDir, encoding: "utf8" });
|
|
2498
|
+
if (result.status !== 0) details.push(`script failed: ${script}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
return {
|
|
2503
|
+
id: finding.id,
|
|
2504
|
+
title: finding.title,
|
|
2505
|
+
status: details.some((detail) => detail.includes("failed:")) ? "blocked" : details.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
2506
|
+
summary: details.length ? ctx.dryRun ? "Planned systemd remediation commands" : "Attempted systemd remediation" : "No changes required",
|
|
2507
|
+
changedFiles,
|
|
2508
|
+
details
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
];
|
|
2513
|
+
function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
2514
|
+
const normalized = content.endsWith("\n") ? content : `${content}
|
|
2515
|
+
`;
|
|
2516
|
+
if (safeReadText(path) === normalized) return;
|
|
2517
|
+
changedFiles.push(path);
|
|
2518
|
+
if (!dryRun) {
|
|
2519
|
+
writeText(path, normalized);
|
|
2520
|
+
if (mode) chmodSync(path, mode);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
function getParityRuleIds() {
|
|
2524
|
+
return RULES.map((rule) => rule.id);
|
|
2525
|
+
}
|
|
2526
|
+
function runAudit(repoArg) {
|
|
2527
|
+
const pjanglerRoot = resolvePjanglerRoot();
|
|
2528
|
+
const ctx = {
|
|
2529
|
+
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2530
|
+
dryRun: true,
|
|
2531
|
+
pjanglerRoot,
|
|
2532
|
+
homeDir: homedir4()
|
|
2533
|
+
};
|
|
2534
|
+
const rules = RULES.map((rule) => rule.audit(ctx));
|
|
2535
|
+
return {
|
|
2536
|
+
repo: ctx.repoRoot,
|
|
2537
|
+
ok: rules.every((rule) => rule.status === "pass" || rule.status === "skip"),
|
|
2538
|
+
auditedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2539
|
+
rules
|
|
2540
|
+
};
|
|
2541
|
+
}
|
|
2542
|
+
function runMigration(selector, repoArg, dryRun, all) {
|
|
2543
|
+
const pjanglerRoot = resolvePjanglerRoot();
|
|
2544
|
+
const ctx = {
|
|
2545
|
+
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2546
|
+
dryRun,
|
|
2547
|
+
pjanglerRoot,
|
|
2548
|
+
homeDir: homedir4()
|
|
2549
|
+
};
|
|
2550
|
+
const selected = all ? RULES : RULES.filter((rule) => rule.id === selector);
|
|
2551
|
+
if (!selected.length) {
|
|
2552
|
+
throw new Error(`Unknown parity rule: ${selector}`);
|
|
2553
|
+
}
|
|
2554
|
+
const results = selected.map((rule) => {
|
|
2555
|
+
try {
|
|
2556
|
+
return rule.migrate(ctx, rule.audit(ctx));
|
|
2557
|
+
} catch (err) {
|
|
2558
|
+
return {
|
|
2559
|
+
id: rule.id,
|
|
2560
|
+
title: rule.title,
|
|
2561
|
+
status: "blocked",
|
|
2562
|
+
summary: `migrate threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
2563
|
+
changedFiles: [],
|
|
2564
|
+
details: []
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
});
|
|
2568
|
+
const changedFiles = Array.from(new Set(results.flatMap((result) => result.changedFiles))).sort();
|
|
2569
|
+
return {
|
|
2570
|
+
repo: ctx.repoRoot,
|
|
2571
|
+
dryRun,
|
|
2572
|
+
ok: results.every((result) => result.status !== "blocked"),
|
|
2573
|
+
selectedRules: selected.map((rule) => rule.id),
|
|
2574
|
+
results,
|
|
2575
|
+
changedFiles
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
function formatAuditReport(report) {
|
|
2579
|
+
const lines = [`repo: ${report.repo}`, `ok: ${report.ok}`, `audited_at: ${report.auditedAt}`, "rules:"];
|
|
2580
|
+
for (const rule of report.rules) {
|
|
2581
|
+
lines.push(`- ${rule.id} [${rule.status}] ${rule.summary}`);
|
|
2582
|
+
for (const detail of rule.details) lines.push(` - ${detail}`);
|
|
2583
|
+
}
|
|
2584
|
+
return `${lines.join("\n")}
|
|
2585
|
+
`;
|
|
2586
|
+
}
|
|
2587
|
+
|
|
1273
2588
|
// src/mcp-server.ts
|
|
1274
2589
|
var server = new McpServer({
|
|
1275
2590
|
name: "pjangler-mcp",
|
|
1276
2591
|
version: PJANGLER_VERSION
|
|
1277
2592
|
});
|
|
2593
|
+
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "linear", "trello"]);
|
|
1278
2594
|
function resolveTargetDir(targetDir) {
|
|
1279
|
-
const dir =
|
|
1280
|
-
if (!
|
|
2595
|
+
const dir = resolve2(targetDir ?? process.cwd());
|
|
2596
|
+
if (!existsSync8(dir)) {
|
|
1281
2597
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
1282
2598
|
}
|
|
1283
2599
|
if (!statSync(dir).isDirectory()) {
|
|
@@ -1285,6 +2601,49 @@ function resolveTargetDir(targetDir) {
|
|
|
1285
2601
|
}
|
|
1286
2602
|
return dir;
|
|
1287
2603
|
}
|
|
2604
|
+
function resolvePjanglerRoot2() {
|
|
2605
|
+
let dir = dirname7(fileURLToPath5(import.meta.url));
|
|
2606
|
+
while (dir !== dirname7(dir)) {
|
|
2607
|
+
if (existsSync8(join10(dir, "package.json")) && existsSync8(join10(dir, "templates", "commonproject", "copier.yml"))) {
|
|
2608
|
+
return dir;
|
|
2609
|
+
}
|
|
2610
|
+
dir = dirname7(dir);
|
|
2611
|
+
}
|
|
2612
|
+
throw new Error("Unable to resolve pjangler root");
|
|
2613
|
+
}
|
|
2614
|
+
function slugify(value) {
|
|
2615
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
2616
|
+
}
|
|
2617
|
+
function asText(payload) {
|
|
2618
|
+
return { content: [{ type: "text", text: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2) }] };
|
|
2619
|
+
}
|
|
2620
|
+
function auditSummary(report) {
|
|
2621
|
+
const counts = report.rules.reduce((acc, rule) => {
|
|
2622
|
+
acc[rule.status] = (acc[rule.status] ?? 0) + 1;
|
|
2623
|
+
return acc;
|
|
2624
|
+
}, {});
|
|
2625
|
+
const nextActions = report.rules.filter((rule) => (rule.status === "fail" || rule.status === "warn") && rule.fixable).map((rule) => `pjangler_migrate_project ${rule.id}`);
|
|
2626
|
+
return { counts, nextActions };
|
|
2627
|
+
}
|
|
2628
|
+
function migrationSummary(report) {
|
|
2629
|
+
const counts = report.results.reduce((acc, result) => {
|
|
2630
|
+
acc[result.status] = (acc[result.status] ?? 0) + 1;
|
|
2631
|
+
return acc;
|
|
2632
|
+
}, {});
|
|
2633
|
+
return { counts, changedFileCount: report.changedFiles.length };
|
|
2634
|
+
}
|
|
2635
|
+
function parityGuidance() {
|
|
2636
|
+
return {
|
|
2637
|
+
skill: "@33god-projects",
|
|
2638
|
+
guidance: "Use these tools before editing a project so the repo SOT, agent files, mise hooks, and Hermes role scaffold are current.",
|
|
2639
|
+
workflows: [
|
|
2640
|
+
"audit -> pjangler_audit_project",
|
|
2641
|
+
"migrate -> pjangler_migrate_project",
|
|
2642
|
+
"bootstrap -> pjangler_bootstrap_33god_project",
|
|
2643
|
+
"agent provisioning -> pjangler_deploy_hermes_agent"
|
|
2644
|
+
]
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
1288
2647
|
async function runRecipeWithCapture(recipeName, context) {
|
|
1289
2648
|
const recipe = createRecipe(recipeName, context);
|
|
1290
2649
|
if (!recipe) {
|
|
@@ -1317,11 +2676,31 @@ async function runRecipeWithCapture(recipeName, context) {
|
|
|
1317
2676
|
console.error = origError;
|
|
1318
2677
|
}
|
|
1319
2678
|
}
|
|
2679
|
+
function buildCommonProjectCopierAction(input) {
|
|
2680
|
+
const data = {
|
|
2681
|
+
project_name: input.projectName,
|
|
2682
|
+
project_description: input.projectDescription ?? "",
|
|
2683
|
+
project_slug: input.projectSlug,
|
|
2684
|
+
ticket_provider: input.ticketProvider,
|
|
2685
|
+
plane_workspace: input.planeWorkspace,
|
|
2686
|
+
plane_project_id: input.planeProjectId ?? "",
|
|
2687
|
+
project_identifier: input.projectIdentifier,
|
|
2688
|
+
primary_language: input.primaryLanguage
|
|
2689
|
+
};
|
|
2690
|
+
const args = ["copy", "--trust", input.templateDir, input.targetDir, "--defaults"];
|
|
2691
|
+
for (const [key, value] of Object.entries(data)) args.push("--data", `${key}=${value}`);
|
|
2692
|
+
if (input.overwrite) args.push("--overwrite");
|
|
2693
|
+
return {
|
|
2694
|
+
kind: "copier.copy.commonproject",
|
|
2695
|
+
command: ["copier", ...args],
|
|
2696
|
+
data
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
1320
2699
|
server.registerTool(
|
|
1321
2700
|
"pjangler_list_capabilities",
|
|
1322
2701
|
{
|
|
1323
2702
|
title: "List pjangler capabilities",
|
|
1324
|
-
description: "Returns available recipes and
|
|
2703
|
+
description: "Returns available recipes, commands, parity rules, workflows, and @33god-projects tool guidance.",
|
|
1325
2704
|
inputSchema: {}
|
|
1326
2705
|
},
|
|
1327
2706
|
async () => {
|
|
@@ -1335,11 +2714,197 @@ server.registerTool(
|
|
|
1335
2714
|
name: c.name,
|
|
1336
2715
|
description: c.description,
|
|
1337
2716
|
group: c.group
|
|
1338
|
-
}))
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
2717
|
+
})),
|
|
2718
|
+
parityRules: getParityRuleIds(),
|
|
2719
|
+
recommendedWorkflows: {
|
|
2720
|
+
existingProject: ["pjangler_audit_project", "pjangler_migrate_project"],
|
|
2721
|
+
new33godProject: ["pjangler_bootstrap_33god_project", "pjangler_audit_project"],
|
|
2722
|
+
hermesAgentProvisioning: ["pjangler_deploy_hermes_agent", "pjangler_audit_project"]
|
|
2723
|
+
},
|
|
2724
|
+
skillSynergy: parityGuidance()
|
|
1342
2725
|
};
|
|
2726
|
+
return asText(payload);
|
|
2727
|
+
}
|
|
2728
|
+
);
|
|
2729
|
+
server.registerTool(
|
|
2730
|
+
"pjangler_list_parity_rules",
|
|
2731
|
+
{
|
|
2732
|
+
title: "List pjangler parity rules",
|
|
2733
|
+
description: "Returns parity rule ids plus brief @33god-projects guidance.",
|
|
2734
|
+
inputSchema: {}
|
|
2735
|
+
},
|
|
2736
|
+
async () => asText({ parityRules: getParityRuleIds(), guidance: parityGuidance() })
|
|
2737
|
+
);
|
|
2738
|
+
server.registerTool(
|
|
2739
|
+
"pjangler_audit_project",
|
|
2740
|
+
{
|
|
2741
|
+
title: "Audit project parity",
|
|
2742
|
+
description: "Runs pjangler parity audit for a project and returns structured findings with summary counts and next actions.",
|
|
2743
|
+
inputSchema: {
|
|
2744
|
+
targetDir: z.string().optional(),
|
|
2745
|
+
json: z.boolean().optional()
|
|
2746
|
+
}
|
|
2747
|
+
},
|
|
2748
|
+
async ({ targetDir, json }) => {
|
|
2749
|
+
try {
|
|
2750
|
+
const resolvedTarget = resolveTargetDir(targetDir);
|
|
2751
|
+
const report = runAudit(resolvedTarget);
|
|
2752
|
+
const payload = { ...report, summary: auditSummary(report), guidance: parityGuidance() };
|
|
2753
|
+
return asText(json === false ? formatAuditReport(report) : payload);
|
|
2754
|
+
} catch (err) {
|
|
2755
|
+
return { isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] };
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
);
|
|
2759
|
+
server.registerTool(
|
|
2760
|
+
"pjangler_migrate_project",
|
|
2761
|
+
{
|
|
2762
|
+
title: "Migrate project parity",
|
|
2763
|
+
description: "Runs one pjangler parity migration rule, or all rules, against a project.",
|
|
2764
|
+
inputSchema: {
|
|
2765
|
+
targetDir: z.string().optional(),
|
|
2766
|
+
ruleId: z.string().optional(),
|
|
2767
|
+
all: z.boolean().optional(),
|
|
2768
|
+
dryRun: z.boolean().optional()
|
|
2769
|
+
}
|
|
2770
|
+
},
|
|
2771
|
+
async ({ targetDir, ruleId, all, dryRun }) => {
|
|
2772
|
+
try {
|
|
2773
|
+
const runAll = all ?? false;
|
|
2774
|
+
if (!runAll && !ruleId) throw new Error("Either ruleId or all=true is required");
|
|
2775
|
+
if (runAll && ruleId) throw new Error("Pass either ruleId or all=true, not both");
|
|
2776
|
+
const resolvedTarget = resolveTargetDir(targetDir);
|
|
2777
|
+
const report = runMigration(ruleId, resolvedTarget, dryRun ?? true, runAll);
|
|
2778
|
+
return asText({
|
|
2779
|
+
ok: report.ok,
|
|
2780
|
+
repo: report.repo,
|
|
2781
|
+
dryRun: report.dryRun,
|
|
2782
|
+
selectedRules: report.selectedRules,
|
|
2783
|
+
changedFiles: report.changedFiles,
|
|
2784
|
+
results: report.results,
|
|
2785
|
+
summary: migrationSummary(report)
|
|
2786
|
+
});
|
|
2787
|
+
} catch (err) {
|
|
2788
|
+
return { isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] };
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
);
|
|
2792
|
+
server.registerTool(
|
|
2793
|
+
"pjangler_bootstrap_33god_project",
|
|
2794
|
+
{
|
|
2795
|
+
title: "Bootstrap a new @33god project",
|
|
2796
|
+
description: "Create a new CommonProject-based 33god repo with optional local Hermes agent provisioning. Dry-run is safe and does not require copier.",
|
|
2797
|
+
inputSchema: {
|
|
2798
|
+
parentDir: z.string().optional(),
|
|
2799
|
+
targetDir: z.string().optional(),
|
|
2800
|
+
projectName: z.string(),
|
|
2801
|
+
projectDescription: z.string().optional(),
|
|
2802
|
+
projectSlug: z.string().optional(),
|
|
2803
|
+
ticketProvider: TICKET_PROVIDER_SCHEMA.optional(),
|
|
2804
|
+
planeWorkspace: z.string().optional(),
|
|
2805
|
+
planeProjectId: z.string().optional(),
|
|
2806
|
+
projectIdentifier: z.string().optional(),
|
|
2807
|
+
primaryLanguage: z.string().optional(),
|
|
2808
|
+
skipPlane: z.boolean().optional(),
|
|
2809
|
+
provisionAgent: z.boolean().optional(),
|
|
2810
|
+
agentRole: z.string().optional(),
|
|
2811
|
+
agentPurpose: z.string().optional(),
|
|
2812
|
+
local: z.boolean().optional(),
|
|
2813
|
+
force: z.boolean().optional(),
|
|
2814
|
+
overwrite: z.boolean().optional(),
|
|
2815
|
+
dryRun: z.boolean().optional()
|
|
2816
|
+
}
|
|
2817
|
+
},
|
|
2818
|
+
async (input) => {
|
|
2819
|
+
try {
|
|
2820
|
+
const pjanglerRoot = resolvePjanglerRoot2();
|
|
2821
|
+
const projectSlug = input.projectSlug ?? slugify(input.projectName);
|
|
2822
|
+
const parentDir = resolve2(input.parentDir ?? process.cwd());
|
|
2823
|
+
if (!existsSync8(parentDir) || !statSync(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
2824
|
+
const targetDir = resolve2(input.targetDir ?? join10(parentDir, projectSlug));
|
|
2825
|
+
const overwrite = input.overwrite ?? input.force ?? false;
|
|
2826
|
+
const dryRun = input.dryRun ?? true;
|
|
2827
|
+
const local = input.local ?? true;
|
|
2828
|
+
const skipPlane = input.skipPlane ?? true;
|
|
2829
|
+
const planeProjectId = input.planeProjectId ?? "";
|
|
2830
|
+
if (!skipPlane && !planeProjectId) {
|
|
2831
|
+
throw new Error("planeProjectId is required when skipPlane=false; keep skipPlane=true for safe local bootstrap");
|
|
2832
|
+
}
|
|
2833
|
+
if (existsSync8(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
2834
|
+
const templateDir = join10(pjanglerRoot, "templates", "commonproject");
|
|
2835
|
+
const copierAction = buildCommonProjectCopierAction({
|
|
2836
|
+
templateDir,
|
|
2837
|
+
targetDir,
|
|
2838
|
+
projectName: input.projectName,
|
|
2839
|
+
projectDescription: input.projectDescription,
|
|
2840
|
+
projectSlug,
|
|
2841
|
+
ticketProvider: input.ticketProvider ?? "plane",
|
|
2842
|
+
planeWorkspace: input.planeWorkspace ?? "33god",
|
|
2843
|
+
planeProjectId,
|
|
2844
|
+
projectIdentifier: input.projectIdentifier ?? projectSlug.slice(0, 4).toUpperCase(),
|
|
2845
|
+
primaryLanguage: input.primaryLanguage ?? "python",
|
|
2846
|
+
overwrite
|
|
2847
|
+
});
|
|
2848
|
+
const actions = [
|
|
2849
|
+
{ kind: "ensure.parent", path: dirname7(targetDir) },
|
|
2850
|
+
copierAction
|
|
2851
|
+
];
|
|
2852
|
+
if (input.provisionAgent) {
|
|
2853
|
+
actions.push({
|
|
2854
|
+
kind: "pjangler.recipe.hermes-agent",
|
|
2855
|
+
targetDir,
|
|
2856
|
+
context: {
|
|
2857
|
+
targetRepo: projectSlug,
|
|
2858
|
+
role: input.agentRole ?? "pm",
|
|
2859
|
+
agentPurpose: input.agentPurpose ?? `Project manager for ${input.projectName}`,
|
|
2860
|
+
local,
|
|
2861
|
+
dryRun,
|
|
2862
|
+
force: overwrite,
|
|
2863
|
+
skipTelegram: true,
|
|
2864
|
+
skipEmail: true,
|
|
2865
|
+
skipRuntimeRepo: local,
|
|
2866
|
+
skipPlane: skipPlane || local,
|
|
2867
|
+
skipBloodbank: local,
|
|
2868
|
+
skipSystemd: local || process.platform === "darwin"
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
if (dryRun) {
|
|
2873
|
+
return asText({ ok: true, dryRun, targetDir, projectSlug, actions, guidance: parityGuidance() });
|
|
2874
|
+
}
|
|
2875
|
+
const which = spawnSync5("which", ["copier"], { encoding: "utf8" });
|
|
2876
|
+
if (which.status !== 0) throw new Error("copier not found on PATH. Install with: uv tool install copier or pip install copier");
|
|
2877
|
+
mkdirSync6(dirname7(targetDir), { recursive: true });
|
|
2878
|
+
const result = spawnSync5(copierAction.command[0], copierAction.command.slice(1), { encoding: "utf8", cwd: pjanglerRoot });
|
|
2879
|
+
const logs = [result.stdout.trim()].filter(Boolean);
|
|
2880
|
+
const errors = [result.stderr.trim()].filter(Boolean);
|
|
2881
|
+
if (result.status !== 0) {
|
|
2882
|
+
return asText({ ok: false, dryRun, targetDir, actions, logs, errors, exitCode: result.status });
|
|
2883
|
+
}
|
|
2884
|
+
let agentResult;
|
|
2885
|
+
if (input.provisionAgent) {
|
|
2886
|
+
const context = {
|
|
2887
|
+
targetDir,
|
|
2888
|
+
yes: true,
|
|
2889
|
+
targetRepo: projectSlug,
|
|
2890
|
+
role: input.agentRole ?? "pm",
|
|
2891
|
+
agentPurpose: input.agentPurpose ?? `Project manager for ${input.projectName}`,
|
|
2892
|
+
local,
|
|
2893
|
+
force: overwrite,
|
|
2894
|
+
dryRun: false,
|
|
2895
|
+
skipTelegram: true,
|
|
2896
|
+
skipEmail: true,
|
|
2897
|
+
skipRuntimeRepo: local,
|
|
2898
|
+
skipPlane: skipPlane || local,
|
|
2899
|
+
skipBloodbank: local,
|
|
2900
|
+
skipSystemd: local || process.platform === "darwin"
|
|
2901
|
+
};
|
|
2902
|
+
agentResult = await runRecipeWithCapture("hermes-agent", context);
|
|
2903
|
+
}
|
|
2904
|
+
return asText({ ok: !agentResult || agentResult.success, dryRun, targetDir, projectSlug, actions, logs, errors, agentResult });
|
|
2905
|
+
} catch (err) {
|
|
2906
|
+
return { isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] };
|
|
2907
|
+
}
|
|
1343
2908
|
}
|
|
1344
2909
|
);
|
|
1345
2910
|
server.registerTool(
|
|
@@ -1359,22 +2924,11 @@ server.registerTool(
|
|
|
1359
2924
|
content: [{ type: "text", text: `Recipe not found: ${recipe}` }]
|
|
1360
2925
|
};
|
|
1361
2926
|
}
|
|
1362
|
-
return {
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
{
|
|
1368
|
-
name: info.name,
|
|
1369
|
-
description: info.description,
|
|
1370
|
-
commands: info.commands
|
|
1371
|
-
},
|
|
1372
|
-
null,
|
|
1373
|
-
2
|
|
1374
|
-
)
|
|
1375
|
-
}
|
|
1376
|
-
]
|
|
1377
|
-
};
|
|
2927
|
+
return asText({
|
|
2928
|
+
name: info.name,
|
|
2929
|
+
description: info.description,
|
|
2930
|
+
commands: info.commands
|
|
2931
|
+
});
|
|
1378
2932
|
}
|
|
1379
2933
|
);
|
|
1380
2934
|
server.registerTool(
|
|
@@ -1400,22 +2954,13 @@ server.registerTool(
|
|
|
1400
2954
|
const result = await runRecipeWithCapture(recipe, context);
|
|
1401
2955
|
return {
|
|
1402
2956
|
isError: !result.success,
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
targetDir: resolvedTarget,
|
|
1411
|
-
logs: result.logs,
|
|
1412
|
-
errors: result.errors
|
|
1413
|
-
},
|
|
1414
|
-
null,
|
|
1415
|
-
2
|
|
1416
|
-
)
|
|
1417
|
-
}
|
|
1418
|
-
]
|
|
2957
|
+
...asText({
|
|
2958
|
+
success: result.success,
|
|
2959
|
+
recipe,
|
|
2960
|
+
targetDir: resolvedTarget,
|
|
2961
|
+
logs: result.logs,
|
|
2962
|
+
errors: result.errors
|
|
2963
|
+
})
|
|
1419
2964
|
};
|
|
1420
2965
|
} catch (err) {
|
|
1421
2966
|
return {
|
|
@@ -1429,15 +2974,16 @@ server.registerTool(
|
|
|
1429
2974
|
"pjangler_deploy_hermes_agent",
|
|
1430
2975
|
{
|
|
1431
2976
|
title: "Deploy Hermes agent",
|
|
1432
|
-
description: "Provision a Hermes agent role
|
|
2977
|
+
description: "Provision a Hermes agent role for @33god-projects. For safe MCP use local=true defaults skip runtime repo, Plane, Bloodbank, and systemd; opt out with local=false plus explicit skip flags.",
|
|
1433
2978
|
inputSchema: {
|
|
1434
2979
|
targetDir: z.string(),
|
|
1435
|
-
targetRepo: z.string(),
|
|
1436
|
-
role: z.
|
|
2980
|
+
targetRepo: z.string().optional(),
|
|
2981
|
+
role: z.enum(["pm", "dev", "review", "ops", "qa"]),
|
|
1437
2982
|
agentPurpose: z.string().optional(),
|
|
1438
2983
|
soulTone: z.enum(["direct", "playful", "formal", "terse"]).optional(),
|
|
1439
2984
|
modelProvider: z.string().optional(),
|
|
1440
2985
|
modelName: z.string().optional(),
|
|
2986
|
+
local: z.boolean().optional(),
|
|
1441
2987
|
force: z.boolean().optional(),
|
|
1442
2988
|
dryRun: z.boolean().optional(),
|
|
1443
2989
|
skipTelegram: z.boolean().optional(),
|
|
@@ -1445,61 +2991,58 @@ server.registerTool(
|
|
|
1445
2991
|
skipRuntimeRepo: z.boolean().optional(),
|
|
1446
2992
|
skipPlane: z.boolean().optional(),
|
|
1447
2993
|
skipBloodbank: z.boolean().optional(),
|
|
1448
|
-
skipSystemd: z.boolean().optional()
|
|
2994
|
+
skipSystemd: z.boolean().optional(),
|
|
2995
|
+
ticketProvider: TICKET_PROVIDER_SCHEMA.optional()
|
|
1449
2996
|
}
|
|
1450
2997
|
},
|
|
1451
2998
|
async (input) => {
|
|
1452
2999
|
try {
|
|
1453
3000
|
const resolvedTarget = resolveTargetDir(input.targetDir);
|
|
3001
|
+
const local = input.local ?? true;
|
|
1454
3002
|
const context = {
|
|
1455
3003
|
targetDir: resolvedTarget,
|
|
1456
3004
|
yes: true,
|
|
1457
|
-
|
|
3005
|
+
local,
|
|
3006
|
+
targetRepo: input.targetRepo ?? basename2(resolvedTarget),
|
|
1458
3007
|
role: input.role,
|
|
1459
3008
|
agentPurpose: input.agentPurpose,
|
|
1460
3009
|
soulTone: input.soulTone,
|
|
1461
3010
|
modelProvider: input.modelProvider,
|
|
1462
3011
|
modelName: input.modelName,
|
|
3012
|
+
ticketProvider: input.ticketProvider,
|
|
1463
3013
|
force: input.force ?? false,
|
|
1464
3014
|
dryRun: input.dryRun ?? false,
|
|
1465
3015
|
skipTelegram: input.skipTelegram ?? true,
|
|
1466
3016
|
skipEmail: input.skipEmail ?? true,
|
|
1467
|
-
skipRuntimeRepo: input.skipRuntimeRepo ??
|
|
1468
|
-
skipPlane: input.skipPlane ??
|
|
1469
|
-
skipBloodbank: input.skipBloodbank ??
|
|
1470
|
-
skipSystemd: input.skipSystemd ??
|
|
3017
|
+
skipRuntimeRepo: input.skipRuntimeRepo ?? local,
|
|
3018
|
+
skipPlane: input.skipPlane ?? local,
|
|
3019
|
+
skipBloodbank: input.skipBloodbank ?? local,
|
|
3020
|
+
skipSystemd: input.skipSystemd ?? (local || process.platform === "darwin")
|
|
1471
3021
|
};
|
|
1472
3022
|
const result = await runRecipeWithCapture("hermes-agent", context);
|
|
1473
3023
|
return {
|
|
1474
3024
|
isError: !result.success,
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
errors: result.errors
|
|
1497
|
-
},
|
|
1498
|
-
null,
|
|
1499
|
-
2
|
|
1500
|
-
)
|
|
1501
|
-
}
|
|
1502
|
-
]
|
|
3025
|
+
...asText({
|
|
3026
|
+
success: result.success,
|
|
3027
|
+
recipe: "hermes-agent",
|
|
3028
|
+
targetDir: resolvedTarget,
|
|
3029
|
+
guidance: parityGuidance(),
|
|
3030
|
+
context: {
|
|
3031
|
+
targetRepo: context.targetRepo,
|
|
3032
|
+
role: context.role,
|
|
3033
|
+
local: context.local,
|
|
3034
|
+
dryRun: context.dryRun,
|
|
3035
|
+
force: context.force,
|
|
3036
|
+
skipTelegram: context.skipTelegram,
|
|
3037
|
+
skipEmail: context.skipEmail,
|
|
3038
|
+
skipRuntimeRepo: context.skipRuntimeRepo,
|
|
3039
|
+
skipPlane: context.skipPlane,
|
|
3040
|
+
skipBloodbank: context.skipBloodbank,
|
|
3041
|
+
skipSystemd: context.skipSystemd
|
|
3042
|
+
},
|
|
3043
|
+
logs: result.logs,
|
|
3044
|
+
errors: result.errors
|
|
3045
|
+
})
|
|
1503
3046
|
};
|
|
1504
3047
|
} catch (err) {
|
|
1505
3048
|
return {
|