@delorenj/pjangler 1.1.5 → 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/dist/index.js +481 -277
- package/dist/mcp-server.js +1706 -163
- package/package.json +3 -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,13 +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
|
-
|
|
1451
|
-
|
|
1655
|
+
return templateScript(ctx, "versioning.sh");
|
|
1656
|
+
}
|
|
1657
|
+
function templateLinkAgentfilesScript(ctx) {
|
|
1658
|
+
return templateScript(ctx, "link-agentfiles.sh");
|
|
1452
1659
|
}
|
|
1453
1660
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1454
|
-
const packageJson =
|
|
1455
|
-
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";
|
|
1456
1663
|
}
|
|
1457
1664
|
function replaceOrAppendManagedBlock(text3, startMarker, block, beforePattern) {
|
|
1458
1665
|
if (startMarker.test(text3)) {
|
|
@@ -1471,29 +1678,64 @@ ${text3.slice(match.index)}`;
|
|
|
1471
1678
|
${block}
|
|
1472
1679
|
`;
|
|
1473
1680
|
}
|
|
1474
|
-
|
|
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));
|
|
1475
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"/;
|
|
1476
|
-
if (existing.test(
|
|
1477
|
-
return
|
|
1718
|
+
if (existing.test(withPath)) {
|
|
1719
|
+
return withPath.replace(existing, LINK_AGENTFILES_BLOCK);
|
|
1478
1720
|
}
|
|
1479
|
-
const versioningIndex =
|
|
1721
|
+
const versioningIndex = withPath.indexOf("# >>> mise-versioning >>>");
|
|
1480
1722
|
if (versioningIndex >= 0) {
|
|
1481
|
-
return `${
|
|
1723
|
+
return `${withPath.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${LINK_AGENTFILES_BLOCK}
|
|
1482
1724
|
|
|
1483
|
-
${
|
|
1725
|
+
${withPath.slice(versioningIndex)}`;
|
|
1484
1726
|
}
|
|
1485
|
-
return `${
|
|
1727
|
+
return `${withPath.replace(/\s*$/, "")}
|
|
1486
1728
|
|
|
1487
1729
|
${LINK_AGENTFILES_BLOCK}
|
|
1488
1730
|
`;
|
|
1489
1731
|
}
|
|
1490
1732
|
function readProjectJson(ctx) {
|
|
1491
|
-
return tryParseJson(safeReadText(
|
|
1733
|
+
return tryParseJson(safeReadText(join8(ctx.repoRoot, ".project.json")));
|
|
1492
1734
|
}
|
|
1493
1735
|
function canonicalProjectJson(ctx) {
|
|
1494
1736
|
const roles = discoverRoles(ctx.repoRoot);
|
|
1495
1737
|
const existing = readProjectJson(ctx) ?? {};
|
|
1496
|
-
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"));
|
|
1497
1739
|
const firstRole = roles[0];
|
|
1498
1740
|
const ticketProvider = {
|
|
1499
1741
|
type: String((existing.ticket_provider?.type ?? firstRole?.ticketProviderName ?? "plane") || "plane"),
|
|
@@ -1520,12 +1762,12 @@ function canonicalProjectJson(ctx) {
|
|
|
1520
1762
|
};
|
|
1521
1763
|
}
|
|
1522
1764
|
function projectJsonFinding(ctx) {
|
|
1523
|
-
const projectPath =
|
|
1524
|
-
const planeJsonPath =
|
|
1765
|
+
const projectPath = join8(ctx.repoRoot, ".project.json");
|
|
1766
|
+
const planeJsonPath = join8(ctx.repoRoot, ".plane.json");
|
|
1525
1767
|
const details = [];
|
|
1526
1768
|
const data = readProjectJson(ctx);
|
|
1527
1769
|
const roles = discoverRoles(ctx.repoRoot);
|
|
1528
|
-
if (!
|
|
1770
|
+
if (!existsSync7(projectPath)) {
|
|
1529
1771
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
1530
1772
|
}
|
|
1531
1773
|
if (!data) {
|
|
@@ -1551,7 +1793,7 @@ function projectJsonFinding(ctx) {
|
|
|
1551
1793
|
for (const key of ["type", "workspace", "identifier", "board_id", "board_url"]) {
|
|
1552
1794
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
1553
1795
|
}
|
|
1554
|
-
if (
|
|
1796
|
+
if (existsSync7(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
1555
1797
|
return {
|
|
1556
1798
|
id: "sot.project-json",
|
|
1557
1799
|
title: "Canonical .project.json",
|
|
@@ -1563,8 +1805,8 @@ function projectJsonFinding(ctx) {
|
|
|
1563
1805
|
}
|
|
1564
1806
|
function renderSoul(role) {
|
|
1565
1807
|
const telegram = role.botHandle ? `@${role.botHandle}` : "(unwired)";
|
|
1566
|
-
const tone = role.role === "pm" ? `Direct and brief. Decision-forward. No throat-clearing, no apologies, no "I'll help you with that" preambles.` :
|
|
1567
|
-
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.`;
|
|
1568
1810
|
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
1569
1811
|
return `# ${role.displayName || role.agentId}
|
|
1570
1812
|
|
|
@@ -1584,7 +1826,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
|
|
|
1584
1826
|
|
|
1585
1827
|
## Scope
|
|
1586
1828
|
|
|
1587
|
-
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.
|
|
1588
1830
|
|
|
1589
1831
|
## Tone
|
|
1590
1832
|
|
|
@@ -1601,8 +1843,7 @@ Your memory is the submodule at \`./runtime/memories/\`. Use durable memory deli
|
|
|
1601
1843
|
}
|
|
1602
1844
|
function renderHermesWrapper(role) {
|
|
1603
1845
|
return `#!/usr/bin/env bash
|
|
1604
|
-
# Launcher for ${role.agentId}. Resolves HERMES_HOME
|
|
1605
|
-
# when available, then falls back to the local runtime submodule.
|
|
1846
|
+
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime submodule.
|
|
1606
1847
|
|
|
1607
1848
|
set -euo pipefail
|
|
1608
1849
|
|
|
@@ -1621,12 +1862,7 @@ CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
|
|
|
1621
1862
|
|
|
1622
1863
|
FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
1623
1864
|
PROFILE_NAME="{HERMES_PROFILE_NAME:-${role.profileName || role.agentId}}"
|
|
1624
|
-
|
|
1625
|
-
if [[ -d "$PROFILE_HOME" ]]; then
|
|
1626
|
-
HERMES_HOME="$PROFILE_HOME"
|
|
1627
|
-
else
|
|
1628
|
-
HERMES_HOME="$RUNTIME_HOME"
|
|
1629
|
-
fi
|
|
1865
|
+
HERMES_HOME="$RUNTIME_HOME"
|
|
1630
1866
|
|
|
1631
1867
|
if [[ ! -d "$RUNTIME_HOME" ]]; then
|
|
1632
1868
|
echo "hermes: runtime submodule not initialized at $RUNTIME_HOME" >&2
|
|
@@ -1638,17 +1874,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
1638
1874
|
`.replace(/\u0010/g, "$");
|
|
1639
1875
|
}
|
|
1640
1876
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
1641
|
-
if (!
|
|
1642
|
-
|
|
1877
|
+
if (!existsSync7(sourceDir)) return;
|
|
1878
|
+
mkdirSync5(targetDir, { recursive: true });
|
|
1643
1879
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
1644
|
-
const sourcePath =
|
|
1880
|
+
const sourcePath = join8(sourceDir, entry.name);
|
|
1645
1881
|
if (skip?.(sourcePath)) continue;
|
|
1646
|
-
const targetPath =
|
|
1882
|
+
const targetPath = join8(targetDir, entry.name);
|
|
1647
1883
|
if (entry.isDirectory()) {
|
|
1648
1884
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
1649
1885
|
continue;
|
|
1650
1886
|
}
|
|
1651
|
-
if (
|
|
1887
|
+
if (existsSync7(targetPath)) continue;
|
|
1652
1888
|
changedFiles.push(targetPath);
|
|
1653
1889
|
if (!dryRun) {
|
|
1654
1890
|
ensureParent(targetPath);
|
|
@@ -1657,7 +1893,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
1657
1893
|
}
|
|
1658
1894
|
}
|
|
1659
1895
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
1660
|
-
const gitmodulesPath =
|
|
1896
|
+
const gitmodulesPath = join8(repoRoot, ".gitmodules");
|
|
1661
1897
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
1662
1898
|
const owner = role.runtimeOwner || "delorenj";
|
|
1663
1899
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -1679,7 +1915,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
1679
1915
|
repo: ${role.repo}
|
|
1680
1916
|
role: ${role.role}
|
|
1681
1917
|
display_name: ${JSON.stringify(role.displayName || role.agentId)}
|
|
1682
|
-
project_path: ${ctxEscape(role.roleDir ?
|
|
1918
|
+
project_path: ${ctxEscape(role.roleDir ? dirname5(dirname5(dirname5(role.roleDir))) : "")}
|
|
1683
1919
|
role_dir: ${ctxEscape(role.roleDir)}
|
|
1684
1920
|
profile_name: ${role.profileName || role.agentId}
|
|
1685
1921
|
telegram:
|
|
@@ -1692,7 +1928,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
1692
1928
|
systemd:
|
|
1693
1929
|
gateway_unit: hermes-${role.agentId}-gateway.service
|
|
1694
1930
|
consumer_unit: hermes-${role.agentId}-consumer.service
|
|
1695
|
-
|
|
1931
|
+
heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
|
|
1696
1932
|
`;
|
|
1697
1933
|
const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
|
|
1698
1934
|
${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
|
|
@@ -1757,13 +1993,17 @@ var RULES = [
|
|
|
1757
1993
|
id: "mise.config-root",
|
|
1758
1994
|
title: "mise config_root + AGENTS link hooks",
|
|
1759
1995
|
audit: (ctx) => {
|
|
1760
|
-
const misePath =
|
|
1761
|
-
if (!
|
|
1996
|
+
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
1997
|
+
if (!existsSync7(misePath)) {
|
|
1762
1998
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
1763
1999
|
}
|
|
1764
2000
|
const text3 = readText(misePath);
|
|
1765
2001
|
const details = [];
|
|
1766
|
-
|
|
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(", ")}`);
|
|
1767
2007
|
if (!text3.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
|
|
1768
2008
|
if (!text3.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
|
|
1769
2009
|
if (!text3.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
|
|
@@ -1778,25 +2018,37 @@ var RULES = [
|
|
|
1778
2018
|
};
|
|
1779
2019
|
},
|
|
1780
2020
|
migrate: (ctx, finding) => {
|
|
1781
|
-
const path =
|
|
2021
|
+
const path = join8(ctx.repoRoot, "mise.toml");
|
|
1782
2022
|
const changedFiles = [];
|
|
1783
|
-
if (!
|
|
2023
|
+
if (!existsSync7(path)) {
|
|
1784
2024
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; initialize mise first", changedFiles, details: [] };
|
|
1785
2025
|
}
|
|
1786
2026
|
let text3 = readText(path);
|
|
1787
|
-
const next = upsertLinkAgentfilesBlock(text3);
|
|
2027
|
+
const next = upsertLinkAgentfilesBlock(text3, ctx);
|
|
1788
2028
|
if (next !== text3) {
|
|
1789
2029
|
changedFiles.push(path);
|
|
1790
2030
|
if (!ctx.dryRun) writeText(path, next);
|
|
1791
2031
|
text3 = next;
|
|
1792
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
|
+
}
|
|
1793
2045
|
return {
|
|
1794
2046
|
id: finding.id,
|
|
1795
2047
|
title: finding.title,
|
|
1796
2048
|
status: changedFiles.length ? "applied" : "noop",
|
|
1797
2049
|
summary: changedFiles.length ? "Updated mise AGENTS-linking contract" : "No changes required",
|
|
1798
2050
|
changedFiles,
|
|
1799
|
-
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"] : []
|
|
1800
2052
|
};
|
|
1801
2053
|
}
|
|
1802
2054
|
},
|
|
@@ -1805,13 +2057,13 @@ var RULES = [
|
|
|
1805
2057
|
title: "managed mise versioning block",
|
|
1806
2058
|
audit: (ctx) => {
|
|
1807
2059
|
const details = [];
|
|
1808
|
-
const misePath =
|
|
1809
|
-
const versioningPath =
|
|
1810
|
-
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");
|
|
1811
2063
|
const text3 = safeReadText(misePath);
|
|
1812
2064
|
if (!text3?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
1813
|
-
if (!
|
|
1814
|
-
if (!
|
|
2065
|
+
if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2066
|
+
if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
1815
2067
|
return {
|
|
1816
2068
|
id: "mise.versioning",
|
|
1817
2069
|
title: "managed mise versioning block",
|
|
@@ -1823,8 +2075,8 @@ var RULES = [
|
|
|
1823
2075
|
},
|
|
1824
2076
|
migrate: (ctx, finding) => {
|
|
1825
2077
|
const changedFiles = [];
|
|
1826
|
-
const misePath =
|
|
1827
|
-
if (!
|
|
2078
|
+
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
2079
|
+
if (!existsSync7(misePath)) {
|
|
1828
2080
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing; cannot inject versioning block", changedFiles, details: [] };
|
|
1829
2081
|
}
|
|
1830
2082
|
const currentMise = readText(misePath);
|
|
@@ -1833,7 +2085,7 @@ var RULES = [
|
|
|
1833
2085
|
changedFiles.push(misePath);
|
|
1834
2086
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
1835
2087
|
}
|
|
1836
|
-
const versioningPath =
|
|
2088
|
+
const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
1837
2089
|
const expectedScript = templateVersioningScript(ctx);
|
|
1838
2090
|
if (expectedScript === void 0) {
|
|
1839
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: [] };
|
|
@@ -1845,7 +2097,7 @@ var RULES = [
|
|
|
1845
2097
|
chmodSync(versioningPath, 493);
|
|
1846
2098
|
}
|
|
1847
2099
|
}
|
|
1848
|
-
const manifestPath =
|
|
2100
|
+
const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
|
|
1849
2101
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
1850
2102
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
1851
2103
|
changedFiles.push(manifestPath);
|
|
@@ -1865,13 +2117,24 @@ var RULES = [
|
|
|
1865
2117
|
id: "sot.agent-symlinks",
|
|
1866
2118
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
1867
2119
|
audit: (ctx) => {
|
|
1868
|
-
const agentsPath =
|
|
1869
|
-
if (!
|
|
1870
|
-
|
|
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
|
+
};
|
|
1871
2134
|
}
|
|
1872
2135
|
const details = [];
|
|
1873
2136
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1874
|
-
const full =
|
|
2137
|
+
const full = join8(ctx.repoRoot, file);
|
|
1875
2138
|
const target = readSymlinkTarget(full);
|
|
1876
2139
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
1877
2140
|
}
|
|
@@ -1887,22 +2150,26 @@ var RULES = [
|
|
|
1887
2150
|
migrate: (ctx, finding) => {
|
|
1888
2151
|
const changedFiles = [];
|
|
1889
2152
|
const details = [];
|
|
1890
|
-
|
|
1891
|
-
|
|
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] };
|
|
1892
2159
|
}
|
|
1893
2160
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1894
|
-
const full =
|
|
2161
|
+
const full = join8(ctx.repoRoot, file);
|
|
1895
2162
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
1896
|
-
if (result.blocked)
|
|
2163
|
+
if (result.blocked) blockedDetails.push(result.blocked);
|
|
1897
2164
|
if (result.changed) changedFiles.push(full);
|
|
1898
2165
|
}
|
|
1899
2166
|
return {
|
|
1900
2167
|
id: finding.id,
|
|
1901
2168
|
title: finding.title,
|
|
1902
|
-
status:
|
|
1903
|
-
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",
|
|
1904
2171
|
changedFiles,
|
|
1905
|
-
details
|
|
2172
|
+
details: [...details, ...blockedDetails]
|
|
1906
2173
|
};
|
|
1907
2174
|
}
|
|
1908
2175
|
},
|
|
@@ -1913,7 +2180,7 @@ var RULES = [
|
|
|
1913
2180
|
migrate: (ctx, finding) => {
|
|
1914
2181
|
const changedFiles = [];
|
|
1915
2182
|
const details = [];
|
|
1916
|
-
const path =
|
|
2183
|
+
const path = join8(ctx.repoRoot, ".project.json");
|
|
1917
2184
|
const existing = readProjectJson(ctx) ?? {};
|
|
1918
2185
|
const canonical = canonicalProjectJson(ctx);
|
|
1919
2186
|
const merged = { ...existing, ...canonical };
|
|
@@ -1923,10 +2190,10 @@ var RULES = [
|
|
|
1923
2190
|
changedFiles.push(path);
|
|
1924
2191
|
if (!ctx.dryRun) writeText(path, expected);
|
|
1925
2192
|
}
|
|
1926
|
-
const planeJson =
|
|
1927
|
-
if (
|
|
2193
|
+
const planeJson = join8(ctx.repoRoot, ".plane.json");
|
|
2194
|
+
if (existsSync7(planeJson)) {
|
|
1928
2195
|
const backup = `${planeJson}.migrated-backup`;
|
|
1929
|
-
if (
|
|
2196
|
+
if (existsSync7(backup)) {
|
|
1930
2197
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
1931
2198
|
} else {
|
|
1932
2199
|
changedFiles.push(backup);
|
|
@@ -1948,8 +2215,8 @@ var RULES = [
|
|
|
1948
2215
|
title: ".env.op + gitignore secrets contract",
|
|
1949
2216
|
audit: (ctx) => {
|
|
1950
2217
|
const details = [];
|
|
1951
|
-
const envOp = safeReadText(
|
|
1952
|
-
const gitignore = safeReadText(
|
|
2218
|
+
const envOp = safeReadText(join8(ctx.repoRoot, ".env.op"));
|
|
2219
|
+
const gitignore = safeReadText(join8(ctx.repoRoot, ".gitignore"));
|
|
1953
2220
|
if (!envOp) {
|
|
1954
2221
|
details.push(".env.op missing");
|
|
1955
2222
|
} else {
|
|
@@ -1974,12 +2241,12 @@ var RULES = [
|
|
|
1974
2241
|
migrate: (ctx, finding) => {
|
|
1975
2242
|
const changedFiles = [];
|
|
1976
2243
|
const details = [];
|
|
1977
|
-
const envOpPath =
|
|
1978
|
-
if (!
|
|
2244
|
+
const envOpPath = join8(ctx.repoRoot, ".env.op");
|
|
2245
|
+
if (!existsSync7(envOpPath)) {
|
|
1979
2246
|
changedFiles.push(envOpPath);
|
|
1980
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2247
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join8(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
1981
2248
|
}
|
|
1982
|
-
const gitignorePath =
|
|
2249
|
+
const gitignorePath = join8(ctx.repoRoot, ".gitignore");
|
|
1983
2250
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
1984
2251
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
1985
2252
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2006,7 +2273,7 @@ var RULES = [
|
|
|
2006
2273
|
title: ".copier-answers.yml provenance + drift report",
|
|
2007
2274
|
audit: (ctx) => {
|
|
2008
2275
|
const details = [];
|
|
2009
|
-
const path =
|
|
2276
|
+
const path = join8(ctx.repoRoot, ".copier-answers.yml");
|
|
2010
2277
|
const text3 = safeReadText(path);
|
|
2011
2278
|
const project = readProjectJson(ctx);
|
|
2012
2279
|
if (!text3) {
|
|
@@ -2037,12 +2304,12 @@ var RULES = [
|
|
|
2037
2304
|
const changedFiles = [];
|
|
2038
2305
|
const project = canonicalProjectJson(ctx);
|
|
2039
2306
|
const text3 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2040
|
-
_src_path: ${
|
|
2307
|
+
_src_path: ${join8(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2041
2308
|
project_description: ${String(project.project_description)}
|
|
2042
2309
|
project_name: ${String(project.project_name)}
|
|
2043
2310
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2044
2311
|
`;
|
|
2045
|
-
const path =
|
|
2312
|
+
const path = join8(ctx.repoRoot, ".copier-answers.yml");
|
|
2046
2313
|
if (safeReadText(path) !== text3) {
|
|
2047
2314
|
changedFiles.push(path);
|
|
2048
2315
|
if (!ctx.dryRun) writeText(path, text3);
|
|
@@ -2061,15 +2328,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2061
2328
|
id: "bmad.scaffold",
|
|
2062
2329
|
title: "BMAD modules/docs scaffold",
|
|
2063
2330
|
audit: (ctx) => {
|
|
2064
|
-
const sourceRoot =
|
|
2065
|
-
const targetRoot =
|
|
2331
|
+
const sourceRoot = join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2332
|
+
const targetRoot = join8(ctx.repoRoot, "_bmad");
|
|
2066
2333
|
const sentinels = [
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
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")
|
|
2071
2338
|
];
|
|
2072
|
-
const missing = sentinels.filter((file) =>
|
|
2339
|
+
const missing = sentinels.filter((file) => existsSync7(join8(sourceRoot, file)) && !existsSync7(join8(targetRoot, file)));
|
|
2073
2340
|
return {
|
|
2074
2341
|
id: "bmad.scaffold",
|
|
2075
2342
|
title: "BMAD modules/docs scaffold",
|
|
@@ -2081,7 +2348,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2081
2348
|
},
|
|
2082
2349
|
migrate: (ctx, finding) => {
|
|
2083
2350
|
const changedFiles = [];
|
|
2084
|
-
copyMissingRecursive(
|
|
2351
|
+
copyMissingRecursive(join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join8(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2085
2352
|
return {
|
|
2086
2353
|
id: finding.id,
|
|
2087
2354
|
title: finding.title,
|
|
@@ -2102,12 +2369,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2102
2369
|
return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
|
|
2103
2370
|
}
|
|
2104
2371
|
const details = [];
|
|
2105
|
-
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"]) {
|
|
2106
|
-
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))}`);
|
|
2107
2374
|
}
|
|
2108
|
-
const gitmodules = safeReadText(
|
|
2375
|
+
const gitmodules = safeReadText(join8(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2109
2376
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2110
|
-
if (!profileMetaInheritsDefault(
|
|
2377
|
+
if (!profileMetaInheritsDefault(join8(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2111
2378
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2112
2379
|
}
|
|
2113
2380
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -2128,79 +2395,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2128
2395
|
if (!role) {
|
|
2129
2396
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2130
2397
|
}
|
|
2131
|
-
const templateRoleDir =
|
|
2132
|
-
writeIfDifferent(
|
|
2133
|
-
writeIfDifferent(
|
|
2134
|
-
writeIfDifferent(
|
|
2135
|
-
copyMissingRecursive(
|
|
2136
|
-
copyMissingRecursive(
|
|
2137
|
-
copyMissingRecursive(
|
|
2138
|
-
|
|
2139
|
-
const
|
|
2140
|
-
if (
|
|
2141
|
-
|
|
2142
|
-
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
2143
|
-
return {
|
|
2144
|
-
id: finding.id,
|
|
2145
|
-
title: finding.title,
|
|
2146
|
-
status: changedFiles.length ? "applied" : "noop",
|
|
2147
|
-
summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
|
|
2148
|
-
changedFiles,
|
|
2149
|
-
details
|
|
2150
|
-
};
|
|
2151
|
-
}
|
|
2152
|
-
},
|
|
2153
|
-
{
|
|
2154
|
-
id: "hermes.scrum-master-scaffold",
|
|
2155
|
-
title: "Hermes scrum-master scaffold parity",
|
|
2156
|
-
audit: (ctx) => {
|
|
2157
|
-
const roles = discoverRoles(ctx.repoRoot);
|
|
2158
|
-
const role = roles.find((item) => item.role === "scrum-master");
|
|
2159
|
-
if (!role) {
|
|
2160
|
-
return { id: "hermes.scrum-master-scaffold", title: "Hermes scrum-master scaffold parity", status: "skip", summary: "No scrum-master role present", details: [], fixable: false };
|
|
2161
|
-
}
|
|
2162
|
-
const details = [];
|
|
2163
|
-
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"]) {
|
|
2164
|
-
if (!existsSync5(join6(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join6(role.roleDir, rel))}`);
|
|
2165
|
-
}
|
|
2166
|
-
const gitmodules = safeReadText(join6(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2167
|
-
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing scrum-master runtime submodule entry");
|
|
2168
|
-
if (!profileMetaInheritsDefault(join6(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2169
|
-
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2170
|
-
}
|
|
2171
|
-
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
2172
|
-
if (!registry?.includes(`${role.agentId}:`)) details.push(`fleet registry missing ${role.agentId}`);
|
|
2173
|
-
return {
|
|
2174
|
-
id: "hermes.scrum-master-scaffold",
|
|
2175
|
-
title: "Hermes scrum-master scaffold parity",
|
|
2176
|
-
status: details.length === 0 ? "pass" : "fail",
|
|
2177
|
-
summary: details.length === 0 ? "scrum-master scaffold parity verified" : `${details.length} scrum-master scaffold issue(s) detected`,
|
|
2178
|
-
details,
|
|
2179
|
-
fixable: true
|
|
2180
|
-
};
|
|
2181
|
-
},
|
|
2182
|
-
migrate: (ctx, finding) => {
|
|
2183
|
-
const role = discoverRoles(ctx.repoRoot).find((item) => item.role === "scrum-master");
|
|
2184
|
-
const changedFiles = [];
|
|
2185
|
-
const details = [];
|
|
2186
|
-
if (!role) {
|
|
2187
|
-
return { id: finding.id, title: finding.title, status: "blocked", summary: "No scrum-master role present", changedFiles, details: [] };
|
|
2188
|
-
}
|
|
2189
|
-
const templateRoleDir = join6(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
2190
|
-
writeIfDifferent(join6(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
2191
|
-
writeIfDifferent(join6(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
2192
|
-
writeIfDifferent(join6(role.roleDir, ".gitignore"), readText(join6(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
2193
|
-
copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
2194
|
-
copyMissingRecursive(join6(templateRoleDir, ".runtime-scaffold"), join6(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
2195
|
-
copyMissingRecursive(join6(templateRoleDir, ".scripts"), join6(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("continuous-ticket-sentinel.prompt.md.jinja"));
|
|
2196
|
-
const promptSource = join6(templateRoleDir, ".scripts", "scrum-master", "continuous-ticket-sentinel.prompt.md.jinja");
|
|
2197
|
-
const promptTarget = join6(role.roleDir, ".scripts", "scrum-master", "continuous-ticket-sentinel.prompt.md");
|
|
2198
|
-
if (!existsSync5(promptTarget)) {
|
|
2199
|
-
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);
|
|
2200
2409
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2201
2410
|
}
|
|
2202
2411
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2203
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
2412
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join8(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2204
2413
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2205
2414
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2206
2415
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -2208,7 +2417,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2208
2417
|
id: finding.id,
|
|
2209
2418
|
title: finding.title,
|
|
2210
2419
|
status: changedFiles.length ? "applied" : "noop",
|
|
2211
|
-
summary: changedFiles.length ? "
|
|
2420
|
+
summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
|
|
2212
2421
|
changedFiles,
|
|
2213
2422
|
details
|
|
2214
2423
|
};
|
|
@@ -2228,14 +2437,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2228
2437
|
}
|
|
2229
2438
|
const details = [];
|
|
2230
2439
|
for (const role of roles) {
|
|
2231
|
-
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`]) {
|
|
2232
2441
|
const state = checkUnit(unit);
|
|
2233
2442
|
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
2234
2443
|
}
|
|
2235
|
-
if (role.role === "scrum-master") {
|
|
2236
|
-
const state = checkUnit(`hermes-${role.agentId}-continuous-ticket-sentinel.timer`);
|
|
2237
|
-
if (!state.enabled || !state.active) details.push(`hermes-${role.agentId}-continuous-ticket-sentinel.timer should be enabled+active`);
|
|
2238
|
-
}
|
|
2239
2444
|
}
|
|
2240
2445
|
return {
|
|
2241
2446
|
id: "systemd.sentinel",
|
|
@@ -2258,10 +2463,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2258
2463
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2259
2464
|
}
|
|
2260
2465
|
for (const role of roles) {
|
|
2261
|
-
const sysDir =
|
|
2262
|
-
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-
|
|
2263
|
-
|
|
2264
|
-
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)));
|
|
2265
2469
|
if (allUnitsPresent) {
|
|
2266
2470
|
if (ctx.dryRun) {
|
|
2267
2471
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -2273,8 +2477,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2273
2477
|
}
|
|
2274
2478
|
continue;
|
|
2275
2479
|
}
|
|
2276
|
-
for (const script of [
|
|
2277
|
-
if (!script || !
|
|
2480
|
+
for (const script of [join8(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
2481
|
+
if (!script || !existsSync7(script)) continue;
|
|
2278
2482
|
if (ctx.dryRun) {
|
|
2279
2483
|
details.push(`would run: bash ${script}`);
|
|
2280
2484
|
} else {
|
|
@@ -2310,7 +2514,7 @@ function runAudit(repoArg) {
|
|
|
2310
2514
|
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2311
2515
|
dryRun: true,
|
|
2312
2516
|
pjanglerRoot,
|
|
2313
|
-
homeDir:
|
|
2517
|
+
homeDir: homedir4()
|
|
2314
2518
|
};
|
|
2315
2519
|
const rules = RULES.map((rule) => rule.audit(ctx));
|
|
2316
2520
|
return {
|
|
@@ -2326,7 +2530,7 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2326
2530
|
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2327
2531
|
dryRun,
|
|
2328
2532
|
pjanglerRoot,
|
|
2329
|
-
homeDir:
|
|
2533
|
+
homeDir: homedir4()
|
|
2330
2534
|
};
|
|
2331
2535
|
const selected = all ? RULES : RULES.filter((rule) => rule.id === selector);
|
|
2332
2536
|
if (!selected.length) {
|
|
@@ -2381,18 +2585,18 @@ function formatMigrationReport(report) {
|
|
|
2381
2585
|
}
|
|
2382
2586
|
|
|
2383
2587
|
// src/utils/version.ts
|
|
2384
|
-
import { readFileSync as
|
|
2385
|
-
import { dirname as
|
|
2386
|
-
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";
|
|
2387
2591
|
var PJANGLER_VERSION = (() => {
|
|
2388
2592
|
try {
|
|
2389
|
-
let dir =
|
|
2593
|
+
let dir = dirname6(fileURLToPath4(import.meta.url));
|
|
2390
2594
|
for (let i = 0; i < 4; i++) {
|
|
2391
2595
|
try {
|
|
2392
|
-
const raw =
|
|
2596
|
+
const raw = readFileSync4(join9(dir, "package.json"), "utf8");
|
|
2393
2597
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
2394
2598
|
} catch {
|
|
2395
|
-
const parent =
|
|
2599
|
+
const parent = dirname6(dir);
|
|
2396
2600
|
if (parent === dir) break;
|
|
2397
2601
|
dir = parent;
|
|
2398
2602
|
}
|