@delorenj/pjangler 1.1.5 → 1.2.2
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 +1492 -358
- package/dist/mcp-server.js +2391 -290
- package/package.json +5 -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,24 +1,16 @@
|
|
|
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
|
|
4
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
6
|
+
import { basename as basename4, join as join11, resolve as resolve3 } from "node:path";
|
|
10
7
|
import { Command as Command3 } from "commander";
|
|
11
8
|
|
|
12
9
|
// src/commands/hermes/types.ts
|
|
13
10
|
var HERMES_AGENT_TEMPLATE = "gh:delorenj/hermes-agent-template";
|
|
14
11
|
var SOUL_TONES = ["direct", "playful", "formal", "terse"];
|
|
15
12
|
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
|
-
},
|
|
13
|
+
{ value: "pm", label: "Project Manager (pm)", hint: "triage, planning, ticket authorship, board reconciliation" },
|
|
22
14
|
{ value: "dev", label: "Developer (dev)", hint: "implements tickets" },
|
|
23
15
|
{ value: "review", label: "Reviewer (review)", hint: "adversarial code review" },
|
|
24
16
|
{ value: "ops", label: "Ops (ops)", hint: "deploy / infra" },
|
|
@@ -38,10 +30,12 @@ function deriveProfileName(repo, role) {
|
|
|
38
30
|
|
|
39
31
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
40
32
|
import { homedir, platform } from "node:os";
|
|
41
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
42
|
-
import { join, dirname } from "node:path";
|
|
33
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
34
|
+
import { join as join2, dirname as dirname2 } from "node:path";
|
|
43
35
|
|
|
44
36
|
// src/commands/Command.ts
|
|
37
|
+
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
38
|
+
import { join, dirname } from "path";
|
|
45
39
|
var Command = class {
|
|
46
40
|
context;
|
|
47
41
|
constructor(context) {
|
|
@@ -54,30 +48,24 @@ var Command = class {
|
|
|
54
48
|
return this.context.dryRun ? `[DRY RUN] ${message}` : message;
|
|
55
49
|
}
|
|
56
50
|
fileExists(filePath) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const fullPath = join8(this.context.targetDir, filePath);
|
|
60
|
-
return existsSync6(fullPath);
|
|
51
|
+
const fullPath = join(this.context.targetDir, filePath);
|
|
52
|
+
return existsSync(fullPath);
|
|
61
53
|
}
|
|
62
54
|
writeFile(filePath, content) {
|
|
63
55
|
if (this.context.dryRun) {
|
|
64
56
|
return;
|
|
65
57
|
}
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
mkdirSync4(dir, { recursive: true });
|
|
71
|
-
writeFileSync3(fullPath, content);
|
|
58
|
+
const fullPath = join(this.context.targetDir, filePath);
|
|
59
|
+
const dir = dirname(fullPath);
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
writeFileSync(fullPath, content);
|
|
72
62
|
}
|
|
73
63
|
createDirectory(dirPath) {
|
|
74
64
|
if (this.context.dryRun) {
|
|
75
65
|
return;
|
|
76
66
|
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const fullPath = join8(this.context.targetDir, dirPath);
|
|
80
|
-
mkdirSync4(fullPath, { recursive: true });
|
|
67
|
+
const fullPath = join(this.context.targetDir, dirPath);
|
|
68
|
+
mkdirSync(fullPath, { recursive: true });
|
|
81
69
|
}
|
|
82
70
|
};
|
|
83
71
|
|
|
@@ -86,26 +74,26 @@ function resolveTemplateConfigPath() {
|
|
|
86
74
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
87
75
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
88
76
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
89
|
-
const base = xdg && xdg.length ? xdg :
|
|
90
|
-
return
|
|
77
|
+
const base = xdg && xdg.length ? xdg : join2(homedir(), ".config");
|
|
78
|
+
return join2(base, "hermes-agent-template", "config.toml");
|
|
91
79
|
}
|
|
92
80
|
function detectHermesBin(home) {
|
|
93
81
|
const candidates = [
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
82
|
+
join2(home, "code", "hermes-agent", "venv", "bin", "hermes"),
|
|
83
|
+
join2(home, "code", "hermes-agent", ".venv", "bin", "hermes"),
|
|
84
|
+
join2(home, ".local", "bin", "hermes")
|
|
97
85
|
];
|
|
98
86
|
for (const c of candidates) {
|
|
99
|
-
if (
|
|
87
|
+
if (existsSync2(c)) return c;
|
|
100
88
|
}
|
|
101
89
|
return candidates[0];
|
|
102
90
|
}
|
|
103
91
|
function renderHostConfig() {
|
|
104
92
|
const home = homedir();
|
|
105
93
|
const hermesBin = detectHermesBin(home);
|
|
106
|
-
const hermesRepo =
|
|
107
|
-
const scaffoldDir =
|
|
108
|
-
const skillsDir =
|
|
94
|
+
const hermesRepo = join2(home, "code", "hermes-agent");
|
|
95
|
+
const scaffoldDir = join2(home, "code", "hermes-agent-template", "runtime-scaffold");
|
|
96
|
+
const skillsDir = join2(home, ".agents", "skills");
|
|
109
97
|
return `# hermes-agent-template \u2014 host configuration
|
|
110
98
|
# Bootstrapped by \`pjangler config bootstrap\` for $HOME=${home} (platform=${platform()}).
|
|
111
99
|
#
|
|
@@ -149,7 +137,7 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
149
137
|
const ctx = this.context;
|
|
150
138
|
const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
|
|
151
139
|
const path = resolveTemplateConfigPath();
|
|
152
|
-
const exists =
|
|
140
|
+
const exists = existsSync2(path);
|
|
153
141
|
if (exists && !force) {
|
|
154
142
|
console.log(`\u2713 Config present: ${path}`);
|
|
155
143
|
return { success: true, message: "" };
|
|
@@ -159,8 +147,8 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
159
147
|
return { success: true, message: "" };
|
|
160
148
|
}
|
|
161
149
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
151
|
+
writeFileSync2(path, renderHostConfig());
|
|
164
152
|
} catch (err) {
|
|
165
153
|
const msg = err instanceof Error ? err.message : String(err);
|
|
166
154
|
return { success: false, message: `\u2717 Failed to write ${path}: ${msg}` };
|
|
@@ -577,12 +565,12 @@ var NodeRecipe = class extends Recipe {
|
|
|
577
565
|
};
|
|
578
566
|
|
|
579
567
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
580
|
-
import { basename, join as
|
|
568
|
+
import { basename, join as join3 } from "node:path";
|
|
581
569
|
import { readFileSync } from "node:fs";
|
|
582
570
|
import * as p from "@clack/prompts";
|
|
583
571
|
function detectTicketProvider(targetDir) {
|
|
584
572
|
try {
|
|
585
|
-
const t = JSON.parse(readFileSync(
|
|
573
|
+
const t = JSON.parse(readFileSync(join3(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
586
574
|
return t === "plane" || t === "linear" || t === "trello" ? t : void 0;
|
|
587
575
|
} catch {
|
|
588
576
|
return void 0;
|
|
@@ -601,7 +589,6 @@ var PromptForAgentConfig = class extends Command {
|
|
|
601
589
|
ctx.modelProvider ??= "";
|
|
602
590
|
ctx.modelName ??= "";
|
|
603
591
|
ctx.ticketProvider ??= detectTicketProvider(ctx.targetDir) ?? "plane";
|
|
604
|
-
ctx.withScrumMaster ??= false;
|
|
605
592
|
ctx.skipTelegram ??= true;
|
|
606
593
|
ctx.skipEmail ??= true;
|
|
607
594
|
ctx.agentId = deriveAgentId(ctx.targetRepo, ctx.role);
|
|
@@ -647,14 +634,6 @@ var PromptForAgentConfig = class extends Command {
|
|
|
647
634
|
if (p.isCancel(answer)) return this.cancelled();
|
|
648
635
|
ctx.ticketProvider = answer;
|
|
649
636
|
}
|
|
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
637
|
if (!ctx.agentPurpose) {
|
|
659
638
|
const answer = await p.text({
|
|
660
639
|
message: "One-line purpose",
|
|
@@ -727,21 +706,21 @@ var PromptForAgentConfig = class extends Command {
|
|
|
727
706
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
728
707
|
import { spawnSync } from "node:child_process";
|
|
729
708
|
import { homedir as homedir2 } from "node:os";
|
|
730
|
-
import { join as
|
|
731
|
-
import { existsSync as
|
|
709
|
+
import { join as join4, dirname as dirname3 } from "node:path";
|
|
710
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
732
711
|
import { fileURLToPath } from "node:url";
|
|
733
712
|
import * as p2 from "@clack/prompts";
|
|
734
713
|
function resolveVendoredTemplate(name) {
|
|
735
714
|
let dir;
|
|
736
715
|
try {
|
|
737
|
-
dir =
|
|
716
|
+
dir = dirname3(fileURLToPath(import.meta.url));
|
|
738
717
|
} catch {
|
|
739
718
|
return void 0;
|
|
740
719
|
}
|
|
741
720
|
for (let i = 0; i < 8; i++) {
|
|
742
|
-
const candidate =
|
|
743
|
-
if (
|
|
744
|
-
const parent =
|
|
721
|
+
const candidate = join4(dir, "templates", name);
|
|
722
|
+
if (existsSync3(join4(candidate, "copier.yml"))) return candidate;
|
|
723
|
+
const parent = dirname3(dir);
|
|
745
724
|
if (parent === dir) break;
|
|
746
725
|
dir = parent;
|
|
747
726
|
}
|
|
@@ -753,14 +732,13 @@ var RunCopierTemplate = class extends Command {
|
|
|
753
732
|
const { targetRepo, role, agentPurpose, soulTone, modelProvider, modelName } = ctx;
|
|
754
733
|
const ticketProvider = ctx.ticketProvider ?? "plane";
|
|
755
734
|
const profileName = ctx.profileName ?? (targetRepo && role ? deriveProfileName(targetRepo, role) : void 0);
|
|
756
|
-
const withScrumMaster = role === "pm" && ctx.withScrumMaster === true;
|
|
757
735
|
if (!targetRepo || !role) {
|
|
758
736
|
return {
|
|
759
737
|
success: false,
|
|
760
738
|
message: "PromptForAgentConfig must run before RunCopierTemplate (targetRepo/role unset)"
|
|
761
739
|
};
|
|
762
740
|
}
|
|
763
|
-
const roleDir =
|
|
741
|
+
const roleDir = join4(ctx.targetDir, "agents", "hermes", role);
|
|
764
742
|
ctx.roleDir = roleDir;
|
|
765
743
|
ctx.runtimeRepo = `delorenj/agent-hm-${targetRepo}-${role}`;
|
|
766
744
|
const which = spawnSync("which", ["copier"], { encoding: "utf8" });
|
|
@@ -770,7 +748,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
770
748
|
message: "\u2717 copier not found on PATH. Install with: `uv tool install copier` or `pip install copier`"
|
|
771
749
|
};
|
|
772
750
|
}
|
|
773
|
-
if (
|
|
751
|
+
if (existsSync3(join4(roleDir, "role.yaml")) && !ctx.force) {
|
|
774
752
|
if (ctx.yes) {
|
|
775
753
|
ctx.force = true;
|
|
776
754
|
} else {
|
|
@@ -797,9 +775,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
797
775
|
SKIP_BLOODBANK: ctx.skipBloodbank ? "1" : "0",
|
|
798
776
|
SKIP_SYSTEMD: ctx.skipSystemd ? "1" : "0"
|
|
799
777
|
};
|
|
800
|
-
const LOCAL_TEMPLATE =
|
|
778
|
+
const LOCAL_TEMPLATE = join4(homedir2(), "code", "hermes-agent-template");
|
|
801
779
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
802
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (
|
|
780
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync3(join4(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
803
781
|
const args = [
|
|
804
782
|
"copy",
|
|
805
783
|
templateSrc,
|
|
@@ -820,8 +798,6 @@ var RunCopierTemplate = class extends Command {
|
|
|
820
798
|
`soul_tone=${soulTone ?? "direct"}`,
|
|
821
799
|
"--data",
|
|
822
800
|
`ticket_provider=${ticketProvider}`,
|
|
823
|
-
"--data",
|
|
824
|
-
`with_scrum_master=${withScrumMaster}`,
|
|
825
801
|
"--trust",
|
|
826
802
|
"--vcs-ref=HEAD"
|
|
827
803
|
];
|
|
@@ -832,7 +808,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
832
808
|
message: this.formatMessage(`Would run: copier ${args.join(" ")}`)
|
|
833
809
|
};
|
|
834
810
|
}
|
|
835
|
-
|
|
811
|
+
mkdirSync3(join4(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
836
812
|
const spinner4 = p2.spinner();
|
|
837
813
|
spinner4.start(`Running copier copy (target: agents/hermes/${role})`);
|
|
838
814
|
const result = spawnSync("copier", args, {
|
|
@@ -857,8 +833,8 @@ var RunCopierTemplate = class extends Command {
|
|
|
857
833
|
|
|
858
834
|
// src/commands/hermes/WireTelegram.ts
|
|
859
835
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
860
|
-
import { join as
|
|
861
|
-
import { existsSync as
|
|
836
|
+
import { join as join5 } from "node:path";
|
|
837
|
+
import { existsSync as existsSync4, unlinkSync } from "node:fs";
|
|
862
838
|
import * as p3 from "@clack/prompts";
|
|
863
839
|
var WireTelegram = class extends Command {
|
|
864
840
|
async invoke() {
|
|
@@ -946,15 +922,15 @@ var WireTelegram = class extends Command {
|
|
|
946
922
|
if (p3.isCancel(allowedAnswer)) {
|
|
947
923
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
948
924
|
}
|
|
949
|
-
const script =
|
|
950
|
-
if (!
|
|
925
|
+
const script = join5(roleDir, ".scripts", "30-telegram.sh");
|
|
926
|
+
if (!existsSync4(script)) {
|
|
951
927
|
return {
|
|
952
928
|
success: false,
|
|
953
929
|
message: `\u2717 ${script} not found. Did copier finish? Re-run with --skip-runtime-repo=0 if you skipped it.`
|
|
954
930
|
};
|
|
955
931
|
}
|
|
956
|
-
const marker =
|
|
957
|
-
if (
|
|
932
|
+
const marker = join5(roleDir, ".scripts", ".done-30-telegram");
|
|
933
|
+
if (existsSync4(marker)) unlinkSync(marker);
|
|
958
934
|
const spinner4 = p3.spinner();
|
|
959
935
|
spinner4.start("Verifying token + wiring profile");
|
|
960
936
|
const result = spawnSync2("bash", [script], {
|
|
@@ -981,8 +957,8 @@ function cap(s) {
|
|
|
981
957
|
|
|
982
958
|
// src/commands/hermes/WireEmail.ts
|
|
983
959
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
984
|
-
import { join as
|
|
985
|
-
import { existsSync as
|
|
960
|
+
import { join as join6 } from "node:path";
|
|
961
|
+
import { existsSync as existsSync5, unlinkSync as unlinkSync2 } from "node:fs";
|
|
986
962
|
import * as p4 from "@clack/prompts";
|
|
987
963
|
var WireEmail = class extends Command {
|
|
988
964
|
async invoke() {
|
|
@@ -997,8 +973,8 @@ var WireEmail = class extends Command {
|
|
|
997
973
|
if (!targetRepo || !role || !roleDir) {
|
|
998
974
|
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
999
975
|
}
|
|
1000
|
-
const script =
|
|
1001
|
-
if (!
|
|
976
|
+
const script = join6(roleDir, ".scripts", "50-email.sh");
|
|
977
|
+
if (!existsSync5(script)) {
|
|
1002
978
|
return { success: false, message: `\u2717 ${script} not found` };
|
|
1003
979
|
}
|
|
1004
980
|
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
@@ -1060,8 +1036,8 @@ var WireEmail = class extends Command {
|
|
|
1060
1036
|
}
|
|
1061
1037
|
}
|
|
1062
1038
|
}
|
|
1063
|
-
const marker =
|
|
1064
|
-
if (
|
|
1039
|
+
const marker = join6(roleDir, ".scripts", ".done-50-email");
|
|
1040
|
+
if (existsSync5(marker)) unlinkSync2(marker);
|
|
1065
1041
|
const spinner4 = p4.spinner();
|
|
1066
1042
|
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
1067
1043
|
const result = spawnSync3("bash", [script], {
|
|
@@ -1090,7 +1066,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1090
1066
|
const email = `${targetRepo}-${role}@delo.sh`;
|
|
1091
1067
|
const gw = `hermes-${agentId}-gateway.service`;
|
|
1092
1068
|
const csm = `hermes-${agentId}-consumer.service`;
|
|
1093
|
-
const
|
|
1069
|
+
const hb = `hermes-${agentId}-heartbeat.timer`;
|
|
1094
1070
|
const lines = [];
|
|
1095
1071
|
lines.push(`agent_id ${agentId}`);
|
|
1096
1072
|
lines.push(`role dir ${ctx.roleDir}`);
|
|
@@ -1100,7 +1076,7 @@ var PrintHermesSummary = class extends Command {
|
|
|
1100
1076
|
lines.push("");
|
|
1101
1077
|
lines.push("Start daemons:");
|
|
1102
1078
|
lines.push(` systemctl --user start ${csm}`);
|
|
1103
|
-
lines.push(` systemctl --user start ${
|
|
1079
|
+
lines.push(` systemctl --user start ${hb}`);
|
|
1104
1080
|
if (!skipTelegram) {
|
|
1105
1081
|
lines.push(` systemctl --user start ${gw}`);
|
|
1106
1082
|
} else {
|
|
@@ -1147,6 +1123,193 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
1147
1123
|
}
|
|
1148
1124
|
};
|
|
1149
1125
|
|
|
1126
|
+
// src/commands/AgentHooksCommands.ts
|
|
1127
|
+
import { homedir as homedir3 } from "node:os";
|
|
1128
|
+
import { join as join7, dirname as dirname4 } from "node:path";
|
|
1129
|
+
import { existsSync as existsSync6, cpSync, mkdirSync as mkdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1130
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1131
|
+
function resolveTemplateRoot() {
|
|
1132
|
+
const candidates = [];
|
|
1133
|
+
if (process.env.PJANGLER_COMMONPROJECT_TEMPLATE) {
|
|
1134
|
+
candidates.push(process.env.PJANGLER_COMMONPROJECT_TEMPLATE);
|
|
1135
|
+
}
|
|
1136
|
+
try {
|
|
1137
|
+
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
1138
|
+
for (let i = 0; i < 8; i++) {
|
|
1139
|
+
candidates.push(join7(dir, "templates", "commonproject", "template"));
|
|
1140
|
+
const parent = dirname4(dir);
|
|
1141
|
+
if (parent === dir) break;
|
|
1142
|
+
dir = parent;
|
|
1143
|
+
}
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
candidates.push(join7(homedir3(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
1147
|
+
for (const c of candidates) {
|
|
1148
|
+
if (existsSync6(join7(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
1149
|
+
}
|
|
1150
|
+
throw new Error(
|
|
1151
|
+
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
var CopyAgentHooksTree = class extends Command {
|
|
1155
|
+
async invoke() {
|
|
1156
|
+
let templateRoot;
|
|
1157
|
+
try {
|
|
1158
|
+
templateRoot = resolveTemplateRoot();
|
|
1159
|
+
} catch (e) {
|
|
1160
|
+
return { success: false, message: `\u26A0\uFE0F ${e.message}` };
|
|
1161
|
+
}
|
|
1162
|
+
const items = [
|
|
1163
|
+
{ rel: ".agents/hooks", dir: true },
|
|
1164
|
+
{ rel: ".agents/local.example.json", dir: false },
|
|
1165
|
+
{ rel: ".mise/scripts/link-project-skills-to-clis.sh", dir: false },
|
|
1166
|
+
{ rel: ".mise/scripts/unlink-project-skills-from-clis.sh", dir: false },
|
|
1167
|
+
{ rel: ".mise/scripts/hindsight-setup.sh", dir: false }
|
|
1168
|
+
];
|
|
1169
|
+
const created = [];
|
|
1170
|
+
const skipped = [];
|
|
1171
|
+
for (const { rel, dir } of items) {
|
|
1172
|
+
const src = join7(templateRoot, rel);
|
|
1173
|
+
const dest = join7(this.context.targetDir, rel);
|
|
1174
|
+
if (!existsSync6(src)) continue;
|
|
1175
|
+
if (existsSync6(dest) && !this.context.force) {
|
|
1176
|
+
skipped.push(rel);
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (!this.context.dryRun) {
|
|
1180
|
+
mkdirSync4(dirname4(dest), { recursive: true });
|
|
1181
|
+
cpSync(src, dest, { recursive: dir, force: true });
|
|
1182
|
+
}
|
|
1183
|
+
created.push(rel);
|
|
1184
|
+
}
|
|
1185
|
+
const verb = this.context.dryRun ? "Would copy" : "Copied";
|
|
1186
|
+
const tail = skipped.length ? ` (${skipped.length} already present \u2014 use --force to overwrite)` : "";
|
|
1187
|
+
return {
|
|
1188
|
+
success: created.length > 0,
|
|
1189
|
+
message: this.formatMessage(`\u2705 ${verb} ${created.length} agent-hooks path(s)${tail}`)
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
1194
|
+
static MARKER = "# pjangler:agent-hooks";
|
|
1195
|
+
static CR = "{{config_root}}";
|
|
1196
|
+
// mise's own runtime var — emitted literally
|
|
1197
|
+
async invoke() {
|
|
1198
|
+
const misePath = join7(this.context.targetDir, "mise.toml");
|
|
1199
|
+
if (!existsSync6(misePath)) {
|
|
1200
|
+
return {
|
|
1201
|
+
success: false,
|
|
1202
|
+
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
let content = readFileSync2(misePath, "utf8");
|
|
1206
|
+
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
1207
|
+
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
1208
|
+
}
|
|
1209
|
+
const cr = _WireMiseAgentHooks.CR;
|
|
1210
|
+
const enterAdds = [
|
|
1211
|
+
` "${cr}/.mise/scripts/link-project-skills-to-clis.sh",`,
|
|
1212
|
+
` "${cr}/.agents/hooks/sync.py --install --quiet",`
|
|
1213
|
+
].join("\n");
|
|
1214
|
+
const leaveBlock = [
|
|
1215
|
+
"leave = [",
|
|
1216
|
+
` "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",`,
|
|
1217
|
+
` "${cr}/.agents/hooks/sync.py --uninstall --quiet",`,
|
|
1218
|
+
"]"
|
|
1219
|
+
].join("\n");
|
|
1220
|
+
let wiredHooks = false;
|
|
1221
|
+
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
1222
|
+
if (enterRe.test(content)) {
|
|
1223
|
+
content = content.replace(enterRe, (_m, head, close) => {
|
|
1224
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
1225
|
+
return `${head}${sep}
|
|
1226
|
+
${enterAdds}${close}`;
|
|
1227
|
+
});
|
|
1228
|
+
const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
1229
|
+
if (leaveRe.test(content)) {
|
|
1230
|
+
content = content.replace(leaveRe, (_m, head, close) => {
|
|
1231
|
+
const sep = /[,[]\s*$/.test(head) ? "" : ",";
|
|
1232
|
+
return `${head}${sep}
|
|
1233
|
+
"${cr}/.mise/scripts/unlink-project-skills-from-clis.sh",
|
|
1234
|
+
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
1235
|
+
});
|
|
1236
|
+
} else {
|
|
1237
|
+
content = content.replace(enterRe, (m) => `${m}
|
|
1238
|
+
${leaveBlock}`);
|
|
1239
|
+
}
|
|
1240
|
+
wiredHooks = true;
|
|
1241
|
+
}
|
|
1242
|
+
const appended = [
|
|
1243
|
+
"",
|
|
1244
|
+
_WireMiseAgentHooks.MARKER + " (generated \u2014 see .agents/hooks/README.md)",
|
|
1245
|
+
"[[watch_files]]",
|
|
1246
|
+
'patterns = [".agents/hooks/hooks.master.json"]',
|
|
1247
|
+
'task = "hooks-sync"',
|
|
1248
|
+
"",
|
|
1249
|
+
"[tasks.hooks-sync]",
|
|
1250
|
+
'description = "Fan out hooks.master.json to each agent CLI (claude/codex/kimi/hermes)"',
|
|
1251
|
+
`run = "${cr}/.agents/hooks/sync.py --install"`,
|
|
1252
|
+
"",
|
|
1253
|
+
"[tasks.hooks-check]",
|
|
1254
|
+
'description = "Drift gate: verify generated hook configs match hooks.master.json"',
|
|
1255
|
+
`run = "${cr}/.agents/hooks/sync.py --check"`,
|
|
1256
|
+
"",
|
|
1257
|
+
"[tasks.hooks-uninstall]",
|
|
1258
|
+
'description = "Remove per-user agent-hook injections (codex/kimi/hermes)"',
|
|
1259
|
+
`run = "${cr}/.agents/hooks/sync.py --uninstall"`,
|
|
1260
|
+
"",
|
|
1261
|
+
"[tasks.link-project-skills-to-clis]",
|
|
1262
|
+
'description = "Fan .agents/skills out to each agent CLI (honors local.json)"',
|
|
1263
|
+
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1264
|
+
"",
|
|
1265
|
+
"[tasks.unlink-project-skills-from-clis]",
|
|
1266
|
+
'description = "Remove project skill symlinks from shared per-CLI dirs"',
|
|
1267
|
+
`run = "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh"`,
|
|
1268
|
+
"",
|
|
1269
|
+
"[tasks.skills-relink]",
|
|
1270
|
+
'description = "Re-fan the project skill set to all CLIs"',
|
|
1271
|
+
`run = "${cr}/.mise/scripts/link-project-skills-to-clis.sh"`,
|
|
1272
|
+
"",
|
|
1273
|
+
"[tasks.hindsight-setup]",
|
|
1274
|
+
`description = "Provision this dev's shared project Hindsight key from 1Password into .env"`,
|
|
1275
|
+
`run = "${cr}/.mise/scripts/hindsight-setup.sh"`,
|
|
1276
|
+
"",
|
|
1277
|
+
_WireMiseAgentHooks.MARKER + ":end",
|
|
1278
|
+
""
|
|
1279
|
+
].join("\n");
|
|
1280
|
+
content = content.replace(/\n*$/, "\n") + appended;
|
|
1281
|
+
if (!this.context.dryRun) writeFileSync3(misePath, content);
|
|
1282
|
+
if (wiredHooks) {
|
|
1283
|
+
return { success: true, message: this.formatMessage("\u2705 Wired mise.toml ([hooks] enter/leave + tasks)") };
|
|
1284
|
+
}
|
|
1285
|
+
return {
|
|
1286
|
+
success: true,
|
|
1287
|
+
message: this.formatMessage(
|
|
1288
|
+
`\u2705 Added agent-hooks tasks to mise.toml.
|
|
1289
|
+
\u26A0\uFE0F Could not find a [hooks].enter array to extend \u2014 add these to your [hooks] block manually:
|
|
1290
|
+
enter += "${cr}/.mise/scripts/link-project-skills-to-clis.sh", "${cr}/.agents/hooks/sync.py --install --quiet"
|
|
1291
|
+
leave += "${cr}/.mise/scripts/unlink-project-skills-from-clis.sh", "${cr}/.agents/hooks/sync.py --uninstall --quiet"`
|
|
1292
|
+
)
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
// src/recipes/AgentHooksRecipe.ts
|
|
1298
|
+
var AgentHooksRecipe = class extends Recipe {
|
|
1299
|
+
constructor(context) {
|
|
1300
|
+
super(context);
|
|
1301
|
+
this.addIngredient(CopyAgentHooksTree).addIngredient(WireMiseAgentHooks);
|
|
1302
|
+
}
|
|
1303
|
+
printNextSteps() {
|
|
1304
|
+
console.log("\u{1FA9D} Agent-hooks layer installed!");
|
|
1305
|
+
console.log(" Next steps:");
|
|
1306
|
+
console.log(" 1. mise run hooks-sync # generate .claude/settings.json + inject codex/kimi/hermes");
|
|
1307
|
+
console.log(" 2. git add .claude/settings.json .agents/hooks && commit (codex/kimi/hermes are per-dev)");
|
|
1308
|
+
console.log(" 3. mise run hindsight-setup # set HINDSIGHT_OP_KEY_REF to your 1Password item first");
|
|
1309
|
+
console.log(` 4. If you run a global agent system: echo '{"skills":{"defer_to_global":true}}' > .agents/local.json`);
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1150
1313
|
// src/utils/registry.ts
|
|
1151
1314
|
var RECIPE_REGISTRY = {
|
|
1152
1315
|
mise: {
|
|
@@ -1180,9 +1343,27 @@ var RECIPE_REGISTRY = {
|
|
|
1180
1343
|
"WireEmail",
|
|
1181
1344
|
"PrintHermesSummary"
|
|
1182
1345
|
]
|
|
1346
|
+
},
|
|
1347
|
+
"agent-hooks": {
|
|
1348
|
+
name: "agent-hooks",
|
|
1349
|
+
description: "Retrofit the project-scoped agent-hooks + skill fan-out layer (Claude/Codex/Kimi/Hermes hooks via mise enter/leave)",
|
|
1350
|
+
class: AgentHooksRecipe,
|
|
1351
|
+
commands: ["CopyAgentHooksTree", "WireMiseAgentHooks"]
|
|
1183
1352
|
}
|
|
1184
1353
|
};
|
|
1185
1354
|
var COMMAND_REGISTRY = {
|
|
1355
|
+
CopyAgentHooksTree: {
|
|
1356
|
+
name: "CopyAgentHooksTree",
|
|
1357
|
+
description: "Copy the generic agent-hooks tree (hooks SSOT + sync engine + scripts) from the CommonProject template",
|
|
1358
|
+
group: "agent-hooks",
|
|
1359
|
+
class: CopyAgentHooksTree
|
|
1360
|
+
},
|
|
1361
|
+
WireMiseAgentHooks: {
|
|
1362
|
+
name: "WireMiseAgentHooks",
|
|
1363
|
+
description: "Merge agent-hooks enter/leave + tasks into an existing mise.toml (idempotent)",
|
|
1364
|
+
group: "agent-hooks",
|
|
1365
|
+
class: WireMiseAgentHooks
|
|
1366
|
+
},
|
|
1186
1367
|
AddDockerfile: {
|
|
1187
1368
|
name: "AddDockerfile",
|
|
1188
1369
|
description: "Create Dockerfile for containerization",
|
|
@@ -1260,11 +1441,14 @@ function createRecipe(name, context) {
|
|
|
1260
1441
|
return new info.class(context);
|
|
1261
1442
|
}
|
|
1262
1443
|
|
|
1444
|
+
// src/index.ts
|
|
1445
|
+
import { cancel as cancel2, multiselect, text as text3, isCancel as isCancel5 } from "@clack/prompts";
|
|
1446
|
+
|
|
1263
1447
|
// src/parity/index.ts
|
|
1264
|
-
import { existsSync as
|
|
1265
|
-
import { dirname as
|
|
1266
|
-
import { fileURLToPath as
|
|
1267
|
-
import { homedir as
|
|
1448
|
+
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";
|
|
1449
|
+
import { basename as basename2, dirname as dirname5, join as join8, relative, resolve } from "node:path";
|
|
1450
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1451
|
+
import { homedir as homedir4 } from "node:os";
|
|
1268
1452
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
1269
1453
|
var LINK_AGENTFILES_BLOCK = `# This block will handle the linking of
|
|
1270
1454
|
# agent files to the main AGENTS.md file.
|
|
@@ -1282,6 +1466,28 @@ enter = [
|
|
|
1282
1466
|
patterns = ["AGENTS.md"]
|
|
1283
1467
|
task = "link-agentfiles"
|
|
1284
1468
|
|
|
1469
|
+
[tasks.link-agentfiles]
|
|
1470
|
+
description = "Symlink all agent files to AGENTS.md"
|
|
1471
|
+
run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
|
|
1472
|
+
var LINK_AGENTFILES_HOOK_ENTRIES = [
|
|
1473
|
+
"{{config_root}}/.mise/scripts/link-agentfiles.sh",
|
|
1474
|
+
"op inject -i .env.op > .env"
|
|
1475
|
+
];
|
|
1476
|
+
var LINK_AGENTFILES_HOOKS_BLOCK = `# This block will handle the linking of
|
|
1477
|
+
# agent files to the main AGENTS.md file.
|
|
1478
|
+
#
|
|
1479
|
+
# TODO: Ensure this works for all levels of nesting.
|
|
1480
|
+
# i.e. All linked agent files MUST be siblings at
|
|
1481
|
+
# any given level of nesting.
|
|
1482
|
+
[hooks]
|
|
1483
|
+
enter = [
|
|
1484
|
+
"{{config_root}}/.mise/scripts/link-agentfiles.sh",
|
|
1485
|
+
"op inject -i .env.op > .env",
|
|
1486
|
+
]`;
|
|
1487
|
+
var LINK_AGENTFILES_WATCH_TASK_BLOCK = `[[watch_files]]
|
|
1488
|
+
patterns = ["AGENTS.md"]
|
|
1489
|
+
task = "link-agentfiles"
|
|
1490
|
+
|
|
1285
1491
|
[tasks.link-agentfiles]
|
|
1286
1492
|
description = "Symlink all agent files to AGENTS.md"
|
|
1287
1493
|
run = "{{config_root}}/.mise/scripts/link-agentfiles.sh"`;
|
|
@@ -1312,12 +1518,12 @@ description = "Force every versioned file up to the highest version"
|
|
|
1312
1518
|
run = "{{config_root}}/.mise/scripts/versioning.sh sync"
|
|
1313
1519
|
# <<< mise-versioning <<<`;
|
|
1314
1520
|
function resolvePjanglerRoot() {
|
|
1315
|
-
let dir =
|
|
1316
|
-
while (dir !==
|
|
1317
|
-
if (
|
|
1521
|
+
let dir = dirname5(fileURLToPath3(import.meta.url));
|
|
1522
|
+
while (dir !== dirname5(dir)) {
|
|
1523
|
+
if (existsSync7(join8(dir, "package.json")) && existsSync7(join8(dir, "templates", "commonproject", "copier.yml"))) {
|
|
1318
1524
|
return dir;
|
|
1319
1525
|
}
|
|
1320
|
-
dir =
|
|
1526
|
+
dir = dirname5(dir);
|
|
1321
1527
|
}
|
|
1322
1528
|
throw new Error("Unable to resolve pjangler root");
|
|
1323
1529
|
}
|
|
@@ -1325,22 +1531,22 @@ function normalizeNewlines(value) {
|
|
|
1325
1531
|
return value.replace(/\r\n/g, "\n");
|
|
1326
1532
|
}
|
|
1327
1533
|
function readText(path) {
|
|
1328
|
-
return normalizeNewlines(
|
|
1534
|
+
return normalizeNewlines(readFileSync3(path, "utf8"));
|
|
1329
1535
|
}
|
|
1330
1536
|
function safeReadText(path) {
|
|
1331
|
-
return
|
|
1537
|
+
return existsSync7(path) ? readText(path) : null;
|
|
1332
1538
|
}
|
|
1333
1539
|
function ensureParent(path) {
|
|
1334
|
-
|
|
1540
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
1335
1541
|
}
|
|
1336
1542
|
function writeText(path, content) {
|
|
1337
1543
|
ensureParent(path);
|
|
1338
|
-
|
|
1544
|
+
writeFileSync4(path, content);
|
|
1339
1545
|
}
|
|
1340
|
-
function tryParseJson(
|
|
1341
|
-
if (!
|
|
1546
|
+
function tryParseJson(text4) {
|
|
1547
|
+
if (!text4) return null;
|
|
1342
1548
|
try {
|
|
1343
|
-
return JSON.parse(
|
|
1549
|
+
return JSON.parse(text4);
|
|
1344
1550
|
} catch {
|
|
1345
1551
|
return null;
|
|
1346
1552
|
}
|
|
@@ -1352,7 +1558,7 @@ function titleCaseSlug(slug) {
|
|
|
1352
1558
|
return slug.split(/[-_]/g).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1353
1559
|
}
|
|
1354
1560
|
function readSymlinkTarget(path) {
|
|
1355
|
-
if (!
|
|
1561
|
+
if (!existsSync7(path)) return null;
|
|
1356
1562
|
try {
|
|
1357
1563
|
return readlinkSync(path);
|
|
1358
1564
|
} catch {
|
|
@@ -1360,7 +1566,7 @@ function readSymlinkTarget(path) {
|
|
|
1360
1566
|
}
|
|
1361
1567
|
}
|
|
1362
1568
|
function ensureSymlink(path, target, dryRun) {
|
|
1363
|
-
if (
|
|
1569
|
+
if (existsSync7(path)) {
|
|
1364
1570
|
const stat = lstatSync(path);
|
|
1365
1571
|
if (stat.isSymbolicLink()) {
|
|
1366
1572
|
const current = readSymlinkTarget(path);
|
|
@@ -1376,9 +1582,32 @@ function ensureSymlink(path, target, dryRun) {
|
|
|
1376
1582
|
if (!dryRun) symlinkSync(target, path);
|
|
1377
1583
|
return { changed: true };
|
|
1378
1584
|
}
|
|
1379
|
-
function
|
|
1585
|
+
function bootstrapAgentsFile(repoRoot, dryRun) {
|
|
1586
|
+
const agentsPath = join8(repoRoot, "AGENTS.md");
|
|
1587
|
+
if (existsSync7(agentsPath)) return { changedFiles: [], details: [] };
|
|
1588
|
+
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1589
|
+
const source = join8(repoRoot, file);
|
|
1590
|
+
if (!existsSync7(source)) continue;
|
|
1591
|
+
const stat = lstatSync(source);
|
|
1592
|
+
if (stat.isSymbolicLink()) continue;
|
|
1593
|
+
if (stat.isFile()) {
|
|
1594
|
+
if (!dryRun) renameSync(source, agentsPath);
|
|
1595
|
+
return { changedFiles: [agentsPath], details: [`Moved ${file} to AGENTS.md before wiring agent-file symlinks`] };
|
|
1596
|
+
}
|
|
1597
|
+
return { changedFiles: [], details: [], blocked: `${file} exists but is not a regular file; cannot promote to AGENTS.md` };
|
|
1598
|
+
}
|
|
1599
|
+
const readmePath = join8(repoRoot, "README.md");
|
|
1600
|
+
if (existsSync7(readmePath)) {
|
|
1601
|
+
const stat = lstatSync(readmePath);
|
|
1602
|
+
if (!stat.isFile()) return { changedFiles: [], details: [], blocked: "README.md exists but is not a regular file; cannot copy to AGENTS.md" };
|
|
1603
|
+
if (!dryRun) copyFileSync(readmePath, agentsPath);
|
|
1604
|
+
return { changedFiles: [agentsPath], details: ["Copied README.md to AGENTS.md before wiring agent-file symlinks"] };
|
|
1605
|
+
}
|
|
1606
|
+
return { changedFiles: [], details: [], blocked: "AGENTS.md missing and no CLAUDE.md, GEMINI.md, or README.md source exists" };
|
|
1607
|
+
}
|
|
1608
|
+
function yamlGet(text4, keyPath) {
|
|
1380
1609
|
const parts = keyPath.split(".");
|
|
1381
|
-
const lines =
|
|
1610
|
+
const lines = text4.split("\n");
|
|
1382
1611
|
let start = 0;
|
|
1383
1612
|
let indent = 0;
|
|
1384
1613
|
for (let idx = 0; idx < parts.length; idx += 1) {
|
|
@@ -1407,36 +1636,36 @@ function yamlGet(text3, keyPath) {
|
|
|
1407
1636
|
return "";
|
|
1408
1637
|
}
|
|
1409
1638
|
function discoverRoles(repoRoot) {
|
|
1410
|
-
const rolesDir =
|
|
1411
|
-
if (!
|
|
1639
|
+
const rolesDir = join8(repoRoot, "agents", "hermes");
|
|
1640
|
+
if (!existsSync7(rolesDir)) return [];
|
|
1412
1641
|
return readdirSync(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => {
|
|
1413
|
-
const roleDir =
|
|
1414
|
-
const roleYamlPath =
|
|
1415
|
-
if (!
|
|
1416
|
-
const
|
|
1417
|
-
const runtimeRepoRaw = yamlGet(
|
|
1642
|
+
const roleDir = join8(rolesDir, entry.name);
|
|
1643
|
+
const roleYamlPath = join8(roleDir, "role.yaml");
|
|
1644
|
+
if (!existsSync7(roleYamlPath)) return null;
|
|
1645
|
+
const text4 = readText(roleYamlPath);
|
|
1646
|
+
const runtimeRepoRaw = yamlGet(text4, "runtime.github_repo");
|
|
1418
1647
|
return {
|
|
1419
|
-
role: yamlGet(
|
|
1648
|
+
role: yamlGet(text4, "role") || entry.name,
|
|
1420
1649
|
roleDir,
|
|
1421
1650
|
roleYamlPath,
|
|
1422
|
-
repo: yamlGet(
|
|
1423
|
-
agentId: yamlGet(
|
|
1424
|
-
profileName: yamlGet(
|
|
1425
|
-
displayName: yamlGet(
|
|
1426
|
-
purpose: yamlGet(
|
|
1427
|
-
botHandle: yamlGet(
|
|
1651
|
+
repo: yamlGet(text4, "repo"),
|
|
1652
|
+
agentId: yamlGet(text4, "agent_id"),
|
|
1653
|
+
profileName: yamlGet(text4, "profile") || yamlGet(text4, "agent_id"),
|
|
1654
|
+
displayName: yamlGet(text4, "display_name"),
|
|
1655
|
+
purpose: yamlGet(text4, "purpose"),
|
|
1656
|
+
botHandle: yamlGet(text4, "telegram.bot_username"),
|
|
1428
1657
|
runtimeRepo: runtimeRepoRaw.includes("/") ? runtimeRepoRaw.split("/").slice(-1)[0] ?? runtimeRepoRaw : runtimeRepoRaw,
|
|
1429
|
-
runtimeOwner: yamlGet(
|
|
1430
|
-
planeWorkspace: yamlGet(
|
|
1431
|
-
ticketProviderName: yamlGet(
|
|
1432
|
-
ticketProviderBoardId: yamlGet(
|
|
1433
|
-
ticketProviderBoardUrl: yamlGet(
|
|
1434
|
-
ticketProviderIdentifier: yamlGet(
|
|
1658
|
+
runtimeOwner: yamlGet(text4, "runtime.github_owner"),
|
|
1659
|
+
planeWorkspace: yamlGet(text4, "ticket_provider.workspace") || yamlGet(text4, "plane.workspace"),
|
|
1660
|
+
ticketProviderName: yamlGet(text4, "ticket_provider.name"),
|
|
1661
|
+
ticketProviderBoardId: yamlGet(text4, "ticket_provider.board_id"),
|
|
1662
|
+
ticketProviderBoardUrl: yamlGet(text4, "ticket_provider.board_url"),
|
|
1663
|
+
ticketProviderIdentifier: yamlGet(text4, "plane.identifier")
|
|
1435
1664
|
};
|
|
1436
1665
|
}).filter((value) => Boolean(value));
|
|
1437
1666
|
}
|
|
1438
1667
|
function registryPath(homeDir) {
|
|
1439
|
-
return
|
|
1668
|
+
return join8(homeDir, ".hermes", "agents-registry.yaml");
|
|
1440
1669
|
}
|
|
1441
1670
|
function systemctlUser(args) {
|
|
1442
1671
|
const result = spawnSync4("systemctl", ["--user", ...args], { encoding: "utf8" });
|
|
@@ -1446,86 +1675,257 @@ function systemctlUser(args) {
|
|
|
1446
1675
|
stderr: result.stderr.trim()
|
|
1447
1676
|
};
|
|
1448
1677
|
}
|
|
1678
|
+
function templateScript(ctx, name) {
|
|
1679
|
+
const source = join8(ctx.pjanglerRoot, ".mise", "scripts", name);
|
|
1680
|
+
return existsSync7(source) ? readText(source) : void 0;
|
|
1681
|
+
}
|
|
1449
1682
|
function templateVersioningScript(ctx) {
|
|
1450
|
-
|
|
1451
|
-
|
|
1683
|
+
return templateScript(ctx, "versioning.sh");
|
|
1684
|
+
}
|
|
1685
|
+
function templateLinkAgentfilesScript(ctx) {
|
|
1686
|
+
return templateScript(ctx, "link-agentfiles.sh");
|
|
1687
|
+
}
|
|
1688
|
+
function renderGeneratedProjectMiseToml(ctx, template) {
|
|
1689
|
+
const project = readProjectJson(ctx);
|
|
1690
|
+
const projectName = String(project?.project_name ?? basename2(ctx.repoRoot) ?? "project");
|
|
1691
|
+
return template.replace(/\{%\s*raw\s*%\}([\s\S]*?)\{%\s*endraw\s*%\}/g, "$1").replace(/\{\{\s*project_name\s*\}\}/g, projectName);
|
|
1692
|
+
}
|
|
1693
|
+
function ensureMiseTomlFromTemplate(ctx, changedFiles) {
|
|
1694
|
+
const targetPath = join8(ctx.repoRoot, "mise.toml");
|
|
1695
|
+
if (existsSync7(targetPath)) return false;
|
|
1696
|
+
const sourcePath = join8(ctx.pjanglerRoot, "templates", "commonproject", "template", "mise.toml.jinja");
|
|
1697
|
+
if (!existsSync7(sourcePath)) return false;
|
|
1698
|
+
changedFiles.push(targetPath);
|
|
1699
|
+
if (!ctx.dryRun) {
|
|
1700
|
+
writeText(targetPath, renderGeneratedProjectMiseToml(ctx, readText(sourcePath)));
|
|
1701
|
+
}
|
|
1702
|
+
return true;
|
|
1452
1703
|
}
|
|
1453
1704
|
function templateVersionFilesConf(ctx, repoRoot) {
|
|
1454
|
-
const packageJson =
|
|
1455
|
-
return
|
|
1705
|
+
const packageJson = join8(repoRoot, "package.json");
|
|
1706
|
+
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
1707
|
}
|
|
1457
|
-
function replaceOrAppendManagedBlock(
|
|
1458
|
-
if (startMarker.test(
|
|
1459
|
-
return
|
|
1708
|
+
function replaceOrAppendManagedBlock(text4, startMarker, block, beforePattern) {
|
|
1709
|
+
if (startMarker.test(text4)) {
|
|
1710
|
+
return text4.replace(/# >>> mise-versioning >>>[\s\S]*?# <<< mise-versioning <<</, block);
|
|
1460
1711
|
}
|
|
1461
1712
|
if (beforePattern) {
|
|
1462
|
-
const match =
|
|
1713
|
+
const match = text4.match(beforePattern);
|
|
1463
1714
|
if (match && typeof match.index === "number") {
|
|
1464
|
-
return `${
|
|
1715
|
+
return `${text4.slice(0, match.index).replace(/\s*$/, "\n\n")}${block}
|
|
1465
1716
|
|
|
1466
|
-
${
|
|
1717
|
+
${text4.slice(match.index)}`;
|
|
1467
1718
|
}
|
|
1468
1719
|
}
|
|
1469
|
-
return `${
|
|
1720
|
+
return `${text4.replace(/\s*$/, "")}
|
|
1470
1721
|
|
|
1471
1722
|
${block}
|
|
1472
1723
|
`;
|
|
1473
1724
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1725
|
+
var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
|
|
1726
|
+
var CONDITIONAL_HERMES_PATHS = ["agents/hermes/pm/hermes", "agent/hermes/pm/hermes"];
|
|
1727
|
+
function requiredMisePathEntries(ctx) {
|
|
1728
|
+
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
1729
|
+
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
1730
|
+
if (existsSync7(join8(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
1731
|
+
}
|
|
1732
|
+
return required;
|
|
1733
|
+
}
|
|
1734
|
+
function upsertMisePath(text4, required = BASE_MISE_PATH_ENTRIES) {
|
|
1735
|
+
const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
1736
|
+
const envMatch = text4.match(/(^|\n)(\[env\][\s\S]*?)(?=\n\[[^\]]+\]|$)/);
|
|
1737
|
+
if (!envMatch || typeof envMatch.index !== "number") {
|
|
1738
|
+
return `[env]
|
|
1739
|
+
${render(required)}
|
|
1740
|
+
|
|
1741
|
+
${text4.replace(/^\s+/, "")}`;
|
|
1742
|
+
}
|
|
1743
|
+
const prefix = text4.slice(0, envMatch.index + envMatch[1].length);
|
|
1744
|
+
const section = envMatch[2];
|
|
1745
|
+
const suffix = text4.slice(envMatch.index + envMatch[1].length + section.length);
|
|
1746
|
+
const pathLine = section.match(/^_\.path\s*=\s*\[([^\]]*)\]\s*$/m);
|
|
1747
|
+
if (!pathLine) {
|
|
1748
|
+
return `${prefix}${section.replace(/\n?$/, "\n")}${render(required)}${suffix}`;
|
|
1749
|
+
}
|
|
1750
|
+
const current = [...pathLine[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
1751
|
+
const merged = [...current];
|
|
1752
|
+
for (const value of required) {
|
|
1753
|
+
if (!merged.includes(value)) merged.push(value);
|
|
1754
|
+
}
|
|
1755
|
+
const nextLine = render(merged);
|
|
1756
|
+
if (pathLine[0] === nextLine) return text4;
|
|
1757
|
+
return `${prefix}${section.replace(pathLine[0], nextLine)}${suffix}`;
|
|
1758
|
+
}
|
|
1759
|
+
function removeTomlSection(text4, headerPattern, marker, options) {
|
|
1760
|
+
const lines = text4.split("\n");
|
|
1761
|
+
let start = -1;
|
|
1762
|
+
let end = -1;
|
|
1763
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1764
|
+
if (!headerPattern.test(lines[i])) continue;
|
|
1765
|
+
if (marker) {
|
|
1766
|
+
let hasMarker = false;
|
|
1767
|
+
for (let j = i + 1; j < lines.length && !/^\[[^\]]+\]/.test(lines[j]); j++) {
|
|
1768
|
+
if (marker.test(lines[j])) {
|
|
1769
|
+
hasMarker = true;
|
|
1770
|
+
break;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
if (!hasMarker) continue;
|
|
1774
|
+
}
|
|
1775
|
+
start = i;
|
|
1776
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1777
|
+
if (/^\[[^\]]+\]/.test(lines[j])) {
|
|
1778
|
+
end = j;
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (end === -1) end = lines.length;
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
if (start === -1) return text4;
|
|
1786
|
+
if (options?.includePrecedingComments) {
|
|
1787
|
+
while (start > 0 && lines[start - 1].trim().startsWith("#")) {
|
|
1788
|
+
start--;
|
|
1789
|
+
}
|
|
1478
1790
|
}
|
|
1479
|
-
const
|
|
1791
|
+
const result = lines.slice(0, start).concat(lines.slice(end)).join("\n");
|
|
1792
|
+
return result.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1793
|
+
}
|
|
1794
|
+
function insertTomlBlockBeforeVersioning(text4, block) {
|
|
1795
|
+
const versioningIndex = text4.indexOf("# >>> mise-versioning >>>");
|
|
1480
1796
|
if (versioningIndex >= 0) {
|
|
1481
|
-
return `${
|
|
1797
|
+
return `${text4.slice(0, versioningIndex).replace(/\s*$/, "\n\n")}${block}
|
|
1482
1798
|
|
|
1483
|
-
${
|
|
1799
|
+
${text4.slice(versioningIndex)}`;
|
|
1484
1800
|
}
|
|
1485
|
-
return `${
|
|
1801
|
+
return `${text4.replace(/\s*$/, "")}
|
|
1486
1802
|
|
|
1487
|
-
${
|
|
1803
|
+
${block}
|
|
1488
1804
|
`;
|
|
1489
1805
|
}
|
|
1806
|
+
function extractTomlStrings(text4) {
|
|
1807
|
+
const values = [];
|
|
1808
|
+
const stringPattern = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
|
|
1809
|
+
for (const match of text4.matchAll(stringPattern)) {
|
|
1810
|
+
if (match[1] !== void 0) {
|
|
1811
|
+
try {
|
|
1812
|
+
values.push(JSON.parse(`"${match[1]}"`));
|
|
1813
|
+
} catch {
|
|
1814
|
+
values.push(match[1]);
|
|
1815
|
+
}
|
|
1816
|
+
} else if (match[2] !== void 0) {
|
|
1817
|
+
values.push(match[2]);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return values;
|
|
1821
|
+
}
|
|
1822
|
+
function isManagedHookEntry(value) {
|
|
1823
|
+
const trimmed = value.trim();
|
|
1824
|
+
return trimmed === "op inject -i .env.op > .env" || /(^|\/)link-agentfiles\.sh$/.test(trimmed);
|
|
1825
|
+
}
|
|
1826
|
+
function renderHookEntries(entries, indent = "") {
|
|
1827
|
+
return [
|
|
1828
|
+
`${indent}enter = [`,
|
|
1829
|
+
...entries.map((entry) => `${indent} ${JSON.stringify(entry)},`),
|
|
1830
|
+
`${indent}]`
|
|
1831
|
+
];
|
|
1832
|
+
}
|
|
1833
|
+
function upsertLinkAgentfilesHooks(text4) {
|
|
1834
|
+
const lines = text4.split("\n");
|
|
1835
|
+
const hooksStart = lines.findIndex((line) => /^\[hooks\]$/.test(line.trim()));
|
|
1836
|
+
if (hooksStart === -1) return insertTomlBlockBeforeVersioning(text4, LINK_AGENTFILES_HOOKS_BLOCK);
|
|
1837
|
+
let hooksEnd = lines.length;
|
|
1838
|
+
for (let i = hooksStart + 1; i < lines.length; i++) {
|
|
1839
|
+
if (/^\[[^\]]+\]/.test(lines[i].trim())) {
|
|
1840
|
+
hooksEnd = i;
|
|
1841
|
+
break;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
let enterStart = -1;
|
|
1845
|
+
let enterEnd = -1;
|
|
1846
|
+
for (let i = hooksStart + 1; i < hooksEnd; i++) {
|
|
1847
|
+
if (!/^\s*enter\s*=/.test(lines[i])) continue;
|
|
1848
|
+
enterStart = i;
|
|
1849
|
+
enterEnd = i + 1;
|
|
1850
|
+
const afterEquals = lines[i].slice(lines[i].indexOf("=") + 1);
|
|
1851
|
+
if (afterEquals.includes("[") && !afterEquals.includes("]")) {
|
|
1852
|
+
while (enterEnd < hooksEnd && !lines[enterEnd].includes("]")) enterEnd++;
|
|
1853
|
+
if (enterEnd < hooksEnd) enterEnd++;
|
|
1854
|
+
}
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
const existingBlock = enterStart >= 0 ? lines.slice(enterStart, enterEnd).join("\n") : "";
|
|
1858
|
+
const preserved = extractTomlStrings(existingBlock).filter((entry) => !isManagedHookEntry(entry));
|
|
1859
|
+
const merged = [...LINK_AGENTFILES_HOOK_ENTRIES];
|
|
1860
|
+
for (const entry of preserved) {
|
|
1861
|
+
if (!merged.includes(entry)) merged.push(entry);
|
|
1862
|
+
}
|
|
1863
|
+
const indent = enterStart >= 0 ? lines[enterStart].match(/^\s*/)?.[0] ?? "" : "";
|
|
1864
|
+
const rendered = renderHookEntries(merged, indent);
|
|
1865
|
+
if (enterStart >= 0) {
|
|
1866
|
+
return lines.slice(0, enterStart).concat(rendered, lines.slice(enterEnd)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1867
|
+
}
|
|
1868
|
+
return lines.slice(0, hooksStart + 1).concat(rendered, lines.slice(hooksStart + 1)).join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
|
|
1869
|
+
}
|
|
1870
|
+
function upsertLinkAgentfilesBlock(text4, ctx) {
|
|
1871
|
+
const withPath = upsertMisePath(text4, requiredMisePathEntries(ctx));
|
|
1872
|
+
if (withPath.includes(LINK_AGENTFILES_BLOCK)) return withPath;
|
|
1873
|
+
let cleaned = removeTomlSection(withPath, /^\[tasks\.link-agentfiles\]$/, /link-agentfiles/, { includePrecedingComments: false });
|
|
1874
|
+
cleaned = removeTomlSection(cleaned, /^\[\[watch_files\]\]$/, /AGENTS\.md/, { includePrecedingComments: false });
|
|
1875
|
+
cleaned = upsertLinkAgentfilesHooks(cleaned);
|
|
1876
|
+
return insertTomlBlockBeforeVersioning(cleaned, LINK_AGENTFILES_WATCH_TASK_BLOCK);
|
|
1877
|
+
}
|
|
1490
1878
|
function readProjectJson(ctx) {
|
|
1491
|
-
return tryParseJson(safeReadText(
|
|
1879
|
+
return tryParseJson(safeReadText(join8(ctx.repoRoot, ".project.json")));
|
|
1492
1880
|
}
|
|
1493
1881
|
function canonicalProjectJson(ctx) {
|
|
1494
1882
|
const roles = discoverRoles(ctx.repoRoot);
|
|
1495
1883
|
const existing = readProjectJson(ctx) ?? {};
|
|
1496
|
-
const slug = String(existing.project_slug ?? slugifyRepoName(
|
|
1884
|
+
const slug = String(existing.project_slug ?? slugifyRepoName(dirname5(ctx.repoRoot) === ctx.repoRoot ? ctx.repoRoot.split("/").pop() ?? "project" : ctx.repoRoot.split("/").pop() ?? "project"));
|
|
1497
1885
|
const firstRole = roles[0];
|
|
1498
1886
|
const ticketProvider = {
|
|
1499
1887
|
type: String((existing.ticket_provider?.type ?? firstRole?.ticketProviderName ?? "plane") || "plane"),
|
|
1500
1888
|
workspace: String((existing.ticket_provider?.workspace ?? firstRole?.planeWorkspace ?? "") || ""),
|
|
1501
1889
|
identifier: String((existing.ticket_provider?.identifier ?? firstRole?.ticketProviderIdentifier ?? "") || ""),
|
|
1502
1890
|
board_id: String((existing.ticket_provider?.board_id ?? firstRole?.ticketProviderBoardId ?? "") || ""),
|
|
1503
|
-
board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || "")
|
|
1891
|
+
board_url: String((existing.ticket_provider?.board_url ?? firstRole?.ticketProviderBoardUrl ?? "") || ""),
|
|
1892
|
+
state: String((existing.ticket_provider?.state ?? "planned") || "planned")
|
|
1504
1893
|
};
|
|
1894
|
+
const existingAgents = existing.agents ?? {};
|
|
1895
|
+
const discoveredAgents = Object.fromEntries(
|
|
1896
|
+
roles.map((role) => [
|
|
1897
|
+
role.agentId || `${slug}-${role.role}`,
|
|
1898
|
+
{
|
|
1899
|
+
role: role.role,
|
|
1900
|
+
role_dir: relative(ctx.repoRoot, role.roleDir)
|
|
1901
|
+
}
|
|
1902
|
+
])
|
|
1903
|
+
);
|
|
1904
|
+
const agents = { ...existingAgents };
|
|
1905
|
+
for (const [agentId, discovered] of Object.entries(discoveredAgents)) {
|
|
1906
|
+
const existingAgent = existingAgents[agentId] ?? {};
|
|
1907
|
+
agents[agentId] = {
|
|
1908
|
+
role: discovered.role,
|
|
1909
|
+
role_dir: discovered.role_dir,
|
|
1910
|
+
provisioning_state: existingAgent.provisioning_state
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1505
1913
|
return {
|
|
1506
1914
|
project_name: String(existing.project_name ?? titleCaseSlug(slug)),
|
|
1507
1915
|
project_description: String(existing.project_description ?? ""),
|
|
1508
1916
|
project_slug: slug,
|
|
1509
1917
|
repo_path: ctx.repoRoot,
|
|
1510
1918
|
ticket_provider: ticketProvider,
|
|
1511
|
-
agents
|
|
1512
|
-
roles.map((role) => [
|
|
1513
|
-
role.agentId || `${slug}-${role.role}`,
|
|
1514
|
-
{
|
|
1515
|
-
role: role.role,
|
|
1516
|
-
role_dir: relative(ctx.repoRoot, role.roleDir)
|
|
1517
|
-
}
|
|
1518
|
-
])
|
|
1519
|
-
)
|
|
1919
|
+
agents
|
|
1520
1920
|
};
|
|
1521
1921
|
}
|
|
1522
1922
|
function projectJsonFinding(ctx) {
|
|
1523
|
-
const projectPath =
|
|
1524
|
-
const planeJsonPath =
|
|
1923
|
+
const projectPath = join8(ctx.repoRoot, ".project.json");
|
|
1924
|
+
const planeJsonPath = join8(ctx.repoRoot, ".plane.json");
|
|
1525
1925
|
const details = [];
|
|
1526
1926
|
const data = readProjectJson(ctx);
|
|
1527
1927
|
const roles = discoverRoles(ctx.repoRoot);
|
|
1528
|
-
if (!
|
|
1928
|
+
if (!existsSync7(projectPath)) {
|
|
1529
1929
|
return { id: "sot.project-json", title: "Canonical .project.json", status: "fail", summary: ".project.json missing", details: [], fixable: true };
|
|
1530
1930
|
}
|
|
1531
1931
|
if (!data) {
|
|
@@ -1548,10 +1948,10 @@ function projectJsonFinding(ctx) {
|
|
|
1548
1948
|
}
|
|
1549
1949
|
}
|
|
1550
1950
|
const ticketProvider = data.ticket_provider ?? {};
|
|
1551
|
-
for (const key of ["type", "workspace", "identifier", "board_id", "board_url"]) {
|
|
1951
|
+
for (const key of ["type", "workspace", "identifier", "board_id", "board_url", "state"]) {
|
|
1552
1952
|
if (!(key in ticketProvider)) details.push(`ticket_provider.${key} missing`);
|
|
1553
1953
|
}
|
|
1554
|
-
if (
|
|
1954
|
+
if (existsSync7(planeJsonPath)) details.push(".plane.json should not exist once .project.json is canonical");
|
|
1555
1955
|
return {
|
|
1556
1956
|
id: "sot.project-json",
|
|
1557
1957
|
title: "Canonical .project.json",
|
|
@@ -1563,8 +1963,8 @@ function projectJsonFinding(ctx) {
|
|
|
1563
1963
|
}
|
|
1564
1964
|
function renderSoul(role) {
|
|
1565
1965
|
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
|
|
1966
|
+
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.";
|
|
1967
|
+
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
1968
|
const runtimeOwner = role.runtimeOwner || "delorenj";
|
|
1569
1969
|
return `# ${role.displayName || role.agentId}
|
|
1570
1970
|
|
|
@@ -1584,7 +1984,7 @@ You are **${role.displayName || role.agentId}** \u2014 a Hermes agent provisione
|
|
|
1584
1984
|
|
|
1585
1985
|
## Scope
|
|
1586
1986
|
|
|
1587
|
-
You operate only within the working directory of \`${role.repo}\`. Your HERMES_HOME
|
|
1987
|
+
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
1988
|
|
|
1589
1989
|
## Tone
|
|
1590
1990
|
|
|
@@ -1601,8 +2001,7 @@ Your memory is the submodule at \`./runtime/memories/\`. Use durable memory deli
|
|
|
1601
2001
|
}
|
|
1602
2002
|
function renderHermesWrapper(role) {
|
|
1603
2003
|
return `#!/usr/bin/env bash
|
|
1604
|
-
# Launcher for ${role.agentId}. Resolves HERMES_HOME
|
|
1605
|
-
# when available, then falls back to the local runtime submodule.
|
|
2004
|
+
# Launcher for ${role.agentId}. Resolves HERMES_HOME to the runtime submodule.
|
|
1606
2005
|
|
|
1607
2006
|
set -euo pipefail
|
|
1608
2007
|
|
|
@@ -1621,12 +2020,7 @@ CODEX_HOME="{CODEX_HOME:-{HERMES_FLEET_CODEX_HOME:-$HOME/.codex}}"
|
|
|
1621
2020
|
|
|
1622
2021
|
FLEET_HOME="{HERMES_FLEET_HOME:-$HOME/.hermes}"
|
|
1623
2022
|
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
|
|
2023
|
+
HERMES_HOME="$RUNTIME_HOME"
|
|
1630
2024
|
|
|
1631
2025
|
if [[ ! -d "$RUNTIME_HOME" ]]; then
|
|
1632
2026
|
echo "hermes: runtime submodule not initialized at $RUNTIME_HOME" >&2
|
|
@@ -1638,17 +2032,17 @@ exec env HERMES_HOME="$HERMES_HOME" HERMES_FLEET_ENV="$FLEET_ENV" HERMES_OAUTH
|
|
|
1638
2032
|
`.replace(/\u0010/g, "$");
|
|
1639
2033
|
}
|
|
1640
2034
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
1641
|
-
if (!
|
|
1642
|
-
|
|
2035
|
+
if (!existsSync7(sourceDir)) return;
|
|
2036
|
+
mkdirSync5(targetDir, { recursive: true });
|
|
1643
2037
|
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
1644
|
-
const sourcePath =
|
|
2038
|
+
const sourcePath = join8(sourceDir, entry.name);
|
|
1645
2039
|
if (skip?.(sourcePath)) continue;
|
|
1646
|
-
const targetPath =
|
|
2040
|
+
const targetPath = join8(targetDir, entry.name);
|
|
1647
2041
|
if (entry.isDirectory()) {
|
|
1648
2042
|
copyMissingRecursive(sourcePath, targetPath, changedFiles, dryRun, skip);
|
|
1649
2043
|
continue;
|
|
1650
2044
|
}
|
|
1651
|
-
if (
|
|
2045
|
+
if (existsSync7(targetPath)) continue;
|
|
1652
2046
|
changedFiles.push(targetPath);
|
|
1653
2047
|
if (!dryRun) {
|
|
1654
2048
|
ensureParent(targetPath);
|
|
@@ -1657,7 +2051,7 @@ function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip)
|
|
|
1657
2051
|
}
|
|
1658
2052
|
}
|
|
1659
2053
|
function upsertSubmodule(repoRoot, role, changedFiles, dryRun) {
|
|
1660
|
-
const gitmodulesPath =
|
|
2054
|
+
const gitmodulesPath = join8(repoRoot, ".gitmodules");
|
|
1661
2055
|
const repoName = role.runtimeRepo || `agent-hm-${role.repo}-${role.role}`;
|
|
1662
2056
|
const owner = role.runtimeOwner || "delorenj";
|
|
1663
2057
|
const block = `[submodule "agents/hermes/${role.role}/runtime"]
|
|
@@ -1679,7 +2073,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
1679
2073
|
repo: ${role.repo}
|
|
1680
2074
|
role: ${role.role}
|
|
1681
2075
|
display_name: ${JSON.stringify(role.displayName || role.agentId)}
|
|
1682
|
-
project_path: ${ctxEscape(role.roleDir ?
|
|
2076
|
+
project_path: ${ctxEscape(role.roleDir ? dirname5(dirname5(dirname5(role.roleDir))) : "")}
|
|
1683
2077
|
role_dir: ${ctxEscape(role.roleDir)}
|
|
1684
2078
|
profile_name: ${role.profileName || role.agentId}
|
|
1685
2079
|
telegram:
|
|
@@ -1692,7 +2086,7 @@ function upsertRegistryEntry(role, homeDir, changedFiles, dryRun) {
|
|
|
1692
2086
|
systemd:
|
|
1693
2087
|
gateway_unit: hermes-${role.agentId}-gateway.service
|
|
1694
2088
|
consumer_unit: hermes-${role.agentId}-consumer.service
|
|
1695
|
-
|
|
2089
|
+
heartbeat_timer: hermes-${role.agentId}-heartbeat.timer
|
|
1696
2090
|
`;
|
|
1697
2091
|
const next = current.includes("agents: {}") ? current.replace("agents: {}", `agents:
|
|
1698
2092
|
${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
|
|
@@ -1701,9 +2095,9 @@ ${block}`) : `${current.replace(/\s*$/, "\n")}${block}`;
|
|
|
1701
2095
|
return path;
|
|
1702
2096
|
}
|
|
1703
2097
|
function profileMetaInheritsDefault(path) {
|
|
1704
|
-
const
|
|
2098
|
+
const text4 = safeReadText(path);
|
|
1705
2099
|
return Boolean(
|
|
1706
|
-
|
|
2100
|
+
text4 && /^config:\s*$/m.test(text4) && /^\s+inherit_from:\s*default\s*$/m.test(text4) && /^\s+save_mode:\s*delta\s*$/m.test(text4)
|
|
1707
2101
|
);
|
|
1708
2102
|
}
|
|
1709
2103
|
function upsertInheritedProfileMeta(path, changedFiles, dryRun) {
|
|
@@ -1757,17 +2151,21 @@ var RULES = [
|
|
|
1757
2151
|
id: "mise.config-root",
|
|
1758
2152
|
title: "mise config_root + AGENTS link hooks",
|
|
1759
2153
|
audit: (ctx) => {
|
|
1760
|
-
const misePath =
|
|
1761
|
-
if (!
|
|
2154
|
+
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
2155
|
+
if (!existsSync7(misePath)) {
|
|
1762
2156
|
return { id: "mise.config-root", title: "mise config_root + AGENTS link hooks", status: "fail", summary: "mise.toml missing", details: [], fixable: true };
|
|
1763
2157
|
}
|
|
1764
|
-
const
|
|
2158
|
+
const text4 = readText(misePath);
|
|
1765
2159
|
const details = [];
|
|
1766
|
-
|
|
1767
|
-
if (!
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
if (
|
|
2160
|
+
const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2161
|
+
if (!existsSync7(linkAgentfilesPath)) details.push(".mise/scripts/link-agentfiles.sh missing");
|
|
2162
|
+
const pathValues = [...(text4.match(/^_\.path\s*=\s*\[([^\]]*)\]/m)?.[1] ?? "").matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
|
2163
|
+
const missingPathValues = requiredMisePathEntries(ctx).filter((value) => !pathValues.includes(value));
|
|
2164
|
+
if (missingPathValues.length) details.push(`[env]._.path should include ${missingPathValues.join(", ")}`);
|
|
2165
|
+
if (!text4.includes('"{{config_root}}/.mise/scripts/link-agentfiles.sh"')) details.push("link-agentfiles must use raw {{config_root}} guard");
|
|
2166
|
+
if (!text4.includes("op inject -i .env.op > .env")) details.push("[hooks].enter must materialize .env from .env.op");
|
|
2167
|
+
if (!text4.includes('patterns = ["AGENTS.md"]')) details.push("watch_files must monitor AGENTS.md");
|
|
2168
|
+
if (!text4.includes('task = "link-agentfiles"')) details.push("watch_files must dispatch link-agentfiles task");
|
|
1771
2169
|
return {
|
|
1772
2170
|
id: "mise.config-root",
|
|
1773
2171
|
title: "mise config_root + AGENTS link hooks",
|
|
@@ -1778,17 +2176,36 @@ var RULES = [
|
|
|
1778
2176
|
};
|
|
1779
2177
|
},
|
|
1780
2178
|
migrate: (ctx, finding) => {
|
|
1781
|
-
const path =
|
|
2179
|
+
const path = join8(ctx.repoRoot, "mise.toml");
|
|
1782
2180
|
const changedFiles = [];
|
|
1783
|
-
|
|
1784
|
-
|
|
2181
|
+
const details = [];
|
|
2182
|
+
if (!existsSync7(path)) {
|
|
2183
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2184
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
2185
|
+
}
|
|
2186
|
+
details.push("Initialized mise.toml from generated-project template");
|
|
2187
|
+
if (ctx.dryRun) {
|
|
2188
|
+
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
2189
|
+
}
|
|
1785
2190
|
}
|
|
1786
|
-
let
|
|
1787
|
-
const next = upsertLinkAgentfilesBlock(
|
|
1788
|
-
if (next !==
|
|
1789
|
-
changedFiles.push(path);
|
|
2191
|
+
let text4 = readText(path);
|
|
2192
|
+
const next = upsertLinkAgentfilesBlock(text4, ctx);
|
|
2193
|
+
if (next !== text4) {
|
|
2194
|
+
if (!changedFiles.includes(path)) changedFiles.push(path);
|
|
1790
2195
|
if (!ctx.dryRun) writeText(path, next);
|
|
1791
|
-
|
|
2196
|
+
text4 = next;
|
|
2197
|
+
}
|
|
2198
|
+
const linkAgentfilesPath = join8(ctx.repoRoot, ".mise", "scripts", "link-agentfiles.sh");
|
|
2199
|
+
const expectedScript = templateLinkAgentfilesScript(ctx);
|
|
2200
|
+
if (expectedScript === void 0) {
|
|
2201
|
+
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: [] };
|
|
2202
|
+
}
|
|
2203
|
+
if (safeReadText(linkAgentfilesPath) !== expectedScript) {
|
|
2204
|
+
changedFiles.push(linkAgentfilesPath);
|
|
2205
|
+
if (!ctx.dryRun) {
|
|
2206
|
+
writeText(linkAgentfilesPath, expectedScript);
|
|
2207
|
+
chmodSync(linkAgentfilesPath, 493);
|
|
2208
|
+
}
|
|
1792
2209
|
}
|
|
1793
2210
|
return {
|
|
1794
2211
|
id: finding.id,
|
|
@@ -1796,7 +2213,7 @@ var RULES = [
|
|
|
1796
2213
|
status: changedFiles.length ? "applied" : "noop",
|
|
1797
2214
|
summary: changedFiles.length ? "Updated mise AGENTS-linking contract" : "No changes required",
|
|
1798
2215
|
changedFiles,
|
|
1799
|
-
details: changedFiles.length ? ["Normalized hooks/watch_files/tasks.link-agentfiles block"] : []
|
|
2216
|
+
details: changedFiles.length ? ["Normalized hooks/watch_files/tasks.link-agentfiles block and script"] : []
|
|
1800
2217
|
};
|
|
1801
2218
|
}
|
|
1802
2219
|
},
|
|
@@ -1805,13 +2222,13 @@ var RULES = [
|
|
|
1805
2222
|
title: "managed mise versioning block",
|
|
1806
2223
|
audit: (ctx) => {
|
|
1807
2224
|
const details = [];
|
|
1808
|
-
const misePath =
|
|
1809
|
-
const versioningPath =
|
|
1810
|
-
const manifestPath =
|
|
1811
|
-
const
|
|
1812
|
-
if (!
|
|
1813
|
-
if (!
|
|
1814
|
-
if (!
|
|
2225
|
+
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
2226
|
+
const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
2227
|
+
const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
|
|
2228
|
+
const text4 = safeReadText(misePath);
|
|
2229
|
+
if (!text4?.includes("# >>> mise-versioning >>>")) details.push("mise versioning managed block missing");
|
|
2230
|
+
if (!existsSync7(versioningPath)) details.push(".mise/scripts/versioning.sh missing");
|
|
2231
|
+
if (!existsSync7(manifestPath)) details.push(".mise/version-files.conf missing");
|
|
1815
2232
|
return {
|
|
1816
2233
|
id: "mise.versioning",
|
|
1817
2234
|
title: "managed mise versioning block",
|
|
@@ -1823,17 +2240,24 @@ var RULES = [
|
|
|
1823
2240
|
},
|
|
1824
2241
|
migrate: (ctx, finding) => {
|
|
1825
2242
|
const changedFiles = [];
|
|
1826
|
-
const
|
|
1827
|
-
|
|
1828
|
-
|
|
2243
|
+
const details = [];
|
|
2244
|
+
const misePath = join8(ctx.repoRoot, "mise.toml");
|
|
2245
|
+
if (!existsSync7(misePath)) {
|
|
2246
|
+
if (!ensureMiseTomlFromTemplate(ctx, changedFiles)) {
|
|
2247
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "mise.toml missing and no generated-project mise template available to initialize from", changedFiles, details: [] };
|
|
2248
|
+
}
|
|
2249
|
+
details.push("Initialized mise.toml from generated-project template");
|
|
2250
|
+
if (ctx.dryRun) {
|
|
2251
|
+
return { id: finding.id, title: finding.title, status: "applied", summary: "Would initialize mise.toml from generated-project template", changedFiles, details };
|
|
2252
|
+
}
|
|
1829
2253
|
}
|
|
1830
2254
|
const currentMise = readText(misePath);
|
|
1831
2255
|
const nextMise = replaceOrAppendManagedBlock(currentMise, /# >>> mise-versioning >>>/, VERSIONING_BLOCK, /^\[tasks\.build\]/m);
|
|
1832
2256
|
if (nextMise !== currentMise) {
|
|
1833
|
-
changedFiles.push(misePath);
|
|
2257
|
+
if (!changedFiles.includes(misePath)) changedFiles.push(misePath);
|
|
1834
2258
|
if (!ctx.dryRun) writeText(misePath, nextMise);
|
|
1835
2259
|
}
|
|
1836
|
-
const versioningPath =
|
|
2260
|
+
const versioningPath = join8(ctx.repoRoot, ".mise", "scripts", "versioning.sh");
|
|
1837
2261
|
const expectedScript = templateVersioningScript(ctx);
|
|
1838
2262
|
if (expectedScript === void 0) {
|
|
1839
2263
|
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 +2269,7 @@ var RULES = [
|
|
|
1845
2269
|
chmodSync(versioningPath, 493);
|
|
1846
2270
|
}
|
|
1847
2271
|
}
|
|
1848
|
-
const manifestPath =
|
|
2272
|
+
const manifestPath = join8(ctx.repoRoot, ".mise", "version-files.conf");
|
|
1849
2273
|
const expectedManifest = templateVersionFilesConf(ctx, ctx.repoRoot);
|
|
1850
2274
|
if (safeReadText(manifestPath) !== expectedManifest) {
|
|
1851
2275
|
changedFiles.push(manifestPath);
|
|
@@ -1865,13 +2289,24 @@ var RULES = [
|
|
|
1865
2289
|
id: "sot.agent-symlinks",
|
|
1866
2290
|
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
1867
2291
|
audit: (ctx) => {
|
|
1868
|
-
const agentsPath =
|
|
1869
|
-
if (!
|
|
1870
|
-
|
|
2292
|
+
const agentsPath = join8(ctx.repoRoot, "AGENTS.md");
|
|
2293
|
+
if (!existsSync7(agentsPath)) {
|
|
2294
|
+
const fallbackSources = ["CLAUDE.md", "GEMINI.md", "README.md"].filter((file) => existsSync7(join8(ctx.repoRoot, file)));
|
|
2295
|
+
if (fallbackSources.length === 0) {
|
|
2296
|
+
return { id: "sot.agent-symlinks", title: "AGENTS/CLAUDE/GEMINI symlink contract", status: "skip", summary: "AGENTS.md missing; symlink contract not applicable", details: [], fixable: false };
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
id: "sot.agent-symlinks",
|
|
2300
|
+
title: "AGENTS/CLAUDE/GEMINI symlink contract",
|
|
2301
|
+
status: "fail",
|
|
2302
|
+
summary: "AGENTS.md missing but can be derived from existing project documentation",
|
|
2303
|
+
details: [`AGENTS.md can be created from ${fallbackSources[0]}`],
|
|
2304
|
+
fixable: true
|
|
2305
|
+
};
|
|
1871
2306
|
}
|
|
1872
2307
|
const details = [];
|
|
1873
2308
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1874
|
-
const full =
|
|
2309
|
+
const full = join8(ctx.repoRoot, file);
|
|
1875
2310
|
const target = readSymlinkTarget(full);
|
|
1876
2311
|
if (target !== "AGENTS.md") details.push(`${file} should be a symlink to AGENTS.md`);
|
|
1877
2312
|
}
|
|
@@ -1887,22 +2322,26 @@ var RULES = [
|
|
|
1887
2322
|
migrate: (ctx, finding) => {
|
|
1888
2323
|
const changedFiles = [];
|
|
1889
2324
|
const details = [];
|
|
1890
|
-
|
|
1891
|
-
|
|
2325
|
+
const blockedDetails = [];
|
|
2326
|
+
const bootstrap = bootstrapAgentsFile(ctx.repoRoot, ctx.dryRun);
|
|
2327
|
+
changedFiles.push(...bootstrap.changedFiles);
|
|
2328
|
+
details.push(...bootstrap.details);
|
|
2329
|
+
if (bootstrap.blocked) {
|
|
2330
|
+
return { id: finding.id, title: finding.title, status: "blocked", summary: "AGENTS.md missing; cannot derive canonical agent file", changedFiles, details: [bootstrap.blocked] };
|
|
1892
2331
|
}
|
|
1893
2332
|
for (const file of ["CLAUDE.md", "GEMINI.md"]) {
|
|
1894
|
-
const full =
|
|
2333
|
+
const full = join8(ctx.repoRoot, file);
|
|
1895
2334
|
const result = ensureSymlink(full, "AGENTS.md", ctx.dryRun);
|
|
1896
|
-
if (result.blocked)
|
|
2335
|
+
if (result.blocked) blockedDetails.push(result.blocked);
|
|
1897
2336
|
if (result.changed) changedFiles.push(full);
|
|
1898
2337
|
}
|
|
1899
2338
|
return {
|
|
1900
2339
|
id: finding.id,
|
|
1901
2340
|
title: finding.title,
|
|
1902
|
-
status:
|
|
1903
|
-
summary:
|
|
2341
|
+
status: blockedDetails.length ? "blocked" : changedFiles.length ? "applied" : "noop",
|
|
2342
|
+
summary: blockedDetails.length ? "One or more files could not be replaced safely" : changedFiles.length ? "Symlink contract repaired" : "No changes required",
|
|
1904
2343
|
changedFiles,
|
|
1905
|
-
details
|
|
2344
|
+
details: [...details, ...blockedDetails]
|
|
1906
2345
|
};
|
|
1907
2346
|
}
|
|
1908
2347
|
},
|
|
@@ -1913,7 +2352,7 @@ var RULES = [
|
|
|
1913
2352
|
migrate: (ctx, finding) => {
|
|
1914
2353
|
const changedFiles = [];
|
|
1915
2354
|
const details = [];
|
|
1916
|
-
const path =
|
|
2355
|
+
const path = join8(ctx.repoRoot, ".project.json");
|
|
1917
2356
|
const existing = readProjectJson(ctx) ?? {};
|
|
1918
2357
|
const canonical = canonicalProjectJson(ctx);
|
|
1919
2358
|
const merged = { ...existing, ...canonical };
|
|
@@ -1923,10 +2362,10 @@ var RULES = [
|
|
|
1923
2362
|
changedFiles.push(path);
|
|
1924
2363
|
if (!ctx.dryRun) writeText(path, expected);
|
|
1925
2364
|
}
|
|
1926
|
-
const planeJson =
|
|
1927
|
-
if (
|
|
2365
|
+
const planeJson = join8(ctx.repoRoot, ".plane.json");
|
|
2366
|
+
if (existsSync7(planeJson)) {
|
|
1928
2367
|
const backup = `${planeJson}.migrated-backup`;
|
|
1929
|
-
if (
|
|
2368
|
+
if (existsSync7(backup)) {
|
|
1930
2369
|
details.push(`cannot back up .plane.json because ${relative(ctx.repoRoot, backup)} already exists`);
|
|
1931
2370
|
} else {
|
|
1932
2371
|
changedFiles.push(backup);
|
|
@@ -1948,14 +2387,15 @@ var RULES = [
|
|
|
1948
2387
|
title: ".env.op + gitignore secrets contract",
|
|
1949
2388
|
audit: (ctx) => {
|
|
1950
2389
|
const details = [];
|
|
1951
|
-
const envOp = safeReadText(
|
|
1952
|
-
const gitignore = safeReadText(
|
|
2390
|
+
const envOp = safeReadText(join8(ctx.repoRoot, ".env.op"));
|
|
2391
|
+
const gitignore = safeReadText(join8(ctx.repoRoot, ".gitignore"));
|
|
1953
2392
|
if (!envOp) {
|
|
1954
2393
|
details.push(".env.op missing");
|
|
1955
2394
|
} else {
|
|
1956
2395
|
const invalidLines = envOp.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).filter((line) => {
|
|
1957
2396
|
const value = line.slice(line.indexOf("=") + 1).trim();
|
|
1958
|
-
|
|
2397
|
+
const quotedLiteral = /^"[^"\r\n]*"$/.test(value) || /^'[^'\r\n]*'$/.test(value);
|
|
2398
|
+
return !value.startsWith("op://") && !/^https?:\/\//.test(value) && !/^[A-Za-z0-9_.:-]+$/.test(value) && !quotedLiteral;
|
|
1959
2399
|
});
|
|
1960
2400
|
if (invalidLines.length) details.push(`.env.op has non-reference values that do not look like safe literals: ${invalidLines.join(", ")}`);
|
|
1961
2401
|
}
|
|
@@ -1974,12 +2414,12 @@ var RULES = [
|
|
|
1974
2414
|
migrate: (ctx, finding) => {
|
|
1975
2415
|
const changedFiles = [];
|
|
1976
2416
|
const details = [];
|
|
1977
|
-
const envOpPath =
|
|
1978
|
-
if (!
|
|
2417
|
+
const envOpPath = join8(ctx.repoRoot, ".env.op");
|
|
2418
|
+
if (!existsSync7(envOpPath)) {
|
|
1979
2419
|
changedFiles.push(envOpPath);
|
|
1980
|
-
if (!ctx.dryRun) writeText(envOpPath, readText(
|
|
2420
|
+
if (!ctx.dryRun) writeText(envOpPath, readText(join8(ctx.pjanglerRoot, "templates", "commonproject", "template", ".env.op")));
|
|
1981
2421
|
}
|
|
1982
|
-
const gitignorePath =
|
|
2422
|
+
const gitignorePath = join8(ctx.repoRoot, ".gitignore");
|
|
1983
2423
|
const gitignore = safeReadText(gitignorePath) ?? "";
|
|
1984
2424
|
const requiredBlock = `# Secrets \u2014 .env is materialized by \`op inject -i .env.op > .env\` on mise enter.
|
|
1985
2425
|
# NEVER commit it. .env.op holds only 1Password references or safe literals and IS committed.
|
|
@@ -2006,20 +2446,20 @@ var RULES = [
|
|
|
2006
2446
|
title: ".copier-answers.yml provenance + drift report",
|
|
2007
2447
|
audit: (ctx) => {
|
|
2008
2448
|
const details = [];
|
|
2009
|
-
const path =
|
|
2010
|
-
const
|
|
2449
|
+
const path = join8(ctx.repoRoot, ".copier-answers.yml");
|
|
2450
|
+
const text4 = safeReadText(path);
|
|
2011
2451
|
const project = readProjectJson(ctx);
|
|
2012
|
-
if (!
|
|
2452
|
+
if (!text4) {
|
|
2013
2453
|
details.push(".copier-answers.yml missing");
|
|
2014
2454
|
} else {
|
|
2015
|
-
if (!
|
|
2016
|
-
if (!
|
|
2455
|
+
if (!text4.startsWith("# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY")) details.push("missing Copier overwrite warning header");
|
|
2456
|
+
if (!text4.includes("_src_path:")) details.push("_src_path missing");
|
|
2017
2457
|
if (project?.project_name) {
|
|
2018
|
-
const nameMatch =
|
|
2458
|
+
const nameMatch = text4.match(/project_name:\s*(.+)/);
|
|
2019
2459
|
if (!nameMatch || nameMatch[1]?.trim() !== String(project.project_name)) details.push("project_name drift between .copier-answers.yml and .project.json");
|
|
2020
2460
|
}
|
|
2021
2461
|
if (project?.project_description) {
|
|
2022
|
-
const descMatch =
|
|
2462
|
+
const descMatch = text4.match(/project_description:\s*([\s\S]*?)(?=\n\w|$)/);
|
|
2023
2463
|
const yamlDesc = descMatch?.[1]?.replace(/\n\s+/g, " ").trim() ?? "";
|
|
2024
2464
|
if (yamlDesc !== String(project.project_description)) details.push("project_description drift between .copier-answers.yml and .project.json");
|
|
2025
2465
|
}
|
|
@@ -2036,16 +2476,16 @@ var RULES = [
|
|
|
2036
2476
|
migrate: (ctx, finding) => {
|
|
2037
2477
|
const changedFiles = [];
|
|
2038
2478
|
const project = canonicalProjectJson(ctx);
|
|
2039
|
-
const
|
|
2040
|
-
_src_path: ${
|
|
2479
|
+
const text4 = `# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
2480
|
+
_src_path: ${join8(ctx.pjanglerRoot, "templates", "commonproject")}
|
|
2041
2481
|
project_description: ${String(project.project_description)}
|
|
2042
2482
|
project_name: ${String(project.project_name)}
|
|
2043
2483
|
ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
2044
2484
|
`;
|
|
2045
|
-
const path =
|
|
2046
|
-
if (safeReadText(path) !==
|
|
2485
|
+
const path = join8(ctx.repoRoot, ".copier-answers.yml");
|
|
2486
|
+
if (safeReadText(path) !== text4) {
|
|
2047
2487
|
changedFiles.push(path);
|
|
2048
|
-
if (!ctx.dryRun) writeText(path,
|
|
2488
|
+
if (!ctx.dryRun) writeText(path, text4);
|
|
2049
2489
|
}
|
|
2050
2490
|
return {
|
|
2051
2491
|
id: finding.id,
|
|
@@ -2061,15 +2501,15 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2061
2501
|
id: "bmad.scaffold",
|
|
2062
2502
|
title: "BMAD modules/docs scaffold",
|
|
2063
2503
|
audit: (ctx) => {
|
|
2064
|
-
const sourceRoot =
|
|
2065
|
-
const targetRoot =
|
|
2504
|
+
const sourceRoot = join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad");
|
|
2505
|
+
const targetRoot = join8(ctx.repoRoot, "_bmad");
|
|
2066
2506
|
const sentinels = [
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2507
|
+
join8("core", "config.yaml"),
|
|
2508
|
+
join8("custom", "config.yaml"),
|
|
2509
|
+
join8("custom", "workflows", "ticket-lifecycle", "workflow.yaml"),
|
|
2510
|
+
join8("bmm", "workflows", "workflow-status", "workflow.yaml")
|
|
2071
2511
|
];
|
|
2072
|
-
const missing = sentinels.filter((file) =>
|
|
2512
|
+
const missing = sentinels.filter((file) => existsSync7(join8(sourceRoot, file)) && !existsSync7(join8(targetRoot, file)));
|
|
2073
2513
|
return {
|
|
2074
2514
|
id: "bmad.scaffold",
|
|
2075
2515
|
title: "BMAD modules/docs scaffold",
|
|
@@ -2081,7 +2521,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2081
2521
|
},
|
|
2082
2522
|
migrate: (ctx, finding) => {
|
|
2083
2523
|
const changedFiles = [];
|
|
2084
|
-
copyMissingRecursive(
|
|
2524
|
+
copyMissingRecursive(join8(ctx.pjanglerRoot, "templates", "commonproject", "_bmad"), join8(ctx.repoRoot, "_bmad"), changedFiles, ctx.dryRun);
|
|
2085
2525
|
return {
|
|
2086
2526
|
id: finding.id,
|
|
2087
2527
|
title: finding.title,
|
|
@@ -2102,12 +2542,12 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2102
2542
|
return { id: "hermes.pm-scaffold", title: "Hermes PM scaffold parity", status: "skip", summary: "No pm role present", details: [], fixable: false };
|
|
2103
2543
|
}
|
|
2104
2544
|
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 (!
|
|
2545
|
+
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"]) {
|
|
2546
|
+
if (!existsSync7(join8(role.roleDir, rel))) details.push(`missing ${relative(ctx.repoRoot, join8(role.roleDir, rel))}`);
|
|
2107
2547
|
}
|
|
2108
|
-
const gitmodules = safeReadText(
|
|
2548
|
+
const gitmodules = safeReadText(join8(ctx.repoRoot, ".gitmodules")) ?? "";
|
|
2109
2549
|
if (!gitmodules.includes(`agents/hermes/${role.role}/runtime`)) details.push(".gitmodules missing pm runtime submodule entry");
|
|
2110
|
-
if (!profileMetaInheritsDefault(
|
|
2550
|
+
if (!profileMetaInheritsDefault(join8(role.roleDir, "runtime", "profile.yaml"))) {
|
|
2111
2551
|
details.push("runtime/profile.yaml missing inherited default config metadata");
|
|
2112
2552
|
}
|
|
2113
2553
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -2128,79 +2568,21 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2128
2568
|
if (!role) {
|
|
2129
2569
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "No pm role present", changedFiles, details: [] };
|
|
2130
2570
|
}
|
|
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);
|
|
2571
|
+
const templateRoleDir = join8(ctx.pjanglerRoot, "templates", "hermes-agent", "template");
|
|
2572
|
+
writeIfDifferent(join8(role.roleDir, "SOUL.md"), renderSoul(role), ctx.dryRun, changedFiles);
|
|
2573
|
+
writeIfDifferent(join8(role.roleDir, "hermes"), renderHermesWrapper(role), ctx.dryRun, changedFiles, 493);
|
|
2574
|
+
writeIfDifferent(join8(role.roleDir, ".gitignore"), readText(join8(templateRoleDir, ".gitignore.jinja")).replace(/\{\{ role \}\}/g, role.role), ctx.dryRun, changedFiles);
|
|
2575
|
+
copyMissingRecursive(join8(templateRoleDir, ".runtime-scaffold"), join8(role.roleDir, ".runtime-scaffold"), changedFiles, ctx.dryRun);
|
|
2576
|
+
copyMissingRecursive(join8(templateRoleDir, ".runtime-scaffold"), join8(role.roleDir, "runtime"), changedFiles, ctx.dryRun);
|
|
2577
|
+
copyMissingRecursive(join8(templateRoleDir, ".scripts"), join8(role.roleDir, ".scripts"), changedFiles, ctx.dryRun, (source) => source.endsWith("sentinel.prompt.md.jinja"));
|
|
2578
|
+
const promptSource = join8(templateRoleDir, ".scripts", "sentinel.prompt.md.jinja");
|
|
2579
|
+
const promptTarget = join8(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
2580
|
+
if (existsSync7(promptSource) && !existsSync7(promptTarget)) {
|
|
2581
|
+
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
2582
|
writeIfDifferent(promptTarget, prompt, ctx.dryRun, changedFiles);
|
|
2201
2583
|
}
|
|
2202
2584
|
upsertSubmodule(ctx.repoRoot, role, changedFiles, ctx.dryRun);
|
|
2203
|
-
const profileMetaUpdated = upsertInheritedProfileMeta(
|
|
2585
|
+
const profileMetaUpdated = upsertInheritedProfileMeta(join8(role.roleDir, "runtime", "profile.yaml"), changedFiles, ctx.dryRun);
|
|
2204
2586
|
if (profileMetaUpdated) details.push(`updated ${profileMetaUpdated}`);
|
|
2205
2587
|
const registryUpdated = upsertRegistryEntry(role, ctx.homeDir, changedFiles, ctx.dryRun);
|
|
2206
2588
|
if (registryUpdated) details.push(`updated ${registryUpdated}`);
|
|
@@ -2208,7 +2590,7 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2208
2590
|
id: finding.id,
|
|
2209
2591
|
title: finding.title,
|
|
2210
2592
|
status: changedFiles.length ? "applied" : "noop",
|
|
2211
|
-
summary: changedFiles.length ? "
|
|
2593
|
+
summary: changedFiles.length ? "PM scaffold normalized" : "No changes required",
|
|
2212
2594
|
changedFiles,
|
|
2213
2595
|
details
|
|
2214
2596
|
};
|
|
@@ -2228,14 +2610,10 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2228
2610
|
}
|
|
2229
2611
|
const details = [];
|
|
2230
2612
|
for (const role of roles) {
|
|
2231
|
-
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-
|
|
2613
|
+
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
|
|
2232
2614
|
const state = checkUnit(unit);
|
|
2233
2615
|
if (!state.enabled || !state.active) details.push(`${unit} should be enabled+active`);
|
|
2234
2616
|
}
|
|
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
2617
|
}
|
|
2240
2618
|
return {
|
|
2241
2619
|
id: "systemd.sentinel",
|
|
@@ -2258,10 +2636,9 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2258
2636
|
return { id: finding.id, title: finding.title, status: "blocked", summary: "systemd --user unavailable on this host", changedFiles, details };
|
|
2259
2637
|
}
|
|
2260
2638
|
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)));
|
|
2639
|
+
const sysDir = join8(ctx.homeDir, ".config", "systemd", "user");
|
|
2640
|
+
const units = [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-consumer.service`, `hermes-${role.agentId}-heartbeat.timer`];
|
|
2641
|
+
const allUnitsPresent = units.every((unit) => existsSync7(join8(sysDir, unit)));
|
|
2265
2642
|
if (allUnitsPresent) {
|
|
2266
2643
|
if (ctx.dryRun) {
|
|
2267
2644
|
details.push(`would run: systemctl --user enable --now ${units.join(" ")}`);
|
|
@@ -2273,8 +2650,8 @@ ticket_provider: ${String(project.ticket_provider?.type ?? "plane")}
|
|
|
2273
2650
|
}
|
|
2274
2651
|
continue;
|
|
2275
2652
|
}
|
|
2276
|
-
for (const script of [
|
|
2277
|
-
if (!script || !
|
|
2653
|
+
for (const script of [join8(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
2654
|
+
if (!script || !existsSync7(script)) continue;
|
|
2278
2655
|
if (ctx.dryRun) {
|
|
2279
2656
|
details.push(`would run: bash ${script}`);
|
|
2280
2657
|
} else {
|
|
@@ -2304,13 +2681,16 @@ function writeIfDifferent(path, content, dryRun, changedFiles, mode) {
|
|
|
2304
2681
|
if (mode) chmodSync(path, mode);
|
|
2305
2682
|
}
|
|
2306
2683
|
}
|
|
2684
|
+
function getParityRuleIds() {
|
|
2685
|
+
return RULES.map((rule) => rule.id);
|
|
2686
|
+
}
|
|
2307
2687
|
function runAudit(repoArg) {
|
|
2308
2688
|
const pjanglerRoot = resolvePjanglerRoot();
|
|
2309
2689
|
const ctx = {
|
|
2310
2690
|
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2311
2691
|
dryRun: true,
|
|
2312
2692
|
pjanglerRoot,
|
|
2313
|
-
homeDir:
|
|
2693
|
+
homeDir: homedir4()
|
|
2314
2694
|
};
|
|
2315
2695
|
const rules = RULES.map((rule) => rule.audit(ctx));
|
|
2316
2696
|
return {
|
|
@@ -2320,17 +2700,17 @@ function runAudit(repoArg) {
|
|
|
2320
2700
|
rules
|
|
2321
2701
|
};
|
|
2322
2702
|
}
|
|
2323
|
-
function
|
|
2703
|
+
function runMigrationForRules(ruleIds, repoArg, dryRun) {
|
|
2324
2704
|
const pjanglerRoot = resolvePjanglerRoot();
|
|
2325
2705
|
const ctx = {
|
|
2326
2706
|
repoRoot: resolve(repoArg ?? process.cwd()),
|
|
2327
2707
|
dryRun,
|
|
2328
2708
|
pjanglerRoot,
|
|
2329
|
-
homeDir:
|
|
2709
|
+
homeDir: homedir4()
|
|
2330
2710
|
};
|
|
2331
|
-
const selected =
|
|
2711
|
+
const selected = RULES.filter((rule) => ruleIds.includes(rule.id));
|
|
2332
2712
|
if (!selected.length) {
|
|
2333
|
-
throw new Error(`Unknown parity
|
|
2713
|
+
throw new Error(`Unknown parity rules: ${ruleIds.join(", ")}`);
|
|
2334
2714
|
}
|
|
2335
2715
|
const results = selected.map((rule) => {
|
|
2336
2716
|
try {
|
|
@@ -2356,6 +2736,10 @@ function runMigration(selector, repoArg, dryRun, all) {
|
|
|
2356
2736
|
changedFiles
|
|
2357
2737
|
};
|
|
2358
2738
|
}
|
|
2739
|
+
function runMigration(selector, repoArg, dryRun, all) {
|
|
2740
|
+
const ruleIds = all ? RULES.map((rule) => rule.id) : selector ? [selector] : [];
|
|
2741
|
+
return runMigrationForRules(ruleIds, repoArg, dryRun);
|
|
2742
|
+
}
|
|
2359
2743
|
function formatAuditReport(report) {
|
|
2360
2744
|
const lines = [`repo: ${report.repo}`, `ok: ${report.ok}`, `audited_at: ${report.auditedAt}`, "rules:"];
|
|
2361
2745
|
for (const rule of report.rules) {
|
|
@@ -2380,19 +2764,421 @@ function formatMigrationReport(report) {
|
|
|
2380
2764
|
`;
|
|
2381
2765
|
}
|
|
2382
2766
|
|
|
2767
|
+
// src/project/index.ts
|
|
2768
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
2769
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, renameSync as renameSync2, statSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2770
|
+
import { homedir as homedir5 } from "node:os";
|
|
2771
|
+
import { basename as basename3, dirname as dirname6, join as join9, resolve as resolve2 } from "node:path";
|
|
2772
|
+
import YAML from "yaml";
|
|
2773
|
+
var PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
2774
|
+
var PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
2775
|
+
var KNOWN_SKILL_ROOTS = [
|
|
2776
|
+
"/home/delorenj/code/skillex/all-skills",
|
|
2777
|
+
"/home/delorenj/code/CoachingAgentFramework/.agents/skills",
|
|
2778
|
+
"/home/delorenj/code/pjangler/.agents/skills",
|
|
2779
|
+
join9(homedir5(), ".codex", "skills")
|
|
2780
|
+
];
|
|
2781
|
+
function projectRegistryPath(env = process.env) {
|
|
2782
|
+
return expandHome(env[PROJECT_REGISTRY_ENV] || join9(homedir5(), ".config", "pjangler", "projects.yaml"));
|
|
2783
|
+
}
|
|
2784
|
+
function emptyProjectRegistry() {
|
|
2785
|
+
return { schema_version: PROJECT_REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
2786
|
+
}
|
|
2787
|
+
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
2788
|
+
if (!existsSync8(path)) return emptyProjectRegistry();
|
|
2789
|
+
const raw = YAML.parse(readFileSync4(path, "utf8"));
|
|
2790
|
+
if (raw == null) return emptyProjectRegistry();
|
|
2791
|
+
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
2792
|
+
const registry = raw;
|
|
2793
|
+
const normalized = {
|
|
2794
|
+
schema_version: Number(registry.schema_version ?? PROJECT_REGISTRY_SCHEMA_VERSION),
|
|
2795
|
+
projects: isRecord(registry.projects) ? registry.projects : {}
|
|
2796
|
+
};
|
|
2797
|
+
validateProjectRegistry(normalized);
|
|
2798
|
+
return normalized;
|
|
2799
|
+
}
|
|
2800
|
+
function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
2801
|
+
validateProjectRegistry(registry);
|
|
2802
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
2803
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
2804
|
+
writeFileSync5(temp, YAML.stringify(registry, { lineWidth: 0 }), "utf8");
|
|
2805
|
+
renameSync2(temp, path);
|
|
2806
|
+
}
|
|
2807
|
+
function validateProjectRegistry(registry) {
|
|
2808
|
+
if (registry.schema_version !== PROJECT_REGISTRY_SCHEMA_VERSION) {
|
|
2809
|
+
throw new Error(`Unsupported project registry schema_version: ${registry.schema_version}`);
|
|
2810
|
+
}
|
|
2811
|
+
if (!isRecord(registry.projects)) throw new Error("Project registry projects must be a mapping");
|
|
2812
|
+
const slugs = /* @__PURE__ */ new Set();
|
|
2813
|
+
const repoPaths = /* @__PURE__ */ new Map();
|
|
2814
|
+
const identifiers = /* @__PURE__ */ new Map();
|
|
2815
|
+
for (const [slug, project] of Object.entries(registry.projects)) {
|
|
2816
|
+
validateProjectRecord(project, slug);
|
|
2817
|
+
if (slugs.has(project.slug)) throw new Error(`Duplicate project slug: ${project.slug}`);
|
|
2818
|
+
slugs.add(project.slug);
|
|
2819
|
+
const repoKey = resolve2(project.repo_path);
|
|
2820
|
+
const existingRepoSlug = repoPaths.get(repoKey);
|
|
2821
|
+
if (existingRepoSlug && existingRepoSlug !== slug) {
|
|
2822
|
+
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
2823
|
+
}
|
|
2824
|
+
repoPaths.set(repoKey, slug);
|
|
2825
|
+
const identifier = project.ticket_provider.identifier?.toUpperCase();
|
|
2826
|
+
if (identifier) {
|
|
2827
|
+
const existingIdentifierSlug = identifiers.get(identifier);
|
|
2828
|
+
if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
|
|
2829
|
+
throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
|
|
2830
|
+
}
|
|
2831
|
+
identifiers.set(identifier, slug);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
function slugifyProjectName(value) {
|
|
2836
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
2837
|
+
}
|
|
2838
|
+
function deriveProjectIdentifier(value) {
|
|
2839
|
+
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
2840
|
+
const identifier = compact.slice(0, 4) || "PROJ";
|
|
2841
|
+
return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
|
|
2842
|
+
}
|
|
2843
|
+
function normalizeAgentRole(value) {
|
|
2844
|
+
return value?.trim() || "pm";
|
|
2845
|
+
}
|
|
2846
|
+
function jsonStable(value) {
|
|
2847
|
+
return JSON.stringify(value);
|
|
2848
|
+
}
|
|
2849
|
+
function projectRecordEquivalent(a, b) {
|
|
2850
|
+
if (!a) return false;
|
|
2851
|
+
const { created_at: _aCreated, updated_at: _aUpdated, ...aComparable } = a;
|
|
2852
|
+
const { created_at: _bCreated, updated_at: _bUpdated, ...bComparable } = b;
|
|
2853
|
+
return jsonStable(aComparable) === jsonStable(bComparable);
|
|
2854
|
+
}
|
|
2855
|
+
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
2856
|
+
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "") || slugifyProjectName(name);
|
|
2857
|
+
return resolve2(dirname6(resolve2(cwd)), compactName);
|
|
2858
|
+
}
|
|
2859
|
+
function resolveSourceSkillPath(sourceSkill) {
|
|
2860
|
+
if (!sourceSkill) return void 0;
|
|
2861
|
+
const expanded = expandHome(sourceSkill);
|
|
2862
|
+
const direct = resolve2(expanded);
|
|
2863
|
+
if (existsSync8(direct)) return direct;
|
|
2864
|
+
const name = basename3(sourceSkill);
|
|
2865
|
+
for (const root of KNOWN_SKILL_ROOTS) {
|
|
2866
|
+
const candidate = join9(root, name);
|
|
2867
|
+
if (existsSync8(candidate)) return candidate;
|
|
2868
|
+
}
|
|
2869
|
+
const civilWarLetterifier = "/home/delorenj/code/skillex/all-skills/civilwar-letterifier";
|
|
2870
|
+
const hint = existsSync8(civilWarLetterifier) ? ` Did you mean ${civilWarLetterifier}?` : "";
|
|
2871
|
+
throw new Error(`Source skill not found: ${sourceSkill}.${hint}`);
|
|
2872
|
+
}
|
|
2873
|
+
function planProjectInit(input) {
|
|
2874
|
+
if (!input.name.trim()) throw new Error("Project name is required");
|
|
2875
|
+
const registryPath2 = resolve2(projectRegistryPath({ ...process.env, [PROJECT_REGISTRY_ENV]: input.registryPath || process.env[PROJECT_REGISTRY_ENV] }));
|
|
2876
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
2877
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2878
|
+
const slug = input.projectSlug ?? slugifyProjectName(input.name);
|
|
2879
|
+
const targetDir = resolve2(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
2880
|
+
const identifier = (input.projectIdentifier ?? deriveProjectIdentifier(input.name)).toUpperCase();
|
|
2881
|
+
const existing = registry.projects[slug];
|
|
2882
|
+
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
2883
|
+
const overwrite = input.overwrite ?? input.force ?? false;
|
|
2884
|
+
const agentRole = normalizeAgentRole(input.agentRole);
|
|
2885
|
+
const agents = input.provisionAgent ? {
|
|
2886
|
+
...existing?.agents ?? {},
|
|
2887
|
+
[agentRole]: {
|
|
2888
|
+
role: agentRole,
|
|
2889
|
+
provisioning_state: "planned"
|
|
2890
|
+
}
|
|
2891
|
+
} : existing?.agents ?? {};
|
|
2892
|
+
const scaffold = input.scaffold ?? true;
|
|
2893
|
+
const candidateProject = {
|
|
2894
|
+
name: input.name,
|
|
2895
|
+
slug,
|
|
2896
|
+
repo_path: targetDir,
|
|
2897
|
+
description: input.description ?? "",
|
|
2898
|
+
status: "planned",
|
|
2899
|
+
source_artifacts: sourceSkillPath ? [{ kind: "skill", path: sourceSkillPath, package_name: input.packageName ?? slug }] : [],
|
|
2900
|
+
template: {
|
|
2901
|
+
commonproject: {
|
|
2902
|
+
enabled: true,
|
|
2903
|
+
primary_language: input.primaryLanguage ?? "python"
|
|
2904
|
+
}
|
|
2905
|
+
},
|
|
2906
|
+
ticket_provider: {
|
|
2907
|
+
type: input.ticketProvider ?? "plane",
|
|
2908
|
+
workspace: input.planeWorkspace ?? "33god",
|
|
2909
|
+
identifier,
|
|
2910
|
+
board_id: input.planeProjectId ?? "",
|
|
2911
|
+
board_url: input.planeProjectId ? `https://plane.delo.sh/${input.planeWorkspace ?? "33god"}/projects/${input.planeProjectId}/issues/` : "",
|
|
2912
|
+
state: input.live ? "planned" : "planned"
|
|
2913
|
+
},
|
|
2914
|
+
agents,
|
|
2915
|
+
created_at: existing?.created_at ?? now,
|
|
2916
|
+
updated_at: now
|
|
2917
|
+
};
|
|
2918
|
+
const project = {
|
|
2919
|
+
...candidateProject,
|
|
2920
|
+
updated_at: projectRecordEquivalent(existing, candidateProject) ? existing.updated_at : now
|
|
2921
|
+
};
|
|
2922
|
+
validateNoDuplicateProject(registry, project, overwrite);
|
|
2923
|
+
const pjanglerRoot = resolve2(input.pjanglerRoot ?? resolvePjanglerRoot2());
|
|
2924
|
+
const manifest = projectManifestFromRegistryProject(project);
|
|
2925
|
+
const apply = input.apply ?? false;
|
|
2926
|
+
const live = input.live ?? false;
|
|
2927
|
+
const actions = [
|
|
2928
|
+
{ kind: "registry.upsert", registryPath: registryPath2, slug, project }
|
|
2929
|
+
];
|
|
2930
|
+
if (scaffold) {
|
|
2931
|
+
actions.push(buildCommonProjectCopierAction({
|
|
2932
|
+
pjanglerRoot,
|
|
2933
|
+
targetDir,
|
|
2934
|
+
projectName: project.name,
|
|
2935
|
+
projectDescription: project.description,
|
|
2936
|
+
projectSlug: project.slug,
|
|
2937
|
+
ticketProvider: project.ticket_provider.type,
|
|
2938
|
+
planeWorkspace: project.ticket_provider.workspace ?? "33god",
|
|
2939
|
+
planeProjectId: project.ticket_provider.board_id ?? "",
|
|
2940
|
+
projectIdentifier: identifier,
|
|
2941
|
+
primaryLanguage: project.template.commonproject.primary_language,
|
|
2942
|
+
overwrite
|
|
2943
|
+
}));
|
|
2944
|
+
}
|
|
2945
|
+
actions.push(
|
|
2946
|
+
{ kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
|
|
2947
|
+
{
|
|
2948
|
+
kind: "plane.create-or-link",
|
|
2949
|
+
enabled: live,
|
|
2950
|
+
live,
|
|
2951
|
+
workspace: project.ticket_provider.workspace ?? "33god",
|
|
2952
|
+
identifier,
|
|
2953
|
+
state: live ? "planned" : "planned",
|
|
2954
|
+
reason: live ? void 0 : "network/cloud actions require --live"
|
|
2955
|
+
},
|
|
2956
|
+
{
|
|
2957
|
+
kind: "hermes.provision-agent",
|
|
2958
|
+
enabled: input.provisionAgent ?? false,
|
|
2959
|
+
local: !live,
|
|
2960
|
+
targetDir,
|
|
2961
|
+
targetRepo: slug,
|
|
2962
|
+
role: agentRole,
|
|
2963
|
+
context: {
|
|
2964
|
+
skipRuntimeRepo: !live,
|
|
2965
|
+
skipPlane: !live,
|
|
2966
|
+
skipBloodbank: !live,
|
|
2967
|
+
skipSystemd: !live || process.platform === "darwin"
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
);
|
|
2971
|
+
return { ok: true, apply, dryRun: !apply, live, registryPath: registryPath2, project, manifest, actions };
|
|
2972
|
+
}
|
|
2973
|
+
function executeProjectInitPlan(plan) {
|
|
2974
|
+
const logs = [];
|
|
2975
|
+
const errors = [];
|
|
2976
|
+
const changedFiles = [];
|
|
2977
|
+
if (!plan.apply) return { ok: true, plan, logs, errors, changedFiles };
|
|
2978
|
+
const registry = loadProjectRegistry(plan.registryPath);
|
|
2979
|
+
let pendingRegistryAction;
|
|
2980
|
+
for (const action of plan.actions) {
|
|
2981
|
+
if (action.kind === "copier.copy.commonproject") {
|
|
2982
|
+
mkdirSync6(dirname6(action.targetDir), { recursive: true });
|
|
2983
|
+
const result = spawnSync5(action.command[0], action.command.slice(1), { encoding: "utf8", cwd: action.cwd });
|
|
2984
|
+
if (result.stdout?.trim()) logs.push(result.stdout.trim());
|
|
2985
|
+
if (result.stderr?.trim()) logs.push(result.stderr.trim());
|
|
2986
|
+
if (result.error) {
|
|
2987
|
+
const code = result.error.code;
|
|
2988
|
+
errors.push(
|
|
2989
|
+
code === "ENOENT" ? "copier not found on PATH. Install with: uv tool install copier or pip install copier" : `copier failed: ${result.error.message}`
|
|
2990
|
+
);
|
|
2991
|
+
break;
|
|
2992
|
+
}
|
|
2993
|
+
if (result.status !== 0) {
|
|
2994
|
+
errors.push(`copier exited with status ${result.status ?? "unknown"}`);
|
|
2995
|
+
if (existsSync8(action.targetDir)) changedFiles.push(action.targetDir);
|
|
2996
|
+
break;
|
|
2997
|
+
}
|
|
2998
|
+
changedFiles.push(action.targetDir);
|
|
2999
|
+
} else if (action.kind === "project.write-manifest") {
|
|
3000
|
+
mkdirSync6(dirname6(action.path), { recursive: true });
|
|
3001
|
+
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
3002
|
+
`;
|
|
3003
|
+
const current = existsSync8(action.path) ? readFileSync4(action.path, "utf8") : void 0;
|
|
3004
|
+
if (current !== next) {
|
|
3005
|
+
writeFileSync5(action.path, next, "utf8");
|
|
3006
|
+
changedFiles.push(action.path);
|
|
3007
|
+
}
|
|
3008
|
+
} else if (action.kind === "registry.upsert") {
|
|
3009
|
+
pendingRegistryAction = action;
|
|
3010
|
+
} else if (action.kind === "plane.create-or-link") {
|
|
3011
|
+
logs.push(action.enabled ? "plane.create-or-link requires a live provider integration" : "plane.create-or-link skipped (requires --live)");
|
|
3012
|
+
} else if (action.kind === "hermes.provision-agent") {
|
|
3013
|
+
logs.push(action.enabled ? "hermes.provision-agent planned for the caller to execute" : "hermes.provision-agent skipped");
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
if (pendingRegistryAction && errors.length === 0) {
|
|
3017
|
+
if (!projectRecordEquivalent(registry.projects[pendingRegistryAction.slug], pendingRegistryAction.project)) {
|
|
3018
|
+
registry.projects[pendingRegistryAction.slug] = pendingRegistryAction.project;
|
|
3019
|
+
saveProjectRegistry(registry, pendingRegistryAction.registryPath);
|
|
3020
|
+
changedFiles.push(pendingRegistryAction.registryPath);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
return { ok: errors.length === 0, plan, logs, errors, changedFiles };
|
|
3024
|
+
}
|
|
3025
|
+
function projectManifestFromRegistryProject(project) {
|
|
3026
|
+
const agents = Object.fromEntries(
|
|
3027
|
+
Object.entries(project.agents).map(([name, agent]) => [
|
|
3028
|
+
`${project.slug}-${name}`,
|
|
3029
|
+
{
|
|
3030
|
+
role: agent.role,
|
|
3031
|
+
role_dir: agent.role_dir,
|
|
3032
|
+
provisioning_state: agent.provisioning_state
|
|
3033
|
+
}
|
|
3034
|
+
])
|
|
3035
|
+
);
|
|
3036
|
+
return {
|
|
3037
|
+
project_name: project.name,
|
|
3038
|
+
project_description: project.description,
|
|
3039
|
+
project_slug: project.slug,
|
|
3040
|
+
repo_path: project.repo_path,
|
|
3041
|
+
ticket_provider: {
|
|
3042
|
+
type: project.ticket_provider.type,
|
|
3043
|
+
workspace: project.ticket_provider.workspace ?? "",
|
|
3044
|
+
identifier: project.ticket_provider.identifier ?? "",
|
|
3045
|
+
board_id: project.ticket_provider.board_id ?? "",
|
|
3046
|
+
board_url: project.ticket_provider.board_url ?? "",
|
|
3047
|
+
state: project.ticket_provider.state
|
|
3048
|
+
},
|
|
3049
|
+
agents
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
function formatProjectInitPlan(plan) {
|
|
3053
|
+
const lines = [
|
|
3054
|
+
`${plan.dryRun ? "[DRY RUN] " : ""}Project init plan: ${plan.project.name} (${plan.project.slug})`,
|
|
3055
|
+
`Registry: ${plan.registryPath}`,
|
|
3056
|
+
`Target: ${plan.project.repo_path}`,
|
|
3057
|
+
"Actions:"
|
|
3058
|
+
];
|
|
3059
|
+
for (const action of plan.actions) {
|
|
3060
|
+
lines.push(` - ${action.kind}`);
|
|
3061
|
+
if (action.kind === "copier.copy.commonproject") lines.push(` target: ${action.targetDir}`);
|
|
3062
|
+
if (action.kind === "project.write-manifest") lines.push(` path: ${action.path}`);
|
|
3063
|
+
if (action.kind === "plane.create-or-link" && action.reason) lines.push(` note: ${action.reason}`);
|
|
3064
|
+
}
|
|
3065
|
+
return lines.join("\n");
|
|
3066
|
+
}
|
|
3067
|
+
function formatProjectList(registry) {
|
|
3068
|
+
const projects = Object.values(registry.projects).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
3069
|
+
if (!projects.length) return "No projects registered.";
|
|
3070
|
+
return projects.map((project) => `${project.slug.padEnd(18)} ${String(project.ticket_provider.identifier ?? "").padEnd(6)} ${project.status.padEnd(8)} ${project.repo_path}`).join("\n");
|
|
3071
|
+
}
|
|
3072
|
+
function getProject(registry, slug) {
|
|
3073
|
+
const project = registry.projects[slug];
|
|
3074
|
+
if (!project) throw new Error(`Project not found in registry: ${slug}`);
|
|
3075
|
+
return project;
|
|
3076
|
+
}
|
|
3077
|
+
function doctorProjectRegistry(registryPath2 = projectRegistryPath(), slug) {
|
|
3078
|
+
const issues = [];
|
|
3079
|
+
const registry = loadProjectRegistry(registryPath2);
|
|
3080
|
+
const projects = slug ? [[slug, getProject(registry, slug)]] : Object.entries(registry.projects);
|
|
3081
|
+
for (const [projectSlug, project] of projects) {
|
|
3082
|
+
if (!existsSync8(project.repo_path)) {
|
|
3083
|
+
issues.push({ level: "warn", slug: projectSlug, message: `repo_path does not exist: ${project.repo_path}` });
|
|
3084
|
+
} else if (!statSync(project.repo_path).isDirectory()) {
|
|
3085
|
+
issues.push({ level: "error", slug: projectSlug, message: `repo_path is not a directory: ${project.repo_path}` });
|
|
3086
|
+
} else {
|
|
3087
|
+
const manifestPath = join9(project.repo_path, ".project.json");
|
|
3088
|
+
if (!existsSync8(manifestPath)) issues.push({ level: "warn", slug: projectSlug, message: ".project.json is missing" });
|
|
3089
|
+
}
|
|
3090
|
+
for (const artifact of project.source_artifacts) {
|
|
3091
|
+
if (artifact.path && !existsSync8(artifact.path)) {
|
|
3092
|
+
issues.push({ level: "warn", slug: projectSlug, message: `source artifact missing: ${artifact.path}` });
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
return {
|
|
3097
|
+
ok: !issues.some((issue) => issue.level === "error"),
|
|
3098
|
+
registryPath: registryPath2,
|
|
3099
|
+
checkedProjects: projects.map(([projectSlug]) => projectSlug),
|
|
3100
|
+
issues
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
function buildCommonProjectCopierAction(input) {
|
|
3104
|
+
const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
|
|
3105
|
+
const data = {
|
|
3106
|
+
project_name: input.projectName,
|
|
3107
|
+
project_description: input.projectDescription ?? "",
|
|
3108
|
+
project_slug: input.projectSlug,
|
|
3109
|
+
ticket_provider: input.ticketProvider,
|
|
3110
|
+
plane_workspace: input.planeWorkspace,
|
|
3111
|
+
plane_project_id: input.planeProjectId ?? "",
|
|
3112
|
+
project_identifier: input.projectIdentifier,
|
|
3113
|
+
primary_language: input.primaryLanguage
|
|
3114
|
+
};
|
|
3115
|
+
const command = ["copier", "copy", "--trust", templateDir, input.targetDir, "--defaults"];
|
|
3116
|
+
for (const [key, value] of Object.entries(data)) command.push("--data", `${key}=${value}`);
|
|
3117
|
+
if (input.overwrite) command.push("--overwrite");
|
|
3118
|
+
return {
|
|
3119
|
+
kind: "copier.copy.commonproject",
|
|
3120
|
+
cwd: input.pjanglerRoot,
|
|
3121
|
+
command,
|
|
3122
|
+
targetDir: input.targetDir,
|
|
3123
|
+
data,
|
|
3124
|
+
overwrite: input.overwrite
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
function resolvePjanglerRoot2() {
|
|
3128
|
+
let dir = dirname6(new URL(import.meta.url).pathname);
|
|
3129
|
+
while (dir !== dirname6(dir)) {
|
|
3130
|
+
if (existsSync8(join9(dir, "package.json")) && existsSync8(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
3131
|
+
dir = dirname6(dir);
|
|
3132
|
+
}
|
|
3133
|
+
return resolve2(process.cwd());
|
|
3134
|
+
}
|
|
3135
|
+
function validateNoDuplicateProject(registry, project, overwrite) {
|
|
3136
|
+
const existingSameSlug = registry.projects[project.slug];
|
|
3137
|
+
if (existingSameSlug && !overwrite && resolve2(existingSameSlug.repo_path) !== resolve2(project.repo_path)) {
|
|
3138
|
+
throw new Error(`Project slug already exists in registry: ${project.slug}`);
|
|
3139
|
+
}
|
|
3140
|
+
for (const [slug, existing] of Object.entries(registry.projects)) {
|
|
3141
|
+
if (slug === project.slug) continue;
|
|
3142
|
+
if (resolve2(existing.repo_path) === resolve2(project.repo_path)) {
|
|
3143
|
+
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
3144
|
+
}
|
|
3145
|
+
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
3146
|
+
throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
function validateProjectRecord(project, key) {
|
|
3151
|
+
if (!isRecord(project)) throw new Error(`Project ${key} must be a mapping`);
|
|
3152
|
+
if (!project.name) throw new Error(`Project ${key} missing name`);
|
|
3153
|
+
if (!project.slug) throw new Error(`Project ${key} missing slug`);
|
|
3154
|
+
if (project.slug !== key) throw new Error(`Project key ${key} does not match slug ${project.slug}`);
|
|
3155
|
+
if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
|
|
3156
|
+
if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
|
|
3157
|
+
if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
|
|
3158
|
+
if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
|
|
3159
|
+
}
|
|
3160
|
+
function expandHome(path) {
|
|
3161
|
+
if (path === "~") return homedir5();
|
|
3162
|
+
if (path.startsWith("~/")) return join9(homedir5(), path.slice(2));
|
|
3163
|
+
return path;
|
|
3164
|
+
}
|
|
3165
|
+
function isRecord(value) {
|
|
3166
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
2383
3169
|
// src/utils/version.ts
|
|
2384
|
-
import { readFileSync as
|
|
2385
|
-
import { dirname as
|
|
2386
|
-
import { fileURLToPath as
|
|
3170
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
3171
|
+
import { dirname as dirname7, join as join10 } from "node:path";
|
|
3172
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2387
3173
|
var PJANGLER_VERSION = (() => {
|
|
2388
3174
|
try {
|
|
2389
|
-
let dir =
|
|
3175
|
+
let dir = dirname7(fileURLToPath4(import.meta.url));
|
|
2390
3176
|
for (let i = 0; i < 4; i++) {
|
|
2391
3177
|
try {
|
|
2392
|
-
const raw =
|
|
3178
|
+
const raw = readFileSync5(join10(dir, "package.json"), "utf8");
|
|
2393
3179
|
return JSON.parse(raw).version ?? "0.0.0";
|
|
2394
3180
|
} catch {
|
|
2395
|
-
const parent =
|
|
3181
|
+
const parent = dirname7(dir);
|
|
2396
3182
|
if (parent === dir) break;
|
|
2397
3183
|
dir = parent;
|
|
2398
3184
|
}
|
|
@@ -2403,6 +3189,190 @@ var PJANGLER_VERSION = (() => {
|
|
|
2403
3189
|
})();
|
|
2404
3190
|
|
|
2405
3191
|
// src/index.ts
|
|
3192
|
+
function printMigrationReport(report, asJson) {
|
|
3193
|
+
if (asJson) {
|
|
3194
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3195
|
+
} else {
|
|
3196
|
+
console.log(formatMigrationReport(report));
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
async function promptForRuleIds(rules) {
|
|
3200
|
+
const options = rules.filter((rule) => rule.fixable).map((rule) => ({
|
|
3201
|
+
value: rule.id,
|
|
3202
|
+
label: `${rule.id} [${rule.status}] ${rule.title}`,
|
|
3203
|
+
hint: rule.summary
|
|
3204
|
+
}));
|
|
3205
|
+
if (!options.length) {
|
|
3206
|
+
return [];
|
|
3207
|
+
}
|
|
3208
|
+
const initialValues = rules.filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => rule.id);
|
|
3209
|
+
const selected = await multiselect({
|
|
3210
|
+
message: "Select parity rules to apply (space to toggle, enter to confirm):",
|
|
3211
|
+
options,
|
|
3212
|
+
initialValues
|
|
3213
|
+
});
|
|
3214
|
+
if (isCancel5(selected)) {
|
|
3215
|
+
return [];
|
|
3216
|
+
}
|
|
3217
|
+
return selected;
|
|
3218
|
+
}
|
|
3219
|
+
function readJson(path) {
|
|
3220
|
+
if (!existsSync9(path)) return void 0;
|
|
3221
|
+
try {
|
|
3222
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
3223
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
3224
|
+
} catch {
|
|
3225
|
+
return void 0;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
function findGitRoot(cwd) {
|
|
3229
|
+
const result = spawnSync6("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
3230
|
+
if (result.status !== 0) return void 0;
|
|
3231
|
+
return resolve3(result.stdout.trim());
|
|
3232
|
+
}
|
|
3233
|
+
function packageNameToProjectName(value) {
|
|
3234
|
+
if (!value) return void 0;
|
|
3235
|
+
const name = value.split("/").pop() ?? value;
|
|
3236
|
+
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
3237
|
+
}
|
|
3238
|
+
function deriveProjectDefaults(targetDir) {
|
|
3239
|
+
const manifest = readJson(join11(targetDir, ".project.json"));
|
|
3240
|
+
const pkg = readJson(join11(targetDir, "package.json"));
|
|
3241
|
+
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename4(targetDir)) || "Project";
|
|
3242
|
+
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
3243
|
+
return {
|
|
3244
|
+
name,
|
|
3245
|
+
description: String(manifest?.project_description ?? pkg?.description ?? ""),
|
|
3246
|
+
slug: typeof manifest?.project_slug === "string" ? manifest.project_slug : void 0,
|
|
3247
|
+
identifier: typeof ticketProvider.identifier === "string" ? ticketProvider.identifier : void 0
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
function isInteractiveProjectInit(options) {
|
|
3251
|
+
return !options.json && !options.yes && options.tui !== false && Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
3252
|
+
}
|
|
3253
|
+
async function promptTextValue(message, initialValue) {
|
|
3254
|
+
const value = await text3({
|
|
3255
|
+
message,
|
|
3256
|
+
initialValue,
|
|
3257
|
+
validate: (input) => input?.trim() ? void 0 : "Required"
|
|
3258
|
+
});
|
|
3259
|
+
if (isCancel5(value)) {
|
|
3260
|
+
cancel2("project init cancelled");
|
|
3261
|
+
process.exit(1);
|
|
3262
|
+
}
|
|
3263
|
+
return value.trim();
|
|
3264
|
+
}
|
|
3265
|
+
function projectInitActionLabel(kind) {
|
|
3266
|
+
switch (kind) {
|
|
3267
|
+
case "registry.upsert":
|
|
3268
|
+
return "Register/update project registry entry";
|
|
3269
|
+
case "copier.copy.commonproject":
|
|
3270
|
+
return "Render CommonProject scaffold";
|
|
3271
|
+
case "project.write-manifest":
|
|
3272
|
+
return "Write repo-local .project.json projection";
|
|
3273
|
+
case "plane.create-or-link":
|
|
3274
|
+
return "Create/link ticket provider project";
|
|
3275
|
+
case "hermes.provision-agent":
|
|
3276
|
+
return "Provision Hermes agent";
|
|
3277
|
+
default:
|
|
3278
|
+
return kind;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
function registryNeedsUpsert(plan) {
|
|
3282
|
+
const registry = loadProjectRegistry(plan.registryPath);
|
|
3283
|
+
const existing = registry.projects[plan.project.slug];
|
|
3284
|
+
if (!existing) return true;
|
|
3285
|
+
const { created_at: _existingCreated, updated_at: _existingUpdated, ...existingComparable } = existing;
|
|
3286
|
+
const { created_at: _projectCreated, updated_at: _projectUpdated, ...projectComparable } = plan.project;
|
|
3287
|
+
return JSON.stringify(existingComparable) !== JSON.stringify(projectComparable);
|
|
3288
|
+
}
|
|
3289
|
+
function actionNeedsRun(plan, kind, syncMode) {
|
|
3290
|
+
if (kind === "registry.upsert") return registryNeedsUpsert(plan);
|
|
3291
|
+
if (kind === "project.write-manifest") {
|
|
3292
|
+
const action = plan.actions.find((item) => item.kind === "project.write-manifest");
|
|
3293
|
+
if (!action || action.kind !== "project.write-manifest") return false;
|
|
3294
|
+
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
3295
|
+
`;
|
|
3296
|
+
return !existsSync9(action.path) || readFileSync6(action.path, "utf8") !== next;
|
|
3297
|
+
}
|
|
3298
|
+
if (kind === "copier.copy.commonproject") return true;
|
|
3299
|
+
if (kind === "plane.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
3300
|
+
if (kind === "hermes.provision-agent") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
3301
|
+
return true;
|
|
3302
|
+
}
|
|
3303
|
+
async function selectProjectInitOperations(input) {
|
|
3304
|
+
const planOperations = input.plan.actions.filter((action) => actionNeedsRun(input.plan, action.kind, input.syncMode)).map((action) => ({
|
|
3305
|
+
value: action.kind,
|
|
3306
|
+
label: projectInitActionLabel(action.kind),
|
|
3307
|
+
hint: action.kind === "registry.upsert" ? input.plan.registryPath : action.kind
|
|
3308
|
+
}));
|
|
3309
|
+
const parityOperations = input.auditRules.filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => ({
|
|
3310
|
+
value: `parity:${rule.id}`,
|
|
3311
|
+
label: `${rule.title}`,
|
|
3312
|
+
hint: `${rule.id}: ${rule.summary}`
|
|
3313
|
+
}));
|
|
3314
|
+
const operations = [...planOperations, ...parityOperations];
|
|
3315
|
+
const all = operations.map((operation) => operation.value);
|
|
3316
|
+
if (input.options.yes || input.options.apply && !isInteractiveProjectInit(input.options)) {
|
|
3317
|
+
return {
|
|
3318
|
+
selectedOperations: all,
|
|
3319
|
+
selectedParityRules: parityOperations.map((operation) => operation.value.replace(/^parity:/, ""))
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
if (input.options.dryRun || !isInteractiveProjectInit(input.options)) {
|
|
3323
|
+
return { selectedOperations: [], selectedParityRules: [] };
|
|
3324
|
+
}
|
|
3325
|
+
if (!operations.length) return { selectedOperations: [], selectedParityRules: [] };
|
|
3326
|
+
const selected = await multiselect({
|
|
3327
|
+
message: "Select project init operations to run:",
|
|
3328
|
+
options: operations,
|
|
3329
|
+
initialValues: all
|
|
3330
|
+
});
|
|
3331
|
+
if (isCancel5(selected)) {
|
|
3332
|
+
cancel2("project init cancelled");
|
|
3333
|
+
process.exit(1);
|
|
3334
|
+
}
|
|
3335
|
+
return {
|
|
3336
|
+
selectedOperations: selected,
|
|
3337
|
+
selectedParityRules: selected.filter((value) => value.startsWith("parity:")).map((value) => value.replace(/^parity:/, ""))
|
|
3338
|
+
};
|
|
3339
|
+
}
|
|
3340
|
+
async function resolveProjectInitTarget(name, options) {
|
|
3341
|
+
const interactive = isInteractiveProjectInit(options);
|
|
3342
|
+
const cwd = process.cwd();
|
|
3343
|
+
const cwdGitRoot = findGitRoot(cwd);
|
|
3344
|
+
let targetDir = options.targetDir ? resolve3(options.targetDir) : void 0;
|
|
3345
|
+
if (!targetDir && cwdGitRoot) {
|
|
3346
|
+
targetDir = cwdGitRoot;
|
|
3347
|
+
}
|
|
3348
|
+
if (!targetDir && interactive) {
|
|
3349
|
+
const defaultName = name ?? basename4(cwd);
|
|
3350
|
+
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
3351
|
+
const defaultDir = join11(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3352
|
+
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
3353
|
+
name = promptedName;
|
|
3354
|
+
}
|
|
3355
|
+
if (!targetDir) {
|
|
3356
|
+
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
3357
|
+
targetDir = resolve3(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
3358
|
+
}
|
|
3359
|
+
const targetExists = existsSync9(targetDir);
|
|
3360
|
+
if (targetExists && !statSync2(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
3361
|
+
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
3362
|
+
const syncMode = Boolean(targetGitRoot && resolve3(targetGitRoot) === resolve3(targetDir));
|
|
3363
|
+
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename4(targetDir)) ?? "Project", description: "" };
|
|
3364
|
+
if (!name && interactive && !syncMode) {
|
|
3365
|
+
name = await promptTextValue("Project name", defaults.name);
|
|
3366
|
+
}
|
|
3367
|
+
return {
|
|
3368
|
+
name: name ?? defaults.name,
|
|
3369
|
+
targetDir,
|
|
3370
|
+
description: options.description ?? defaults.description,
|
|
3371
|
+
syncMode,
|
|
3372
|
+
slug: options.slug ?? defaults.slug,
|
|
3373
|
+
identifier: options.identifier ?? defaults.identifier
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
2406
3376
|
var program = new Command3();
|
|
2407
3377
|
program.name("pjangler").description("Project subsystem bootstrapper CLI").version(PJANGLER_VERSION);
|
|
2408
3378
|
program.command("init").argument("<subsystem>", "Subsystem to initialize").description("Initialize a project subsystem").option("--dry-run", "Preview changes without writing files").option("-f, --force", "Overwrite existing files").action(async (subsystem, options) => {
|
|
@@ -2436,6 +3406,144 @@ program.command("list").description("List available subsystems").action(() => {
|
|
|
2436
3406
|
console.log(" pjangler init docker");
|
|
2437
3407
|
console.log(" pjangler init node");
|
|
2438
3408
|
});
|
|
3409
|
+
var projectCmd = program.command("project").description("Manage the pjangler project registry");
|
|
3410
|
+
projectCmd.command("init").argument("[name]", "Project display name").description("Plan or apply a registry-backed CommonProject initialization or legacy repo sync").option("--description <text>", "Project description").option("--target-dir <path>", "Target repo path").option("--source-skill <path>", "Source skill/template provenance path").option("--primary-language <language>", "Primary language for CommonProject rendering", "python").option("--provision-agent", "Plan local Hermes PM agent provisioning").option("--agent-role <role>", "Hermes agent role to plan when --provision-agent is set", "pm").option("--apply", "Write the registry and render the repo scaffold").option("--dry-run", "Preview changes without writing files (default)").option("--live", "Allow live/network/cloud provisioning actions").option("--slug <slug>", "Project registry slug override").option("--identifier <identifier>", "Ticket identifier override").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("-f, --force", "Allow replacing an existing registry entry and re-rendering files").option("-y, --yes", "Apply every proposed operation without prompting").option("--no-tui", "Disable interactive prompts").option("--json", "Output machine-parseable JSON").action(async (name, options) => {
|
|
3411
|
+
try {
|
|
3412
|
+
const target = await resolveProjectInitTarget(name, options);
|
|
3413
|
+
const interactive = isInteractiveProjectInit(options);
|
|
3414
|
+
const apply = Boolean(!options.dryRun && (options.yes || options.apply || interactive));
|
|
3415
|
+
const plan = planProjectInit({
|
|
3416
|
+
name: target.name,
|
|
3417
|
+
description: target.description,
|
|
3418
|
+
targetDir: target.targetDir,
|
|
3419
|
+
sourceSkill: options.sourceSkill,
|
|
3420
|
+
primaryLanguage: options.primaryLanguage,
|
|
3421
|
+
provisionAgent: options.provisionAgent ?? false,
|
|
3422
|
+
agentRole: options.agentRole,
|
|
3423
|
+
apply,
|
|
3424
|
+
live: options.live ?? false,
|
|
3425
|
+
projectSlug: target.slug,
|
|
3426
|
+
projectIdentifier: target.identifier,
|
|
3427
|
+
registryPath: options.registry,
|
|
3428
|
+
force: options.force ?? false,
|
|
3429
|
+
overwrite: options.force ?? false,
|
|
3430
|
+
cwd: process.cwd(),
|
|
3431
|
+
scaffold: !target.syncMode
|
|
3432
|
+
});
|
|
3433
|
+
const audit = target.syncMode ? runAudit(target.targetDir) : void 0;
|
|
3434
|
+
const selection = await selectProjectInitOperations({
|
|
3435
|
+
plan,
|
|
3436
|
+
auditRules: audit?.rules ?? [],
|
|
3437
|
+
syncMode: target.syncMode,
|
|
3438
|
+
options
|
|
3439
|
+
});
|
|
3440
|
+
const selectedPlanActionKinds = new Set(selection.selectedOperations.filter((value) => !value.startsWith("parity:")));
|
|
3441
|
+
const selectedPlan = {
|
|
3442
|
+
...plan,
|
|
3443
|
+
apply,
|
|
3444
|
+
dryRun: !apply,
|
|
3445
|
+
actions: apply ? plan.actions.filter((action) => selectedPlanActionKinds.has(action.kind)) : plan.actions
|
|
3446
|
+
};
|
|
3447
|
+
if (!apply) {
|
|
3448
|
+
const payload = {
|
|
3449
|
+
...plan,
|
|
3450
|
+
mode: target.syncMode ? "sync" : "create",
|
|
3451
|
+
audit,
|
|
3452
|
+
proposedOperations: [
|
|
3453
|
+
...plan.actions.filter((action) => actionNeedsRun(plan, action.kind, target.syncMode)).map((action) => action.kind),
|
|
3454
|
+
...(audit?.rules ?? []).filter((rule) => rule.fixable && rule.status !== "pass" && rule.status !== "skip").map((rule) => `parity:${rule.id}`)
|
|
3455
|
+
]
|
|
3456
|
+
};
|
|
3457
|
+
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
3458
|
+
else {
|
|
3459
|
+
console.log(formatProjectInitPlan(plan));
|
|
3460
|
+
if (payload.proposedOperations.length) {
|
|
3461
|
+
console.log("Proposed sync operations:");
|
|
3462
|
+
for (const operation of payload.proposedOperations) console.log(` - ${operation}`);
|
|
3463
|
+
} else {
|
|
3464
|
+
console.log("Project is already in parity.");
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
const initResult = selectedPlan.actions.length ? executeProjectInitPlan(selectedPlan) : { ok: true, plan: selectedPlan, logs: [], errors: [], changedFiles: [] };
|
|
3470
|
+
const migrationReport = selection.selectedParityRules.length ? runMigrationForRules(selection.selectedParityRules, target.targetDir, false) : void 0;
|
|
3471
|
+
const migrationErrors = migrationReport?.results.filter((result2) => result2.status === "blocked").map((result2) => `${result2.id}: ${result2.summary}`) ?? [];
|
|
3472
|
+
const changedFiles = Array.from(/* @__PURE__ */ new Set([
|
|
3473
|
+
...initResult.changedFiles,
|
|
3474
|
+
...migrationReport?.changedFiles ?? []
|
|
3475
|
+
])).sort();
|
|
3476
|
+
const result = {
|
|
3477
|
+
ok: initResult.ok && (migrationReport?.ok ?? true),
|
|
3478
|
+
mode: target.syncMode ? "sync" : "create",
|
|
3479
|
+
plan: selectedPlan,
|
|
3480
|
+
audit,
|
|
3481
|
+
selectedOperations: selection.selectedOperations,
|
|
3482
|
+
selectedParityRules: selection.selectedParityRules,
|
|
3483
|
+
logs: initResult.logs,
|
|
3484
|
+
errors: [...initResult.errors, ...migrationErrors],
|
|
3485
|
+
changedFiles,
|
|
3486
|
+
migrationReport
|
|
3487
|
+
};
|
|
3488
|
+
if (options.json) {
|
|
3489
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3490
|
+
} else {
|
|
3491
|
+
console.log(formatProjectInitPlan(selectedPlan));
|
|
3492
|
+
for (const line of result.logs) console.log(line);
|
|
3493
|
+
for (const line of result.errors) console.error(line);
|
|
3494
|
+
if (migrationReport) console.log(formatMigrationReport(migrationReport));
|
|
3495
|
+
if (result.ok && changedFiles.length) console.log(`Project synchronized: ${plan.project.slug}`);
|
|
3496
|
+
if (result.ok && changedFiles.length === 0) console.log(`Project already in parity: ${plan.project.slug}`);
|
|
3497
|
+
}
|
|
3498
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
3499
|
+
} catch (err) {
|
|
3500
|
+
if (options.json) {
|
|
3501
|
+
console.log(JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }, null, 2));
|
|
3502
|
+
} else {
|
|
3503
|
+
console.error("\u274C project init failed:", err instanceof Error ? err.message : err);
|
|
3504
|
+
}
|
|
3505
|
+
process.exit(1);
|
|
3506
|
+
}
|
|
3507
|
+
});
|
|
3508
|
+
projectCmd.command("list").description("List projects in the pjangler registry").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((options) => {
|
|
3509
|
+
try {
|
|
3510
|
+
const registry = loadProjectRegistry(options.registry ?? projectRegistryPath());
|
|
3511
|
+
if (options.json) console.log(JSON.stringify(registry, null, 2));
|
|
3512
|
+
else console.log(formatProjectList(registry));
|
|
3513
|
+
} catch (err) {
|
|
3514
|
+
console.error("\u274C project list failed:", err instanceof Error ? err.message : err);
|
|
3515
|
+
process.exit(1);
|
|
3516
|
+
}
|
|
3517
|
+
});
|
|
3518
|
+
projectCmd.command("show").argument("<slug>", "Project slug").description("Show one project from the pjangler registry").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((slug, options) => {
|
|
3519
|
+
try {
|
|
3520
|
+
const project = getProject(loadProjectRegistry(options.registry ?? projectRegistryPath()), slug);
|
|
3521
|
+
if (options.json) console.log(JSON.stringify(project, null, 2));
|
|
3522
|
+
else console.log(`${project.name} (${project.slug})
|
|
3523
|
+
${project.repo_path}
|
|
3524
|
+
${project.description}`);
|
|
3525
|
+
} catch (err) {
|
|
3526
|
+
console.error("\u274C project show failed:", err instanceof Error ? err.message : err);
|
|
3527
|
+
process.exit(1);
|
|
3528
|
+
}
|
|
3529
|
+
});
|
|
3530
|
+
projectCmd.command("doctor").argument("[slug]", "Optional project slug").description("Validate the project registry and local projections").option("--registry <path>", `Registry path override (default: ${projectRegistryPath()})`).option("--json", "Output machine-parseable JSON").action((slug, options) => {
|
|
3531
|
+
try {
|
|
3532
|
+
const report = doctorProjectRegistry(options.registry ?? projectRegistryPath(), slug);
|
|
3533
|
+
if (options.json) {
|
|
3534
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3535
|
+
} else if (!report.issues.length) {
|
|
3536
|
+
console.log(`Project registry OK: ${report.registryPath}`);
|
|
3537
|
+
} else {
|
|
3538
|
+
console.log(`Project registry issues: ${report.registryPath}`);
|
|
3539
|
+
for (const issue of report.issues) console.log(` [${issue.level}] ${issue.slug ?? "registry"}: ${issue.message}`);
|
|
3540
|
+
}
|
|
3541
|
+
process.exit(report.ok ? 0 : 1);
|
|
3542
|
+
} catch (err) {
|
|
3543
|
+
console.error("\u274C project doctor failed:", err instanceof Error ? err.message : err);
|
|
3544
|
+
process.exit(1);
|
|
3545
|
+
}
|
|
3546
|
+
});
|
|
2439
3547
|
var recipeCmd = program.command("recipe").description("Manage pjangler recipes");
|
|
2440
3548
|
recipeCmd.command("list").description("List all available recipes").action(() => {
|
|
2441
3549
|
console.log("\u{1F4E6} Available Recipes:");
|
|
@@ -2568,24 +3676,50 @@ program.command("audit").argument("[repo]", "Path to repo to audit (default: cwd
|
|
|
2568
3676
|
process.exit(1);
|
|
2569
3677
|
}
|
|
2570
3678
|
});
|
|
2571
|
-
program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit
|
|
3679
|
+
program.command("migrate").argument("[rule-id]", "Rule ID to migrate (omit to open interactive rule selector)").argument("[repo]", "Path to repo (default: cwd)").description("Idempotent migration recipe for a parity rule (or open the rule selector)").option("--all", "Apply every migration recipe in order").option("--dry-run", "Preview changes without writing files").option("--json", "Output machine-parseable JSON").action(async (ruleId, repo, options) => {
|
|
2572
3680
|
try {
|
|
2573
3681
|
const all = options.all ?? false;
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
3682
|
+
const dryRun = options.dryRun ?? false;
|
|
3683
|
+
if (all) {
|
|
3684
|
+
let actualRepo = repo;
|
|
3685
|
+
if (ruleId && !actualRepo) {
|
|
3686
|
+
actualRepo = ruleId;
|
|
3687
|
+
}
|
|
3688
|
+
const report2 = runMigration(void 0, actualRepo, dryRun, true);
|
|
3689
|
+
printMigrationReport(report2, options.json);
|
|
3690
|
+
process.exit(report2.ok ? 0 : 1);
|
|
3691
|
+
}
|
|
3692
|
+
if (ruleId && repo) {
|
|
3693
|
+
if (!getParityRuleIds().includes(ruleId)) {
|
|
3694
|
+
console.error(`\u274C Unknown parity rule: ${ruleId}`);
|
|
3695
|
+
process.exit(1);
|
|
3696
|
+
}
|
|
3697
|
+
const report2 = runMigration(ruleId, repo, dryRun, false);
|
|
3698
|
+
printMigrationReport(report2, options.json);
|
|
3699
|
+
process.exit(report2.ok ? 0 : 1);
|
|
2577
3700
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
3701
|
+
if (ruleId && getParityRuleIds().includes(ruleId)) {
|
|
3702
|
+
const report2 = runMigration(ruleId, void 0, dryRun, false);
|
|
3703
|
+
printMigrationReport(report2, options.json);
|
|
3704
|
+
process.exit(report2.ok ? 0 : 1);
|
|
2582
3705
|
}
|
|
2583
|
-
const report = runMigration(actualRuleId, actualRepo, options.dryRun ?? false, all);
|
|
2584
3706
|
if (options.json) {
|
|
2585
|
-
console.
|
|
2586
|
-
|
|
2587
|
-
|
|
3707
|
+
console.error("\u274C JSON output requires a rule-id or --all");
|
|
3708
|
+
process.exit(1);
|
|
3709
|
+
}
|
|
3710
|
+
if (!process.stdin.isTTY) {
|
|
3711
|
+
console.error("\u274C Provide a rule-id, use --all, or run in an interactive terminal");
|
|
3712
|
+
process.exit(1);
|
|
3713
|
+
}
|
|
3714
|
+
const targetRepo = ruleId ?? repo;
|
|
3715
|
+
const audit = runAudit(targetRepo);
|
|
3716
|
+
const ruleIds = await promptForRuleIds(audit.rules);
|
|
3717
|
+
if (!ruleIds.length) {
|
|
3718
|
+
console.log("No rules selected; nothing to migrate.");
|
|
3719
|
+
process.exit(0);
|
|
2588
3720
|
}
|
|
3721
|
+
const report = runMigrationForRules(ruleIds, targetRepo, dryRun);
|
|
3722
|
+
printMigrationReport(report, false);
|
|
2589
3723
|
process.exit(report.ok ? 0 : 1);
|
|
2590
3724
|
} catch (err) {
|
|
2591
3725
|
console.error("\u274C migrate failed:", err);
|