@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/index.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
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/index.ts
|
|
10
4
|
import { Command as Command3 } from "commander";
|
|
@@ -13,12 +7,7 @@ import { Command as Command3 } from "commander";
|
|
|
13
7
|
var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
|
|
14
8
|
var SOUL_TONES = ["direct", "playful", "formal", "terse"];
|
|
15
9
|
var ROLE_CHOICES = [
|
|
16
|
-
{ value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship" },
|
|
17
|
-
{
|
|
18
|
-
value: "scrum-master",
|
|
19
|
-
label: "Scrum Master (Ticket Sentinel)",
|
|
20
|
-
hint: "continuous ticket sentinel + autonomous delegated review"
|
|
21
|
-
},
|
|
10
|
+
{ value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
|
|
22
11
|
{ value: "dev", label: "Developer (dev)", hint: "implements tickets" },
|
|
23
12
|
{ value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
|
|
24
13
|
{ value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
|
|
@@ -38,10 +27,12 @@ function deriveProfileName(repo, role) {
|
|
|
38
27
|
|
|
39
28
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
40
29
|
import { homedir, platform } from "node:os";
|
|
41
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
42
|
-
import { join, dirname } from "node:path";
|
|
30
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
31
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
43
32
|
|
|
44
33
|
// src/commands/Command.ts
|
|
34
|
+
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
35
|
+
import { join, dirname } from "path";
|
|
45
36
|
var Command = class {
|
|
46
37
|
context;
|
|
47
38
|
constructor(context) {
|
|
@@ -54,30 +45,24 @@ var Command = class {
|
|
|
54
45
|
return this.context.dryRun ? `[DRY RUN] ${message}` : message;
|
|
55
46
|
}
|
|
56
47
|
fileExists(filePath) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const fullPath = join8(this.context.targetDir, filePath);
|
|
60
|
-
return existsSync6(fullPath);
|
|
48
|
+
const fullPath = join(this.context.targetDir, filePath);
|
|
49
|
+
return existsSync(fullPath);
|
|
61
50
|
}
|
|
62
51
|
writeFile(filePath, content) {
|
|
63
52
|
if (this.context.dryRun) {
|
|
64
53
|
return;
|
|
65
54
|
}
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
mkdirSync4(dir, { recursive: true });
|
|
71
|
-
writeFileSync3(fullPath, content);
|
|
55
|
+
const fullPath = join(this.context.targetDir, filePath);
|
|
56
|
+
const dir = dirname(fullPath);
|
|
57
|
+
mkdirSync(dir, { recursive: true });
|
|
58
|
+
writeFileSync(fullPath, content);
|
|
72
59
|
}
|
|
73
60
|
createDirectory(dirPath) {
|
|
74
61
|
if (this.context.dryRun) {
|
|
75
62
|
return;
|
|
76
63
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const fullPath = join8(this.context.targetDir, dirPath);
|
|
80
|
-
mkdirSync4(fullPath, { recursive: true });
|
|
64
|
+
const fullPath = join(this.context.targetDir, dirPath);
|
|
65
|
+
mkdirSync(fullPath, { recursive: true });
|
|
81
66
|
}
|
|
82
67
|
};
|
|
83
68
|
|
|
@@ -86,26 +71,26 @@ function resolveTemplateConfigPath() {
|
|
|
86
71
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
87
72
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
88
73
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
89
|
-
const base = xdg && xdg.length ? xdg :
|
|
90
|
-
return
|
|
74
|
+
const base = xdg && xdg.length ? xdg : join2(homedir(), ".config");
|
|
75
|
+
return join2(base, "hermes-agent-template", "config.toml");
|
|
91
76
|
}
|
|
92
77
|
function detectHermesBin(home) {
|
|
93
78
|
const candidates = [
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
79
|
+
join2(home, "code", "hermes-agent", "venv", "bin", "hermes"),
|
|
80
|
+
join2(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
|
|
81
|
+
join2(home, ".local", "bin", "hermes")
|
|
97
82
|
];
|
|
98
83
|
for (const c of candidates) {
|
|
99
|
-
if (
|
|
84
|
+
if (existsSync2(c)) return c;
|
|
100
85
|
}
|
|
101
86
|
return candidates[0];
|
|
102
87
|
}
|
|
103
88
|
function renderHostConfig() {
|
|
104
89
|
const home = homedir();
|
|
105
90
|
const hermesBin = detectHermesBin(home);
|
|
106
|
-
const hermesRepo =
|
|
107
|
-
const scaffoldDir =
|
|
108
|
-
const skillsDir =
|
|
91
|
+
const hermesRepo = join2(home, "code", "hermes-agent");
|
|
92
|
+
const scaffoldDir = join2(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
93
|
+
const skillsDir = join2(home, ".agents", "skills");
|
|
109
94
|
return `# hermes-agent-template \u2014 host configuration
|
|
110
95
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
111
96
|
#
|
|
@@ -149,7 +134,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
149
134
|
const ctx = this.context;
|
|
150
135
|
const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
|
|
151
136
|
const path = resolveTemplateConfigPath();
|
|
152
|
-
const exists =
|
|
137
|
+
const exists = existsSync2(path);
|
|
153
138
|
if (exists && !force) {
|
|
154
139
|
console.log(`\u2713 Config present: ${path}`);
|
|
155
140
|
return { success: true, message: "" };
|
|
@@ -159,8 +144,8 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
159
144
|
return { success: true, message: "" };
|
|
160
145
|
}
|
|
161
146
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
147
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
148
|
+
writeFileSync2(path, renderHostConfig());
|
|
164
149
|
} catch (err) {
|
|
165
150
|
const msg = err instanceof Error ? err.message : String(err);
|
|
166
151
|
return { success: false, message: `\u2717 Failed to write ${path}: ${msg}` };
|
|
@@ -577,12 +562,12 @@ var NodeRecipe = class extends Recipe {
|
|
|
577
562
|
};
|
|
578
563
|
|
|
579
564
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
580
|
-
import { basename, join as
|
|
565
|
+
import { basename, join as join3 } from "node:path";
|
|
581
566
|
import { readFileSync } from "node:fs";
|
|
582
567
|
import * as p from "@clack/prompts";
|
|
583
568
|
function detectTicketProvider(targetDir) {
|
|
584
569
|
try {
|
|
585
|
-
const t = JSON.parse(readFileSync(
|
|
570
|
+
const t = JSON.parse(readFileSync(join3(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
586
571
|
return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
|
|
587
572
|
} catch {
|
|
588
573
|
return void 0;
|
|
@@ -601,7 +586,6 @@ var PromptForAgentConfig = class extends Command {
|
|
|
601
586
|
ctx.modelProvider ??= "";
|
|
602
587
|
ctx.modelName ??= "";
|
|
603
588
|
ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
|
|
604
|
-
ctx.withScrumMaster ??= false;
|
|
605
589
|
ctx.skipTelegram ??= true;
|
|
606
590
|
ctx.skipEmail ??= true;
|
|
607
591
|
ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
|
|
@@ -647,14 +631,6 @@ var PromptForAgentConfig = class extends Command {
|
|
|
647
631
|
if (p.isCancel(answer)) return this.cancelled();
|
|
648
632
|
ctx.ticketProvider = answer;
|
|
649
633
|
}
|
|
650
|
-
if (ctx.role === "pm" && ctx.withScrumMaster === void 0) {
|
|
651
|
-
const answer = await p.confirm({
|
|
652
|
-
message: "Also provision the paired Scrum Master (Ticket Sentinel) for this repo?",
|
|
653
|
-
initialValue: true
|
|
654
|
-
});
|
|
655
|
-
if (p.isCancel(answer)) return this.cancelled();
|
|
656
|
-
ctx.withScrumMaster = answer === true;
|
|
657
|
-
}
|
|
658
634
|
if (!ctx.agentPurpose) {
|
|
659
635
|
const answer = await p.text({
|
|
660
636
|
message: "One-line purpose",
|
|
@@ -727,21 +703,21 @@ var PromptForAgentConfig = class extends Command {
|
|
|
727
703
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
728
704
|
import { spawnSync } from "node:child_process";
|
|
729
705
|
import { homedir as homedir2 } from "node:os";
|
|
730
|
-
import { join as
|
|
731
|
-
import { existsSync as
|
|
706
|
+
import { join as join4, dirname as dirname3 } from "node:path";
|
|
707
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
732
708
|
import { fileURLToPath } from "node:url";
|
|
733
709
|
import * as p2 from "@clack/prompts";
|
|
734
710
|
function resolveVendoredTemplate(name) {
|
|
735
711
|
let dir;
|
|
736
712
|
try {
|
|
737
|
-
dir =
|
|
713
|
+
dir = dirname3(fileURLToPath(import.meta.url));
|
|
738
714
|
} catch {
|
|
739
715
|
return void 0;
|
|
740
716
|
}
|
|
741
717
|
for (let i = 0; i < 8; i++) {
|
|
742
|
-
const candidate =
|
|
743
|
-
if (
|
|
744
|
-
const parent =
|
|
718
|
+
const candidate = join4(dir, "templates", name);
|
|
719
|
+
if (existsSync3(join4(candidate, "copier.yml"))) return candidate;
|
|
720
|
+
const parent = dirname3(dir);
|
|
745
721
|
if (parent === dir) break;
|
|
746
722
|
dir = parent;
|
|
747
723
|
}
|
|
@@ -753,14 +729,13 @@ var RunCopierTemplate = class extends Command {
|
|
|
753
729
|
const { targetRepo, role, agentPurpose, soulTone, modelProvider, modelName } = ctx;
|
|
754
730
|
const ticketProvider = ctx.ticketProvider ?? "plane";
|
|
755
731
|
const profileName = ctx.profileName ?? (targetRepo && role ? deriveProfileName(targetRepo, role) : void 0);
|
|
756
|
-
const withScrumMaster = role === "pm" && ctx.withScrumMaster === true;
|
|
757
732
|
if (!targetRepo || !role) {
|
|
758
733
|
return {
|
|
759
734
|
success: false,
|
|
760
735
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
761
736
|
};
|
|
762
737
|
}
|
|
763
|
-
const roleDir =
|
|
738
|
+
const roleDir = join4(ctx.targetDir, "agents", "hermes", role);
|
|
764
739
|
ctx.roleDir = roleDir;
|
|
765
740
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
766
741
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -770,7 +745,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
770
745
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
771
746
|
};
|
|
772
747
|
}
|
|
773
|
-
if (
|
|
748
|
+
if (existsSync3(join4(roleDir, "role.yaml")) && !ctx.force) {
|
|
774
749
|
if (ctx.yes) {
|
|
775
750
|
ctx.force = true;
|
|
776
751
|
} else {
|
|
@@ -797,9 +772,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
797
772
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
798
773
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
799
774
|
};
|
|
800
|
-
const LOCAL_TEMPLATE =
|
|
775
|
+
const LOCAL_TEMPLATE = join4(homedir2(), "code", "hermes-agent-template");
|
|
801
776
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
802
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (
|
|
777
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join4(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
803
778
|
const args = [
|
|
804
779
|
"copy",
|
|
805
780
|
templateSrc,
|
|
@@ -820,8 +795,6 @@ var RunCopierTemplate = class extends Command {
|
|
|
820
795
|
`soul_tone=${soulTone ?? "direct"}`,
|
|
821
796
|
"--data",
|
|
822
797
|
`ticket_provider=${ticketProvider}`,
|
|
823
|
-
"--data",
|
|
824
|
-
`with_scrum_master=${withScrumMaster}`,
|
|
825
798
|
"--trust",
|
|
826
799
|
"--vcs-ref=HEAD"
|
|
827
800
|
];
|
|
@@ -832,7 +805,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
832
805
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
833
806
|
};
|
|
834
807
|
}
|
|
835
|
-
|
|
808
|
+
mkdirSync3(join4(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
836
809
|
const spinner4 = p2.spinner();
|
|
837
810
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
838
811
|
const result = spawnSync("copier", args, {
|
|
@@ -857,8 +830,8 @@ var RunCopierTemplate = class extends Command {
|
|
|
857
830
|
|
|
858
831
|
// src/commands/hermes/WireTelegram.ts
|
|
859
832
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
860
|
-
import { join as
|
|
861
|
-
import { existsSync as
|
|
833
|
+
import { join as join5 } from "node:path";
|
|
834
|
+
import { existsSync as existsSync4, unlinkSync } from "node:fs";
|
|
862
835
|
import * as p3 from "@clack/prompts";
|
|
863
836
|
var WireTelegram = class extends Command {
|
|
864
837
|
async invoke() {
|
|
@@ -946,15 +919,15 @@ var WireTelegram = class extends Command {
|
|
|
946
919
|
if (p3.isCancel(allowedAnswer)) {
|
|
947
920
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
948
921
|
}
|
|
949
|
-
const script =
|
|
950
|
-
if (!
|
|
922
|
+
const script = join5(roleDir, ".scripts", "30-telegram.sh");
|
|
923
|
+
if (!existsSync4(script)) {
|
|
951
924
|
return {
|
|
952
925
|
success: false,
|
|
953
926
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
954
927
|
};
|
|
955
928
|
}
|
|
956
|
-
const marker =
|
|
957
|
-
if (
|
|
929
|
+
const marker = join5(roleDir, ".scripts", ".done-30-telegram");
|
|
930
|
+
if (existsSync4(marker)) unlinkSync(marker);
|
|
958
931
|
const spinner4 = p3.spinner();
|
|
959
932
|
spinner4.start("Verifying token + wiring profile");
|
|
960
933
|
const result = spawnSync2("bash", [script], {
|
|
@@ -981,8 +954,8 @@ function cap(s) {
|
|
|
981
954
|
|
|
982
955
|
// src/commands/hermes/WireEmail.ts
|
|
983
956
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
984
|
-
import { join as
|
|
985
|
-
import { existsSync as
|
|
957
|
+
import { join as join6 } from "node:path";
|
|
958
|
+
import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
986
959
|
import * as p4 from "@clack/prompts";
|
|
987
960
|
var WireEmail = class extends Command {
|
|
988
961
|
async invoke() {
|
|
@@ -997,8 +970,8 @@ var WireEmail = class extends Command {
|
|
|
997
970
|
if (!targetRepo || !role || !roleDir) {
|
|
998
971
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
999
972
|
}
|
|
1000
|
-
const script =
|
|
1001
|
-
if (!
|
|
973
|
+
const script = join6(roleDir, ".scripts", "50-email.sh");
|
|
974
|
+
if (!existsSync5(script)) {
|
|
1002
975
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1003
976
|
}
|
|
1004
977
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
@@ -1060,8 +1033,8 @@ var WireEmail = class extends Command {
|
|
|
1060
1033
|
}
|
|
1061
1034
|
}
|
|
1062
1035
|
}
|
|
1063
|
-
const marker =
|
|
1064
|
-
if (
|
|
1036
|
+
const marker = join6(roleDir, ".scripts", ".done-50-email");
|
|
1037
|
+
if (existsSync5(marker)) unlinkSync2(marker);
|
|
1065
1038
|
const spinner4 = p4.spinner();
|
|
1066
1039
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1067
1040
|
const result = spawnSync3("bash", [script], {
|
|
@@ -1090,7 +1063,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1090
1063
|
const email = `${targetRepo}-${role}@delo.sh`;
|
|
1091
1064
|
const gw = `hermes-${agentId}-gateway.service`;
|
|
1092
1065
|
const csm = `hermes-${agentId}-consumer.service`;
|
|
1093
|
-
const
|
|
1066
|
+
const hb = `hermes-${agentId}-heartbeat.timer`;
|
|
1094
1067
|
const lines = [];
|
|
1095
1068
|
lines.push(`agent_id ${agentId}`);
|
|
1096
1069
|
lines.push(`role dir ${ctx.roleDir}`);
|
|
@@ -1100,7 +1073,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1100
1073
|
lines.push("");
|
|
1101
1074
|
lines.push("Start daemons:");
|
|
1102
1075
|
lines.push(` systemctl --user start ${csm}`);
|
|
1103
|
-
lines.push(` systemctl --user start ${
|
|
1076
|
+
lines.push(` systemctl --user start ${hb}`);
|
|
1104
1077
|
if (!skipTelegram) {
|
|
1105
1078
|
lines.push(` systemctl --user start ${gw}`);
|
|
1106
1079
|
} else {
|
|
@@ -1147,6 +1120,193 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1147
1120
|
}
|
|
1148
1121
|
};
|
|
1149
1122
|
|
|
1123
|
+
// src/commands/AgentHooksCommands.ts
|
|
1124
|
+
import { homedir as homedir3 } from "node:os";
|
|
1125
|
+
import { join as join7, dirname as dirname4 } from "node:path";
|
|
1126
|
+
import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1127
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1128
|
+
function resolveTemplateRoot() {
|
|
1129
|
+
const candidates = [];
|
|
1130
|
+
if (process.env.PJANGLER_COMMONPROJECT_TEMPLATE) {
|
|
1131
|
+
candidates.push(process.env.PJANGLER_COMMONPROJECT_TEMPLATE);
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
1135
|
+
for (let i = 0; i < 8; i++) {
|
|
1136
|
+
candidates.push(join7(dir, "templates", "commonproject", "template"));
|
|
1137
|
+
const parent = dirname4(dir);
|
|
1138
|
+
if (parent === dir) break;
|
|
1139
|
+
dir = parent;
|
|
1140
|
+
}
|
|
1141
|
+
} catch {
|
|
1142
|
+
}
|
|
1143
|
+
candidates.push(join7(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1144
|
+
for (const c of candidates) {
|
|
1145
|
+
if (existsSync6(join7(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1146
|
+
}
|
|
1147
|
+
throw new Error(
|
|
1148
|
+
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
var CopyAgentHooksTree = class extends Command {
|
|
1152
|
+
async invoke() {
|
|
1153
|
+
let templateRoot;
|
|
1154
|
+
try {
|
|
1155
|
+
templateRoot = resolveTemplateRoot();
|
|
1156
|
+
} catch (e) {
|
|
1157
|
+
return { success: false, message: `\u26A0\uFE0F ${e.message}` };
|
|
1158
|
+
}
|
|
1159
|
+
const items = [
|
|
1160
|
+
{ rel: ".agents/hooks", dir: true },
|
|
1161
|
+
{ rel: ".agents/local.example.json", dir: false },
|
|
1162
|
+
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
1163
|
+
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
1164
|
+
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
1165
|
+
];
|
|
1166
|
+
const created = [];
|
|
1167
|
+
const skipped = [];
|
|
1168
|
+
for (const { rel, dir } of items) {
|
|
1169
|
+
const src = join7(templateRoot, rel);
|
|
1170
|
+
const dest = join7(this.context.targetDir, rel);
|
|
1171
|
+
if (!existsSync6(src)) continue;
|
|
1172
|
+
if (existsSync6(dest) && !this.context.force) {
|
|
1173
|
+
skipped.push(rel);
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
if (!this.context.dryRun) {
|
|
1177
|
+
mkdirSync4(dirname4(dest), { recursive: true });
|
|
1178
|
+
cpSync(src, dest, { recursive: dir, force: true });
|
|
1179
|
+
}
|
|
1180
|
+
created.push(rel);
|
|
1181
|
+
}
|
|
1182
|
+
const verb = this.context.dryRun ? "Would copy" : "Copied";
|
|
1183
|
+
const tail = skipped.length ? ` (${skipped.length} already present \u2014 use --force to overwrite)` : "";
|
|
1184
|
+
return {
|
|
1185
|
+
success: created.length > 0,
|
|
1186
|
+
message: this.formatMessage(`\u2705 ${verb} ${created.length} agent-hooks path(s)${tail}`)
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
1191
|
+
static MARKER = "# pjangler:agent-hooks";
|
|
1192
|
+
static CR = "{{config_root}}";
|
|
1193
|
+
// mise's own runtime var — emitted literally
|
|
1194
|
+
async invoke() {
|
|
1195
|
+
const misePath = join7(this.context.targetDir, "mise.toml");
|
|
1196
|
+
if (!existsSync6(misePath)) {
|
|
1197
|
+
return {
|
|
1198
|
+
success: false,
|
|
1199
|
+
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
let content = readFileSync2(misePath, "utf8");
|
|
1203
|
+
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1204
|
+
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1205
|
+
}
|
|
1206
|
+
const cr = _WireMiseAgentHooks.CR;
|
|
1207
|
+
const enterAdds = [
|
|
1208
|
+
` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
|
|
1209
|
+
` "${cr}/.agents/hooks/sync.py --install --quiet",`
|
|
1210
|
+
].join("\n");
|
|
1211
|
+
const leaveBlock = [
|
|
1212
|
+
"leave = [",
|
|
1213
|
+
` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
|
|
1214
|
+
` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
|
|
1215
|
+
"]"
|
|
1216
|
+
].join("\n");
|
|
1217
|
+
let wiredHooks = false;
|
|
1218
|
+
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
1219
|
+
if (enterRe.test(content)) {
|
|
1220
|
+
content = content.replace(enterRe, (_m, head, close) => {
|
|
1221
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
1222
|
+
return `${head}${sep}
|
|
1223
|
+
${enterAdds}${close}`;
|
|
1224
|
+
});
|
|
1225
|
+
const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
1226
|
+
if (leaveRe.test(content)) {
|
|
1227
|
+
content = content.replace(leaveRe, (_m, head, close) => {
|
|
1228
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
1229
|
+
return `${head}${sep}
|
|
1230
|
+
"${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
|
|
1231
|
+
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
1232
|
+
});
|
|
1233
|
+
} else {
|
|
1234
|
+
content = content.replace(enterRe, (m) => `${m}
|
|
1235
|
+
${leaveBlock}`);
|
|
1236
|
+
}
|
|
1237
|
+
wiredHooks = true;
|
|
1238
|
+
}
|
|
1239
|
+
const appended = [
|
|
1240
|
+
"",
|
|
1241
|
+
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
1242
|
+
"[[watch_files]]",
|
|
1243
|
+
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
1244
|
+
'task = "hooks-sync"',
|
|
1245
|
+
"",
|
|
1246
|
+
"[tasks.hooks-sync]",
|
|
1247
|
+
'description = "Fan out hooks.master.json to each agent CLI (claude/codex/kimi/hermes)"',
|
|
1248
|
+
`run = "${cr}/.agents/hooks/sync.py --install"`,
|
|
1249
|
+
"",
|
|
1250
|
+
"[tasks.hooks-check]",
|
|
1251
|
+
'description = "Drift gate: verify generated hook configs match hooks.master.json"',
|
|
1252
|
+
`run = "${cr}/.agents/hooks/sync.py --check"`,
|
|
1253
|
+
"",
|
|
1254
|
+
"[tasks.hooks-uninstall]",
|
|
1255
|
+
'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
|
|
1256
|
+
`run = "${cr}/.agents/hooks/sync.py --uninstall"`,
|
|
1257
|
+
"",
|
|
1258
|
+
"[tasks.link-project-skills-to-clis]",
|
|
1259
|
+
'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
|
|
1260
|
+
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1261
|
+
"",
|
|
1262
|
+
"[tasks.unlink-project-skills-from-clis]",
|
|
1263
|
+
'description = "Remove project skill symlinks from shared per-CLI dirs"',
|
|
1264
|
+
`run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
|
|
1265
|
+
"",
|
|
1266
|
+
"[tasks.skills-relink]",
|
|
1267
|
+
'description = "Re-fan the project skill set to all CLIs"',
|
|
1268
|
+
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1269
|
+
"",
|
|
1270
|
+
"[tasks.hindsight-setup]",
|
|
1271
|
+
`description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
|
|
1272
|
+
`run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
|
|
1273
|
+
"",
|
|
1274
|
+
_WireMiseAgentHooks.MARKER + ":end",
|
|
1275
|
+
""
|
|
1276
|
+
].join("\n");
|
|
1277
|
+
content = content.replace(/\n*$/, "\n") + appended;
|
|
1278
|
+
if (!this.context.dryRun) writeFileSync3(misePath, content);
|
|
1279
|
+
if (wiredHooks) {
|
|
1280
|
+
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
success: true,
|
|
1284
|
+
message: this.formatMessage(
|
|
1285
|
+
`\u2705 Added agent-hooks tasks to mise.toml.
|
|
1286
|
+
\u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
|
|
1287
|
+
enter += "${cr}/.mise/scripts/link-project-skills-to-clis.sh", "${cr}/.agents/hooks/sync.py --install --quiet"
|
|
1288
|
+
leave += "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh", "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
|
|
1289
|
+
)
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
// src/recipes/AgentHooksRecipe.ts
|
|
1295
|
+
var AgentHooksRecipe = class extends Recipe {
|
|
1296
|
+
constructor(context) {
|
|
1297
|
+
super(context);
|
|
1298
|
+
this.addIngredient(CopyAgentHooksTree).addIngredient(WireMiseAgentHooks);
|
|
1299
|
+
}
|
|
1300
|
+
printNextSteps() {
|
|
1301
|
+
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
1302
|
+
console.log(" Next steps:");
|
|
1303
|
+
console.log(" 1. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
1304
|
+
console.log(" 2. git add .claude/settings.json .agents/hooks && commit (codex/kimi/hermes are per-dev)");
|
|
1305
|
+
console.log(" 3. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
1306
|
+
console.log(` 4. If you run a global agent system: echo '{"skills":{"defer_to_global":true}}' > .agents/local.json`);
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1150
1310
|
// src/utils/registry.ts
|
|
1151
1311
|
var RECIPE_REGISTRY = {
|
|
1152
1312
|
mise: {
|
|
@@ -1180,9 +1340,27 @@ var RECIPE_REGISTRY = {
|
|
|
1180
1340
|
"WireEmail",
|
|
1181
1341
|
"PrintHermesSummary"
|
|
1182
1342
|
]
|
|
1343
|
+
},
|
|
1344
|
+
"agent-hooks": {
|
|
1345
|
+
name: "agent-hooks",
|
|
1346
|
+
description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
|
|
1347
|
+
class: AgentHooksRecipe,
|
|
1348
|
+
commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
|
|
1183
1349
|
}
|
|
1184
1350
|
};
|
|
1185
1351
|
var COMMAND_REGISTRY = {
|
|
1352
|
+
CopyAgentHooksTree: {
|
|
1353
|
+
name: "CopyAgentHooksTree",
|
|
1354
|
+
description: "Copy the generic agent-hooks tree (hooks SSOT + sync engine + scripts) from the CommonProject template",
|
|
1355
|
+
group: "agent-hooks",
|
|
1356
|
+
class: CopyAgentHooksTree
|
|
1357
|
+
},
|
|
1358
|
+
WireMiseAgentHooks: {
|
|
1359
|
+
name: "WireMiseAgentHooks",
|
|
1360
|
+
description: "Merge agent-hooks enter/leave + tasks into an existing mise.toml (idempotent)",
|
|
1361
|
+
group: "agent-hooks",
|
|
1362
|
+
class: WireMiseAgentHooks
|
|
1363
|
+
},
|
|
1186
1364
|
AddDockerfile: {
|
|
1187
1365
|
name: "AddDockerfile",
|
|
1188
1366
|
description: "Create Dockerfile for containerization",
|
|
@@ -1261,10 +1439,10 @@ function createRecipe(name, context) {
|
|
|
1261
1439
|
}
|
|
1262
1440
|
|
|
1263
1441
|
// src/parity/index.ts
|
|
1264
|
-
import { existsSync as
|
|
1265
|
-
import { dirname as
|
|
1266
|
-
import { fileURLToPath as
|
|
1267
|
-
import { homedir as
|
|
1442
|
+
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, readFileSync as readFileSync3, readlinkSync, readdirSync, renameSync, symlinkSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4, chmodSync, copyFileSync } from "node:fs";
|
|
1443
|
+
import { dirname as dirname5, join as join8, relative, resolve } from "node:path";
|
|
1444
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1445
|
+
import { homedir as homedir4 } from "node:os";
|
|
1268
1446
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1269
1447
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
1270
1448
|
# agent files to the main AGENTS.md file.
|
|
@@ -1312,12 +1490,12 @@ description = "Force every versioned file up to the highest version"
|
|
|
1312
1490
|
run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
1313
1491
|
# <<< mise-versioning <<<`;
|
|
1314
1492
|
function resolvePjanglerRoot() {
|
|
1315
|
-
let dir =
|
|
1316
|
-
while (dir !==
|
|
1317
|
-
if (
|
|
1493
|
+
let dir = dirname5(fileURLToPath3(import.meta.url));
|
|
1494
|
+
while (dir !== dirname5(dir)) {
|
|
1495
|
+
if (existsSync7(join8(dir, "package.json")) && existsSync7(join8(dir, "templates", "commonproject", "copier.yml"))) {
|
|
1318
1496
|
return dir;
|
|
1319
1497
|
}
|
|
1320
|
-
dir =
|
|
1498
|
+
dir = dirname5(dir);
|
|
1321
1499
|
}
|
|
1322
1500
|
throw new Error("Unable to resolve pjangler root");
|
|
1323
1501
|
}
|
|
@@ -1325,17 +1503,17 @@ function normalizeNewlines(value) {
|
|
|
1325
1503
|
return value.replace(/\r\n/g, "\n");
|
|
1326
1504
|
}
|
|
1327
1505
|
function readText(path) {
|
|
1328
|
-
return normalizeNewlines(
|
|
1506
|
+
return normalizeNewlines(readFileSync3(path, "utf8"));
|
|
1329
1507
|
}
|
|
1330
1508
|
function safeReadText(path) {
|
|
1331
|
-
return
|
|
1509
|
+
return existsSync7(path) ? readText(path) : null;
|
|
1332
1510
|
}
|
|
1333
1511
|
function ensureParent(path) {
|
|
1334
|
-
|
|
1512
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
1335
1513
|
}
|
|
1336
1514
|
function writeText(path, content) {
|
|
1337
1515
|
ensureParent(path);
|
|
1338
|
-
|
|
1516
|
+
writeFileSync4(path, content);
|
|
1339
1517
|
}
|
|
1340
1518
|
function tryParseJson(text3) {
|
|
1341
1519
|
if (!text3) return null;
|
|
@@ -1352,7 +1530,7 @@ function titleCaseSlug(slug) {
|
|
|
1352
1530
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1353
1531
|
}
|
|
1354
1532
|
function readSymlinkTarget(path) {
|
|
1355
|
-
if (!
|
|
1533
|
+
if (!existsSync7(path)) return null;
|
|
1356
1534
|
try {
|
|
1357
1535
|
return readlinkSync(path);
|
|
1358
1536
|
} catch {
|
|
@@ -1360,7 +1538,7 @@ function readSymlinkTarget(path) {
|
|
|
1360
1538
|
}
|
|
1361
1539
|
}
|
|
1362
1540
|
function ensureSymlink(path, target, dryRun) {
|
|
1363
|
-
if (
|
|
1541
|
+
if (existsSync7(path)) {
|
|
1364
1542
|
const stat = lstatSync(path);
|
|
1365
1543
|
if (stat.isSymbolicLink()) {
|
|
1366
1544
|
const current = readSymlinkTarget(path);
|
|
@@ -1376,6 +1554,29 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
1376
1554
|
if (!dryRun) symlinkSync(target, path);
|
|
1377
1555
|
return { changed: true };
|
|
1378
1556
|
}
|
|
1557
|
+
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
1558
|
+
const agentsPath = join8(repoRoot, "AGENTS.md");
|
|
1559
|
+
if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
|
|
1560
|
+
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1561
|
+
const source = join8(repoRoot, file);
|
|
1562
|
+
if (!existsSync7(source)) continue;
|
|
1563
|
+
const stat = lstatSync(source);
|
|
1564
|
+
if (stat.isSymbolicLink()) continue;
|
|
1565
|
+
if (stat.isFile()) {
|
|
1566
|
+
if (!dryRun) renameSync(source, agentsPath);
|
|
1567
|
+
return { changedFiles: [agentsPath], details: [`Moved ${file} to AGENTS.md before wiring agent-file symlinks`] };
|
|
1568
|
+
}
|
|
1569
|
+
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
1570
|
+
}
|
|
1571
|
+
const readmePath = join8(repoRoot, "README.md");
|
|
1572
|
+
if (existsSync7(readmePath)) {
|
|
1573
|
+
const stat = lstatSync(readmePath);
|
|
1574
|
+
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
1575
|
+
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
1576
|
+
return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
|
|
1577
|
+
}
|
|
1578
|
+
return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
|
|
1579
|
+
}
|
|
1379
1580
|
function yamlGet(text3, keyPath) {
|
|
1380
1581
|
const parts = keyPath.split(".");
|
|
1381
1582
|
const lines = text3.split("\n");
|
|
@@ -1407,12 +1608,12 @@ function yamlGet(text3, keyPath) {
|
|
|
1407
1608
|
return "";
|
|
1408
1609
|
}
|
|
1409
1610
|
function discoverRoles(repoRoot) {
|
|
1410
|
-
const rolesDir =
|
|
1411
|
-
if (!
|
|
1611
|
+
const rolesDir = join8(repoRoot, "agents", "hermes");
|
|
1612
|
+
if (!existsSync7(rolesDir)) return [];
|
|
1412
1613
|
return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
1413
|
-
const roleDir =
|
|
1414
|
-
const roleYamlPath =
|
|
1415
|
-
if (!
|
|
1614
|
+
const roleDir = join8(rolesDir, entry.name);
|
|
1615
|
+
const roleYamlPath = join8(roleDir, "role.yaml");
|
|
1616
|
+
if (!existsSync7(roleYamlPath)) return null;
|
|
1416
1617
|
const text3 = readText(roleYamlPath);
|
|
1417
1618
|
const runtimeRepoRaw = yamlGet(text3, "runtime.github_repo");
|
|
1418
1619
|
return {
|
|
@@ -1436,7 +1637,7 @@ function discoverRoles(repoRoot) {
|
|
|
1436
1637
|
}).filter((value) => Boolean(value));
|
|
1437
1638
|
}
|
|
1438
1639
|
function registryPath(homeDir) {
|
|
1439
|
-
return
|
|
1640
|
+
return join8(homeDir, ".hermes", "agents-registry.yaml");
|
|
1440
1641
|
}
|
|
1441
1642
|
function systemctlUser(args) {
|
|
1442
1643
|
const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -1446,12 +1647,19 @@ function systemctlUser(args) {
|
|
|
1446
1647
|
stderr: result.stderr.trim()
|
|
1447
1648
|
};
|
|
1448
1649
|
}
|
|
1650
|
+
function templateScript(ctx, name) {
|
|
1651
|
+
const source = join8(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
1652
|
+
return existsSync7(source) ? readText(source) : void 0;
|
|
1653
|
+
}
|
|
1449
1654
|
function templateVersioningScript(ctx) {
|
|
1450
|
-
return
|
|
1655
|
+
return templateScript(ctx, "versioning.sh");
|
|
1656
|
+
}
|
|
1657
|
+
function templateLinkAgentfilesScript(ctx) {
|
|
1658
|
+
return templateScript(ctx, "link-agentfiles.sh");
|
|
1451
1659
|
}
|
|
1452
1660
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1453
|
-
const packageJson =
|
|
1454
|
-
return
|
|
1661
|
+
const packageJson = join8(repoRoot, "package.json");
|
|
1662
|
+
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";
|
|
1455
1663
|
}
|
|
1456
1664
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
1457
1665
|
if (startMarker.test(text3)) {
|
|
@@ -1470,29 +1678,64 @@ ${text3.slice(match.index)}`;
|
|
|
1470
1678
|
${block}
|
|
1471
1679
|
`;
|
|
1472
1680
|
}
|
|
1473
|
-
|
|
1681
|
+
var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
|
|
1682
|
+
var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/hermes"];
|
|
1683
|
+
function requiredMisePathEntries(ctx) {
|
|
1684
|
+
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
1685
|
+
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
1686
|
+
if (existsSync7(join8(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1687
|
+
}
|
|
1688
|
+
return required;
|
|
1689
|
+
}
|
|
1690
|
+
function upsertMisePath(text3, required = BASE_MISE_PATH_ENTRIES) {
|
|
1691
|
+
const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
1692
|
+
const envMatch = text3.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
|
|
1693
|
+
if (!envMatch || typeof envMatch.index !== "number") {
|
|
1694
|
+
return `[env]
|
|
1695
|
+
${render(required)}
|
|
1696
|
+
|
|
1697
|
+
${text3.replace(/^\s+/, "")}`;
|
|
1698
|
+
}
|
|
1699
|
+
const prefix = text3.slice(0, envMatch.index + envMatch[1].length);
|
|
1700
|
+
const section = envMatch[2];
|
|
1701
|
+
const suffix = text3.slice(envMatch.index + envMatch[1].length + section.length);
|
|
1702
|
+
const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
|
|
1703
|
+
if (!pathLine) {
|
|
1704
|
+
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
1705
|
+
}
|
|
1706
|
+
const current = [...pathLine[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
1707
|
+
const merged = [...current];
|
|
1708
|
+
for (const value of required) {
|
|
1709
|
+
if (!merged.includes(value)) merged.push(value);
|
|
1710
|
+
}
|
|
1711
|
+
const nextLine = render(merged);
|
|
1712
|
+
if (pathLine[0] === nextLine) return text3;
|
|
1713
|
+
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
1714
|
+
}
|
|
1715
|
+
function upsertLinkAgentfilesBlock(text3, ctx) {
|
|
1716
|
+
const withPath = upsertMisePath(text3, requiredMisePathEntries(ctx));
|
|
1474
1717
|
const existing = /# This block will handle the linking of[\s\S]*?\[tasks\.link-agentfiles\][\s\S]*?run = "\{\{config_root\}\}\/\.mise\/scripts\/link-agentfiles\.sh"/;
|
|
1475
|
-
if (existing.test(
|
|
1476
|
-
return
|
|
1718
|
+
if (existing.test(withPath)) {
|
|
1719
|
+
return withPath.replace(existing, LINK_AGENTFILES_BLOCK);
|
|
1477
1720
|
}
|
|
1478
|
-
const versioningIndex =
|
|
1721
|
+
const versioningIndex = withPath.indexOf("# >>> mise-versioning >>>");
|
|
1479
1722
|
if (versioningIndex >= 0) {
|
|
1480
|
-
return `${
|
|
1723
|
+
return `${withPath.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${LINK_AGENTFILES_BLOCK}
|
|
1481
1724
|
|
|
1482
|
-
${
|
|
1725
|
+
${withPath.slice(versioningIndex)}`;
|
|
1483
1726
|
}
|
|
1484
|
-
return `${
|
|
1727
|
+
return `${withPath.replace(/\s*$/, "")}
|
|
1485
1728
|
|
|
1486
1729
|
${LINK_AGENTFILES_BLOCK}
|
|
1487
1730
|
`;
|
|
1488
1731
|
}
|
|
1489
1732
|
function readProjectJson(ctx) {
|
|
1490
|
-
return tryParseJson(safeReadText(
|
|
1733
|
+
return tryParseJson(safeReadText(join8(ctx.repoRoot, ".project.json")));
|
|
1491
1734
|
}
|
|
1492
1735
|
function canonicalProjectJson(ctx) {
|
|
1493
1736
|
const roles = discoverRoles(ctx.repoRoot);
|
|
1494
1737
|
const existing = readProjectJson(ctx) ?? {};
|
|
1495
|
-
const slug = String(existing.project_slug ?? slugifyRepoName(
|
|
1738
|
+
const slug = String(existing.project_slug ?? slugifyRepoName(dirname5(ctx.repoRoot) === ctx.repoRoot ? ctx.repoRoot.split("/").pop() ?? "project" : ctx.repoRoot.split("/").pop() ?? "project"));
|
|
1496
1739
|
const firstRole = roles[0];
|
|
1497
1740
|
const ticketProvider = {
|
|
1498
1741
|
type: String((existing.ticket_provider?.type ?? firstRole?.ticketProviderName ?? "plane") || "plane"),
|
|
@@ -1519,12 +1762,12 @@ function canonicalProjectJson(ctx) {
|
|
|
1519
1762
|
};
|
|
1520
1763
|
}
|
|
1521
1764
|
function projectJsonFinding(ctx) {
|
|
1522
|
-
const projectPath =
|
|
1523
|
-
const planeJsonPath =
|
|
1765
|
+
const projectPath = join8(ctx.repoRoot, ".project.json");
|
|
1766
|
+
const planeJsonPath = join8(ctx.repoRoot, ".plane.json");
|
|
1524
1767
|
const details = [];
|
|
1525
1768
|
const data = readProjectJson(ctx);
|
|
1526
1769
|
const roles = discoverRoles(ctx.repoRoot);
|
|
1527
|
-
if (!
|
|
1770
|
+
if (!existsSync7(projectPath)) {
|
|
1528
1771
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
1529
1772
|
}
|
|
1530
1773
|
if (!data) {
|
|
@@ -1550,7 +1793,7 @@ function projectJsonFinding(ctx) {
|
|
|
1550
1793
|
for (const key of ["type", "workspace", "identifier", "board_id", "board_url"]) {
|
|
1551
1794
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
1552
1795
|
}
|
|
1553
|
-
if (
|
|
1796
|
+
if (existsSync7(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
1554
1797
|
return {
|
|
1555
1798
|
id: "sot.project-json",
|
|
1556
1799
|
title: "Canonical .project.json",
|
|
@@ -1562,8 +1805,8 @@ function projectJsonFinding(ctx) {
|
|
|
1562
1805
|
}
|
|
1563
1806
|
function renderSoul(role) {
|
|
1564
1807
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
1565
|
-
const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` :
|
|
1566
|
-
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
|
|
1808
|
+
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.";
|
|
1809
|
+
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.`;
|
|
1567
1810
|
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
1568
1811
|
return `# ${role.displayName || role.agentId}
|
|
1569
1812
|
|
|
@@ -1583,7 +1826,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
|
|
|
1583
1826
|
|
|
1584
1827
|
## Scope
|
|
1585
1828
|
|
|
1586
|
-
You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME
|
|
1829
|
+
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.
|
|
1587
1830
|
|
|
1588
1831
|
## Tone
|
|
1589
1832
|
|
|
@@ -1600,8 +1843,7 @@ Your memory is the submodule at \`./runtime/memories/\`. Use durable memory deli
|
|
|
1600
1843
|
}
|
|
1601
1844
|
function renderHermesWrapper(role) {
|
|
1602
1845
|
return `#!/usr/bin/env bash
|
|
1603
|
-
# Launcher for ${role.agentId}. Resolves HERMES_HOME
|
|
1604
|
-
# when available, then falls back to the local runtime submodule.
|
|
1846
|
+
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime submodule.
|
|
1605
1847
|
|
|
1606
1848
|
set -euo pipefail
|
|
1607
1849
|
|
|
@@ -1620,12 +1862,7 @@ CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
|
|
|
1620
1862
|
|
|
1621
1863
|
FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
1622
1864
|
PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
|
|
1623
|
-
|
|
1624
|
-
if [[ -d "$PROFILE_HOME" ]]; then
|
|
1625
|
-
HERMES_HOME="$PROFILE_HOME"
|
|
1626
|
-
else
|
|
1627
|
-
HERMES_HOME="$RUNTIME_HOME"
|
|
1628
|
-
fi
|
|
1865
|
+
HERMES_HOME="$RUNTIME_HOME"
|
|
1629
1866
|
|
|
1630
1867
|
if [[ ! -d "$RUNTIME_HOME" ]]; then
|
|
1631
1868
|
echo "hermes: runtime submodule not initialized at $RUNTIME_HOME" >&2
|
|
@@ -1637,17 +1874,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
1637
1874
|
`.replace(/\u0010/g, "$");
|
|
1638
1875
|
}
|
|
1639
1876
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
1640
|
-
if (!
|
|
1641
|
-
|
|
1877
|
+
if (!existsSync7(sourceDir)) return;
|
|
1878
|
+
mkdirSync5(targetDir, { recursive: true });
|
|
1642
1879
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
1643
|
-
const sourcePath =
|
|
1880
|
+
const sourcePath = join8(sourceDir, entry.name);
|
|
1644
1881
|
if (skip?.(sourcePath)) continue;
|
|
1645
|
-
const targetPath =
|
|
1882
|
+
const targetPath = join8(targetDir, entry.name);
|
|
1646
1883
|
if (entry.isDirectory()) {
|
|
1647
1884
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
1648
1885
|
continue;
|
|
1649
1886
|
}
|
|
1650
|
-
if (
|
|
1887
|
+
if (existsSync7(targetPath)) continue;
|
|
1651
1888
|
changedFiles.push(targetPath);
|
|
1652
1889
|
if (!dryRun) {
|
|
1653
1890
|
ensureParent(targetPath);
|
|
@@ -1656,7 +1893,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
1656
1893
|
}
|
|
1657
1894
|
}
|
|
1658
1895
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
1659
|
-
const gitmodulesPath =
|
|
1896
|
+
const gitmodulesPath = join8(repoRoot, ".gitmodules");
|
|
1660
1897
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
1661
1898
|
const owner = role.runtimeOwner || "delorenj";
|
|
1662
1899
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -1678,7 +1915,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
1678
1915
|
repo: ${role.repo}
|
|
1679
1916
|
role: ${role.role}
|
|
1680
1917
|
display_name: ${JSON.stringify(role.displayName || role.agentId)}
|
|
1681
|
-
project_path: ${ctxEscape(role.roleDir ?
|
|
1918
|
+
project_path: ${ctxEscape(role.roleDir ? dirname5(dirname5(dirname5(role.roleDir))) : "")}
|
|
1682
1919
|
role_dir: ${ctxEscape(role.roleDir)}
|
|
1683
1920
|
profile_name: ${role.profileName || role.agentId}
|
|
1684
1921
|
telegram:
|
|
@@ -1691,7 +1928,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
1691
1928
|
systemd:
|
|
1692
1929
|
gateway_unit: hermes-${role.agentId}-gateway.service
|
|
1693
1930
|
consumer_unit: hermes-${role.agentId}-consumer.service
|
|
1694
|
-
|
|
1931
|
+
heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
|
|
1695
1932
|
`;
|
|
1696
1933
|
const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
|
|
1697
1934
|
${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
|
|
@@ -1756,13 +1993,17 @@ var RULES = [
|
|
|
1756
1993
|
id: "mise.config-root",
|
|
1757
1994
|
title: "mise config_root + AGENTS link hooks",
|
|
1758
1995
|
audit: (ctx) => {
|
|
1759
|
-
const misePath =
|
|
1760
|
-
if (!
|
|
1996
|
+
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
1997
|
+
if (!existsSync7(misePath)) {
|
|
1761
1998
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
1762
1999
|
}
|
|
1763
2000
|
const text3 = readText(misePath);
|
|
1764
2001
|
const details = [];
|
|
1765
|
-
|
|
2002
|
+
const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2003
|
+
if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2004
|
+
const pathValues = [...(text3.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2005
|
+
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2006
|
+
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
1766
2007
|
if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
|
|
1767
2008
|
if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
|
|
1768
2009
|
if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
|
|
@@ -1777,25 +2018,37 @@ var RULES = [
|
|
|
1777
2018
|
};
|
|
1778
2019
|
},
|
|
1779
2020
|
migrate: (ctx, finding) => {
|
|
1780
|
-
const path =
|
|
2021
|
+
const path = join8(ctx.repoRoot, "mise.toml");
|
|
1781
2022
|
const changedFiles = [];
|
|
1782
|
-
if (!
|
|
2023
|
+
if (!existsSync7(path)) {
|
|
1783
2024
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; initialize mise first", changedFiles, details: [] };
|
|
1784
2025
|
}
|
|
1785
2026
|
let text3 = readText(path);
|
|
1786
|
-
const next = upsertLinkAgentfilesBlock(text3);
|
|
2027
|
+
const next = upsertLinkAgentfilesBlock(text3, ctx);
|
|
1787
2028
|
if (next !== text3) {
|
|
1788
2029
|
changedFiles.push(path);
|
|
1789
2030
|
if (!ctx.dryRun) writeText(path, next);
|
|
1790
2031
|
text3 = next;
|
|
1791
2032
|
}
|
|
2033
|
+
const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2034
|
+
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2035
|
+
if (expectedScript === void 0) {
|
|
2036
|
+
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: [] };
|
|
2037
|
+
}
|
|
2038
|
+
if (safeReadText(linkAgentfilesPath) !== expectedScript) {
|
|
2039
|
+
changedFiles.push(linkAgentfilesPath);
|
|
2040
|
+
if (!ctx.dryRun) {
|
|
2041
|
+
writeText(linkAgentfilesPath, expectedScript);
|
|
2042
|
+
chmodSync(linkAgentfilesPath, 493);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
1792
2045
|
return {
|
|
1793
2046
|
id: finding.id,
|
|
1794
2047
|
title: finding.title,
|
|
1795
2048
|
status: changedFiles.length ? "applied" : "noop",
|
|
1796
2049
|
summary: changedFiles.length ? "Updated mise AGENTS-linking contract" : "No changes required",
|
|
1797
2050
|
changedFiles,
|
|
1798
|
-
details: changedFiles.length ? ["Normalized hooks/watch_files/tasks.link-agentfiles block"] : []
|
|
2051
|
+
details: changedFiles.length ? ["Normalized hooks/watch_files/tasks.link-agentfiles block and script"] : []
|
|
1799
2052
|
};
|
|
1800
2053
|
}
|
|
1801
2054
|
},
|
|
@@ -1804,13 +2057,13 @@ var RULES = [
|
|
|
1804
2057
|
title: "managed mise versioning block",
|
|
1805
2058
|
audit: (ctx) => {
|
|
1806
2059
|
const details = [];
|
|
1807
|
-
const misePath =
|
|
1808
|
-
const versioningPath =
|
|
1809
|
-
const manifestPath =
|
|
2060
|
+
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
2061
|
+
const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2062
|
+
const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
|
|
1810
2063
|
const text3 = safeReadText(misePath);
|
|
1811
2064
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
1812
|
-
if (!
|
|
1813
|
-
if (!
|
|
2065
|
+
if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2066
|
+
if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
1814
2067
|
return {
|
|
1815
2068
|
id: "mise.versioning",
|
|
1816
2069
|
title: "managed mise versioning block",
|
|
@@ -1822,8 +2075,8 @@ var RULES = [
|
|
|
1822
2075
|
},
|
|
1823
2076
|
migrate: (ctx, finding) => {
|
|
1824
2077
|
const changedFiles = [];
|
|
1825
|
-
const misePath =
|
|
1826
|
-
if (!
|
|
2078
|
+
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
2079
|
+
if (!existsSync7(misePath)) {
|
|
1827
2080
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; cannot inject versioning block", changedFiles, details: [] };
|
|
1828
2081
|
}
|
|
1829
2082
|
const currentMise = readText(misePath);
|
|
@@ -1832,8 +2085,11 @@ var RULES = [
|
|
|
1832
2085
|
changedFiles.push(misePath);
|
|
1833
2086
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
1834
2087
|
}
|
|
1835
|
-
const versioningPath =
|
|
2088
|
+
const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
1836
2089
|
const expectedScript = templateVersioningScript(ctx);
|
|
2090
|
+
if (expectedScript === void 0) {
|
|
2091
|
+
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: [] };
|
|
2092
|
+
}
|
|
1837
2093
|
if (safeReadText(versioningPath) !== expectedScript) {
|
|
1838
2094
|
changedFiles.push(versioningPath);
|
|
1839
2095
|
if (!ctx.dryRun) {
|
|
@@ -1841,7 +2097,7 @@ var RULES = [
|
|
|
1841
2097
|
chmodSync(versioningPath, 493);
|
|
1842
2098
|
}
|
|
1843
2099
|
}
|
|
1844
|
-
const manifestPath =
|
|
2100
|
+
const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
|
|
1845
2101
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
1846
2102
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
1847
2103
|
changedFiles.push(manifestPath);
|
|
@@ -1861,13 +2117,24 @@ var RULES = [
|
|
|
1861
2117
|
id: "sot.agent-symlinks",
|
|
1862
2118
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
1863
2119
|
audit: (ctx) => {
|
|
1864
|
-
const agentsPath =
|
|
1865
|
-
if (!
|
|
1866
|
-
|
|
2120
|
+
const agentsPath = join8(ctx.repoRoot, "AGENTS.md");
|
|
2121
|
+
if (!existsSync7(agentsPath)) {
|
|
2122
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join8(ctx.repoRoot, file)));
|
|
2123
|
+
if (fallbackSources.length === 0) {
|
|
2124
|
+
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2125
|
+
}
|
|
2126
|
+
return {
|
|
2127
|
+
id: "sot.agent-symlinks",
|
|
2128
|
+
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2129
|
+
status: "fail",
|
|
2130
|
+
summary: "AGENTS.md missing but can be derived from existing project documentation",
|
|
2131
|
+
details: [`AGENTS.md can be created from ${fallbackSources[0]}`],
|
|
2132
|
+
fixable: true
|
|
2133
|
+
};
|
|
1867
2134
|
}
|
|
1868
2135
|
const details = [];
|
|
1869
2136
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1870
|
-
const full =
|
|
2137
|
+
const full = join8(ctx.repoRoot, file);
|
|
1871
2138
|
const target = readSymlinkTarget(full);
|
|
1872
2139
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
1873
2140
|
}
|
|
@@ -1883,22 +2150,26 @@ var RULES = [
|
|
|
1883
2150
|
migrate: (ctx, finding) => {
|
|
1884
2151
|
const changedFiles = [];
|
|
1885
2152
|
const details = [];
|
|
1886
|
-
|
|
1887
|
-
|
|
2153
|
+
const blockedDetails = [];
|
|
2154
|
+
const bootstrap = bootstrapAgentsFile(ctx.repoRoot, ctx.dryRun);
|
|
2155
|
+
changedFiles.push(...bootstrap.changedFiles);
|
|
2156
|
+
details.push(...bootstrap.details);
|
|
2157
|
+
if (bootstrap.blocked) {
|
|
2158
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
1888
2159
|
}
|
|
1889
2160
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1890
|
-
const full =
|
|
2161
|
+
const full = join8(ctx.repoRoot, file);
|
|
1891
2162
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
1892
|
-
if (result.blocked)
|
|
2163
|
+
if (result.blocked) blockedDetails.push(result.blocked);
|
|
1893
2164
|
if (result.changed) changedFiles.push(full);
|
|
1894
2165
|
}
|
|
1895
2166
|
return {
|
|
1896
2167
|
id: finding.id,
|
|
1897
2168
|
title: finding.title,
|
|
1898
|
-
status:
|
|
1899
|
-
summary:
|
|
2169
|
+
status: blockedDetails.length ? "blocked" : changedFiles.length ? "applied" : "noop",
|
|
2170
|
+
summary: blockedDetails.length ? "One or more files could not be replaced safely" : changedFiles.length ? "Symlink contract repaired" : "No changes required",
|
|
1900
2171
|
changedFiles,
|
|
1901
|
-
details
|
|
2172
|
+
details: [...details, ...blockedDetails]
|
|
1902
2173
|
};
|
|
1903
2174
|
}
|
|
1904
2175
|
},
|
|
@@ -1909,7 +2180,7 @@ var RULES = [
|
|
|
1909
2180
|
migrate: (ctx, finding) => {
|
|
1910
2181
|
const changedFiles = [];
|
|
1911
2182
|
const details = [];
|
|
1912
|
-
const path =
|
|
2183
|
+
const path = join8(ctx.repoRoot, ".project.json");
|
|
1913
2184
|
const existing = readProjectJson(ctx) ?? {};
|
|
1914
2185
|
const canonical = canonicalProjectJson(ctx);
|
|
1915
2186
|
const merged = { ...existing, ...canonical };
|
|
@@ -1919,10 +2190,10 @@ var RULES = [
|
|
|
1919
2190
|
changedFiles.push(path);
|
|
1920
2191
|
if (!ctx.dryRun) writeText(path, expected);
|
|
1921
2192
|
}
|
|
1922
|
-
const planeJson =
|
|
1923
|
-
if (
|
|
2193
|
+
const planeJson = join8(ctx.repoRoot, ".plane.json");
|
|
2194
|
+
if (existsSync7(planeJson)) {
|
|
1924
2195
|
const backup = `${planeJson}.migrated-backup`;
|
|
1925
|
-
if (
|
|
2196
|
+
if (existsSync7(backup)) {
|
|
1926
2197
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
1927
2198
|
} else {
|
|
1928
2199
|
changedFiles.push(backup);
|
|
@@ -1944,8 +2215,8 @@ var RULES = [
|
|
|
1944
2215
|
title: ".env.op + gitignore secrets contract",
|
|
1945
2216
|
audit: (ctx) => {
|
|
1946
2217
|
const details = [];
|
|
1947
|
-
const envOp = safeReadText(
|
|
1948
|
-
const gitignore = safeReadText(
|
|
2218
|
+
const envOp = safeReadText(join8(ctx.repoRoot, ".env.op"));
|
|
2219
|
+
const gitignore = safeReadText(join8(ctx.repoRoot, ".gitignore"));
|
|
1949
2220
|
if (!envOp) {
|
|
1950
2221
|
details.push(".env.op missing");
|
|
1951
2222
|
} else {
|
|
@@ -1970,12 +2241,12 @@ var RULES = [
|
|
|
1970
2241
|
migrate: (ctx, finding) => {
|
|
1971
2242
|
const changedFiles = [];
|
|
1972
2243
|
const details = [];
|
|
1973
|
-
const envOpPath =
|
|
1974
|
-
if (!
|
|
2244
|
+
const envOpPath = join8(ctx.repoRoot, ".env.op");
|
|
2245
|
+
if (!existsSync7(envOpPath)) {
|
|
1975
2246
|
changedFiles.push(envOpPath);
|
|
1976
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2247
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join8(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
1977
2248
|
}
|
|
1978
|
-
const gitignorePath =
|
|
2249
|
+
const gitignorePath = join8(ctx.repoRoot, ".gitignore");
|
|
1979
2250
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
1980
2251
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
1981
2252
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2002,7 +2273,7 @@ var RULES = [
|
|
|
2002
2273
|
title: ".copier-answers.yml provenance + drift report",
|
|
2003
2274
|
audit: (ctx) => {
|
|
2004
2275
|
const details = [];
|
|
2005
|
-
const path =
|
|
2276
|
+
const path = join8(ctx.repoRoot, ".copier-answers.yml");
|
|
2006
2277
|
const text3 = safeReadText(path);
|
|
2007
2278
|
const project = readProjectJson(ctx);
|
|
2008
2279
|
if (!text3) {
|
|
@@ -2033,12 +2304,12 @@ var RULES = [
|
|
|
2033
2304
|
const changedFiles = [];
|
|
2034
2305
|
const project = canonicalProjectJson(ctx);
|
|
2035
2306
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2036
|
-
_src_path: ${
|
|
2307
|
+
_src_path: ${join8(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2037
2308
|
project_description: ${String(project.project_description)}
|
|
2038
2309
|
project_name: ${String(project.project_name)}
|
|
2039
2310
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2040
2311
|
`;
|
|
2041
|
-
const path =
|
|
2312
|
+
const path = join8(ctx.repoRoot, ".copier-answers.yml");
|
|
2042
2313
|
if (safeReadText(path) !== text3) {
|
|
2043
2314
|
changedFiles.push(path);
|
|
2044
2315
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -2057,15 +2328,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2057
2328
|
id: "bmad.scaffold",
|
|
2058
2329
|
title: "BMAD modules/docs scaffold",
|
|
2059
2330
|
audit: (ctx) => {
|
|
2060
|
-
const sourceRoot =
|
|
2061
|
-
const targetRoot =
|
|
2331
|
+
const sourceRoot = join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2332
|
+
const targetRoot = join8(ctx.repoRoot, "_bmad");
|
|
2062
2333
|
const sentinels = [
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2334
|
+
join8("core", "config.yaml"),
|
|
2335
|
+
join8("custom", "config.yaml"),
|
|
2336
|
+
join8("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
2337
|
+
join8("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
2067
2338
|
];
|
|
2068
|
-
const missing = sentinels.filter((file) =>
|
|
2339
|
+
const missing = sentinels.filter((file) => existsSync7(join8(sourceRoot, file)) && !existsSync7(join8(targetRoot, file)));
|
|
2069
2340
|
return {
|
|
2070
2341
|
id: "bmad.scaffold",
|
|
2071
2342
|
title: "BMAD modules/docs scaffold",
|
|
@@ -2077,7 +2348,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2077
2348
|
},
|
|
2078
2349
|
migrate: (ctx, finding) => {
|
|
2079
2350
|
const changedFiles = [];
|
|
2080
|
-
copyMissingRecursive(
|
|
2351
|
+
copyMissingRecursive(join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join8(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2081
2352
|
return {
|
|
2082
2353
|
id: finding.id,
|
|
2083
2354
|
title: finding.title,
|
|
@@ -2098,12 +2369,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2098
2369
|
return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
|
|
2099
2370
|
}
|
|
2100
2371
|
const details = [];
|
|
2101
|
-
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/70-systemd.sh", ".runtime-scaffold/README.md", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
|
|
2102
|
-
if (!
|
|
2372
|
+
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"]) {
|
|
2373
|
+
if (!existsSync7(join8(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join8(role.roleDir, rel))}`);
|
|
2103
2374
|
}
|
|
2104
|
-
const gitmodules = safeReadText(
|
|
2375
|
+
const gitmodules = safeReadText(join8(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2105
2376
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2106
|
-
if (!profileMetaInheritsDefault(
|
|
2377
|
+
if (!profileMetaInheritsDefault(join8(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2107
2378
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2108
2379
|
}
|
|
2109
2380
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -2124,79 +2395,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2124
2395
|
if (!role) {
|
|
2125
2396
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2126
2397
|
}
|
|
2127
|
-
const templateRoleDir =
|
|
2128
|
-
writeIfDifferent(
|
|
2129
|
-
writeIfDifferent(
|
|
2130
|
-
writeIfDifferent(
|
|
2131
|
-
copyMissingRecursive(
|
|
2132
|
-
copyMissingRecursive(
|
|
2133
|
-
copyMissingRecursive(
|
|
2134
|
-
|
|
2135
|
-
const
|
|
2136
|
-
if (
|
|
2137
|
-
|
|
2138
|
-
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
2139
|
-
return {
|
|
2140
|
-
id: finding.id,
|
|
2141
|
-
title: finding.title,
|
|
2142
|
-
status: changedFiles.length ? "applied" : "noop",
|
|
2143
|
-
summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
|
|
2144
|
-
changedFiles,
|
|
2145
|
-
details
|
|
2146
|
-
};
|
|
2147
|
-
}
|
|
2148
|
-
},
|
|
2149
|
-
{
|
|
2150
|
-
id: "hermes.scrum-master-scaffold",
|
|
2151
|
-
title: "Hermes scrum-master scaffold parity",
|
|
2152
|
-
audit: (ctx) => {
|
|
2153
|
-
const roles = discoverRoles(ctx.repoRoot);
|
|
2154
|
-
const role = roles.find((item) => item.role === "scrum-master");
|
|
2155
|
-
if (!role) {
|
|
2156
|
-
return { id: "hermes.scrum-master-scaffold", title: "Hermes scrum-master scaffold parity", status: "skip", summary: "No scrum-master role present", details: [], fixable: false };
|
|
2157
|
-
}
|
|
2158
|
-
const details = [];
|
|
2159
|
-
for (const rel of ["role.yaml", "SOUL.md", "hermes", ".gitignore", ".scripts/75-scrum-master.sh", ".scripts/scrum-master/continuous-ticket-sentinel.sh", "runtime/memories/MEMORY.md", "runtime/bloodbank-consumer.py"]) {
|
|
2160
|
-
if (!existsSync5(join6(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join6(role.roleDir, rel))}`);
|
|
2161
|
-
}
|
|
2162
|
-
const gitmodules = safeReadText(join6(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2163
|
-
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing scrum-master runtime submodule entry");
|
|
2164
|
-
if (!profileMetaInheritsDefault(join6(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2165
|
-
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2166
|
-
}
|
|
2167
|
-
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
2168
|
-
if (!registry?.includes(`${role.agentId}:`)) details.push(`fleet registry missing ${role.agentId}`);
|
|
2169
|
-
return {
|
|
2170
|
-
id: "hermes.scrum-master-scaffold",
|
|
2171
|
-
title: "Hermes scrum-master scaffold parity",
|
|
2172
|
-
status: details.length === 0 ? "pass" : "fail",
|
|
2173
|
-
summary: details.length === 0 ? "scrum-master scaffold parity verified" : `${details.length} scrum-master scaffold issue(s) detected`,
|
|
2174
|
-
details,
|
|
2175
|
-
fixable: true
|
|
2176
|
-
};
|
|
2177
|
-
},
|
|
2178
|
-
migrate: (ctx, finding) => {
|
|
2179
|
-
const role = discoverRoles(ctx.repoRoot).find((item) => item.role === "scrum-master");
|
|
2180
|
-
const changedFiles = [];
|
|
2181
|
-
const details = [];
|
|
2182
|
-
if (!role) {
|
|
2183
|
-
return { id: finding.id, title: finding.title, status: "blocked", summary: "No scrum-master role present", changedFiles, details: [] };
|
|
2184
|
-
}
|
|
2185
|
-
const templateRoleDir = join6(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
2186
|
-
writeIfDifferent(join6(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
2187
|
-
writeIfDifferent(join6(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
2188
|
-
writeIfDifferent(join6(role.roleDir, ".gitignore"), readText(join6(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
2189
|
-
copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
2190
|
-
copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
2191
|
-
copyMissingRecursive(join6(templateRoleDir, ".scripts"), join6(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("continuous-ticket-sentinel.prompt.md.jinja"));
|
|
2192
|
-
const promptSource = join6(templateRoleDir, ".scripts", "scrum-master", "continuous-ticket-sentinel.prompt.md.jinja");
|
|
2193
|
-
const promptTarget = join6(role.roleDir, ".scripts", "scrum-master", "continuous-ticket-sentinel.prompt.md");
|
|
2194
|
-
if (!existsSync5(promptTarget)) {
|
|
2195
|
-
const prompt = readText(promptSource).replace(/\{\{ agent_id \}\}/g, role.agentId).replace(/\{\{ role \}\}/g, role.role).replace(/\{\{ target_repo \}\}/g, role.repo);
|
|
2398
|
+
const templateRoleDir = join8(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
2399
|
+
writeIfDifferent(join8(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
2400
|
+
writeIfDifferent(join8(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
2401
|
+
writeIfDifferent(join8(role.roleDir, ".gitignore"), readText(join8(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
2402
|
+
copyMissingRecursive(join8(templateRoleDir, ".runtime-scaffold"), join8(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
2403
|
+
copyMissingRecursive(join8(templateRoleDir, ".runtime-scaffold"), join8(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
2404
|
+
copyMissingRecursive(join8(templateRoleDir, ".scripts"), join8(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
2405
|
+
const promptSource = join8(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
2406
|
+
const promptTarget = join8(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
2407
|
+
if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
|
|
2408
|
+
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);
|
|
2196
2409
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2197
2410
|
}
|
|
2198
2411
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2199
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
2412
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join8(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2200
2413
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2201
2414
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2202
2415
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -2204,7 +2417,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2204
2417
|
id: finding.id,
|
|
2205
2418
|
title: finding.title,
|
|
2206
2419
|
status: changedFiles.length ? "applied" : "noop",
|
|
2207
|
-
summary: changedFiles.length ? "
|
|
2420
|
+
summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
|
|
2208
2421
|
changedFiles,
|
|
2209
2422
|
details
|
|
2210
2423
|
};
|
|
@@ -2224,14 +2437,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2224
2437
|
}
|
|
2225
2438
|
const details = [];
|
|
2226
2439
|
for (const role of roles) {
|
|
2227
|
-
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-
|
|
2440
|
+
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
|
|
2228
2441
|
const state = checkUnit(unit);
|
|
2229
2442
|
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
2230
2443
|
}
|
|
2231
|
-
if (role.role === "scrum-master") {
|
|
2232
|
-
const state = checkUnit(`hermes-${role.agentId}-continuous-ticket-sentinel.timer`);
|
|
2233
|
-
if (!state.enabled || !state.active) details.push(`hermes-${role.agentId}-continuous-ticket-sentinel.timer should be enabled+active`);
|
|
2234
|
-
}
|
|
2235
2444
|
}
|
|
2236
2445
|
return {
|
|
2237
2446
|
id: "systemd.sentinel",
|
|
@@ -2254,10 +2463,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2254
2463
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2255
2464
|
}
|
|
2256
2465
|
for (const role of roles) {
|
|
2257
|
-
const sysDir =
|
|
2258
|
-
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-
|
|
2259
|
-
|
|
2260
|
-
const allUnitsPresent = units.every((unit) => existsSync5(join6(sysDir, unit)));
|
|
2466
|
+
const sysDir = join8(ctx.homeDir, ".config", "systemd", "user");
|
|
2467
|
+
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
2468
|
+
const allUnitsPresent = units.every((unit) => existsSync7(join8(sysDir, unit)));
|
|
2261
2469
|
if (allUnitsPresent) {
|
|
2262
2470
|
if (ctx.dryRun) {
|
|
2263
2471
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -2269,8 +2477,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2269
2477
|
}
|
|
2270
2478
|
continue;
|
|
2271
2479
|
}
|
|
2272
|
-
for (const script of [
|
|
2273
|
-
if (!script || !
|
|
2480
|
+
for (const script of [join8(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
2481
|
+
if (!script || !existsSync7(script)) continue;
|
|
2274
2482
|
if (ctx.dryRun) {
|
|
2275
2483
|
details.push(`would run: bash ${script}`);
|
|
2276
2484
|
} else {
|
|
@@ -2306,7 +2514,7 @@ function runAudit(repoArg) {
|
|
|
2306
2514
|
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2307
2515
|
dryRun: true,
|
|
2308
2516
|
pjanglerRoot,
|
|
2309
|
-
homeDir:
|
|
2517
|
+
homeDir: homedir4()
|
|
2310
2518
|
};
|
|
2311
2519
|
const rules = RULES.map((rule) => rule.audit(ctx));
|
|
2312
2520
|
return {
|
|
@@ -2322,13 +2530,26 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2322
2530
|
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2323
2531
|
dryRun,
|
|
2324
2532
|
pjanglerRoot,
|
|
2325
|
-
homeDir:
|
|
2533
|
+
homeDir: homedir4()
|
|
2326
2534
|
};
|
|
2327
2535
|
const selected = all ? RULES : RULES.filter((rule) => rule.id === selector);
|
|
2328
2536
|
if (!selected.length) {
|
|
2329
2537
|
throw new Error(`Unknown parity rule: ${selector}`);
|
|
2330
2538
|
}
|
|
2331
|
-
const results = selected.map((rule) =>
|
|
2539
|
+
const results = selected.map((rule) => {
|
|
2540
|
+
try {
|
|
2541
|
+
return rule.migrate(ctx, rule.audit(ctx));
|
|
2542
|
+
} catch (err) {
|
|
2543
|
+
return {
|
|
2544
|
+
id: rule.id,
|
|
2545
|
+
title: rule.title,
|
|
2546
|
+
status: "blocked",
|
|
2547
|
+
summary: `migrate threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
2548
|
+
changedFiles: [],
|
|
2549
|
+
details: []
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2332
2553
|
const changedFiles = Array.from(new Set(results.flatMap((result) => result.changedFiles))).sort();
|
|
2333
2554
|
return {
|
|
2334
2555
|
repo: ctx.repoRoot,
|
|
@@ -2364,18 +2585,18 @@ function formatMigrationReport(report) {
|
|
|
2364
2585
|
}
|
|
2365
2586
|
|
|
2366
2587
|
// src/utils/version.ts
|
|
2367
|
-
import { readFileSync as
|
|
2368
|
-
import { dirname as
|
|
2369
|
-
import { fileURLToPath as
|
|
2588
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
2589
|
+
import { dirname as dirname6, join as join9 } from "node:path";
|
|
2590
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2370
2591
|
var PJANGLER_VERSION = (() => {
|
|
2371
2592
|
try {
|
|
2372
|
-
let dir =
|
|
2593
|
+
let dir = dirname6(fileURLToPath4(import.meta.url));
|
|
2373
2594
|
for (let i = 0; i < 4; i++) {
|
|
2374
2595
|
try {
|
|
2375
|
-
const raw =
|
|
2596
|
+
const raw = readFileSync4(join9(dir, "package.json"), "utf8");
|
|
2376
2597
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
2377
2598
|
} catch {
|
|
2378
|
-
const parent =
|
|
2599
|
+
const parent = dirname6(dir);
|
|
2379
2600
|
if (parent === dir) break;
|
|
2380
2601
|
dir = parent;
|
|
2381
2602
|
}
|