@delorenj/pjangler 1.2.33 → 1.3.7
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/README.md +72 -0
- package/dist/index.js +4754 -2564
- package/dist/mcp-server.js +4516 -2141
- package/dist/prompt.js +404 -0
- package/package.json +7 -5
- package/templates/commonproject/copier.yml +6 -1
- package/templates/commonproject/template/.agents/hooks/README.md +10 -10
- package/templates/commonproject/template/.agents/hooks/hooks.master.json +1 -1
- package/templates/commonproject/template/.agents/hooks/sync.py +4 -4
- package/templates/commonproject/template/.agents/local.example.json +1 -1
- package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +1 -1
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +14 -50
- package/templates/commonproject/template/mise.toml.jinja +11 -11
- package/templates/hermes-agent/copier.yml +34 -14
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +11 -10
- package/templates/hermes-agent/template/.scripts/01-config.sh +13 -4
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +73 -14
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +37 -32
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +18 -4
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +114 -24
- package/templates/hermes-agent/template/.scripts/80-registry.sh +19 -3
- package/templates/hermes-agent/template/.scripts/_lib.sh +74 -11
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
- package/templates/hermes-agent/template/.scripts/config.example.toml +7 -4
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +81 -0
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +15 -2
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +734 -0
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +31 -5
- package/templates/hermes-agent/template/.scripts/secret-scan.py +82 -16
- package/templates/hermes-agent/template/SOUL.md.jinja +48 -12
- package/templates/hermes-agent/template/hermes.jinja +51 -10
- package/templates/hermes-agent/template/momo.jinja +177 -0
- package/templates/hermes-agent/template/role.yaml.jinja +31 -21
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/prompt.ts
|
|
4
|
+
import { readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
5
|
+
import { basename, join as join3 } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
// src/describe/activity.ts
|
|
9
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10
|
+
import { statSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
|
|
13
|
+
var MAX_DIRTY_STATS = 500;
|
|
14
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
15
|
+
var GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
16
|
+
function git(repo, args) {
|
|
17
|
+
const result = spawnSync("git", ["-C", repo, ...args], {
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
timeout: GIT_TIMEOUT_MS,
|
|
20
|
+
maxBuffer: GIT_MAX_BUFFER
|
|
21
|
+
});
|
|
22
|
+
if (result.status !== 0 || typeof result.stdout !== "string") return void 0;
|
|
23
|
+
return result.stdout;
|
|
24
|
+
}
|
|
25
|
+
function trimmed(raw) {
|
|
26
|
+
if (raw === void 0) return void 0;
|
|
27
|
+
const value = raw.trim();
|
|
28
|
+
return value === "" ? void 0 : value;
|
|
29
|
+
}
|
|
30
|
+
function gitLine(repo, args) {
|
|
31
|
+
return trimmed(git(repo, args));
|
|
32
|
+
}
|
|
33
|
+
function isGitRepo(repo) {
|
|
34
|
+
return gitLine(repo, ["rev-parse", "--is-inside-work-tree"]) === "true";
|
|
35
|
+
}
|
|
36
|
+
var MINUTE = 60;
|
|
37
|
+
var HOUR = 60 * MINUTE;
|
|
38
|
+
var DAY = 24 * HOUR;
|
|
39
|
+
var WEEK = 7 * DAY;
|
|
40
|
+
var MONTH = 30 * DAY;
|
|
41
|
+
var YEAR = 365 * DAY;
|
|
42
|
+
function plural(count, unit) {
|
|
43
|
+
return `${count} ${unit}${count === 1 ? "" : "s"} ago`;
|
|
44
|
+
}
|
|
45
|
+
function formatRelativeAge(deltaSeconds) {
|
|
46
|
+
const delta = Math.max(0, Math.floor(deltaSeconds));
|
|
47
|
+
if (delta < MINUTE) return "just now";
|
|
48
|
+
if (delta < HOUR) return plural(Math.floor(delta / MINUTE), "minute");
|
|
49
|
+
if (delta < DAY) return plural(Math.floor(delta / HOUR), "hour");
|
|
50
|
+
if (delta < WEEK) return plural(Math.floor(delta / DAY), "day");
|
|
51
|
+
if (delta < MONTH) return plural(Math.floor(delta / WEEK), "week");
|
|
52
|
+
if (delta < YEAR) return plural(Math.floor(delta / MONTH), "month");
|
|
53
|
+
return plural(Math.floor(delta / YEAR), "year");
|
|
54
|
+
}
|
|
55
|
+
function formatCompactAge(deltaSeconds) {
|
|
56
|
+
const delta = Math.max(0, Math.floor(deltaSeconds));
|
|
57
|
+
if (delta < MINUTE) return "now";
|
|
58
|
+
if (delta < HOUR) return `${Math.floor(delta / MINUTE)}m`;
|
|
59
|
+
if (delta < DAY) return `${Math.floor(delta / HOUR)}h`;
|
|
60
|
+
if (delta < WEEK) return `${Math.floor(delta / DAY)}d`;
|
|
61
|
+
if (delta < MONTH) return `${Math.floor(delta / WEEK)}w`;
|
|
62
|
+
if (delta < YEAR) return `${Math.floor(delta / MONTH)}mo`;
|
|
63
|
+
return `${Math.floor(delta / YEAR)}y`;
|
|
64
|
+
}
|
|
65
|
+
var REF_ARGS = [
|
|
66
|
+
"for-each-ref",
|
|
67
|
+
"--sort=-committerdate",
|
|
68
|
+
"--format=%(committerdate:unix)%09%(refname:short)",
|
|
69
|
+
"refs/heads",
|
|
70
|
+
"refs/remotes",
|
|
71
|
+
"refs/tags"
|
|
72
|
+
];
|
|
73
|
+
var WORKTREE_ARGS = ["worktree", "list", "--porcelain"];
|
|
74
|
+
var STATUS_ARGS = ["status", "--porcelain", "-z", "--ignore-submodules=dirty"];
|
|
75
|
+
function parseRefs(raw) {
|
|
76
|
+
if (raw === void 0) return { count: 0 };
|
|
77
|
+
const lines = raw.split("\n").filter((line) => line.trim() !== "");
|
|
78
|
+
if (!lines.length) return { count: 0 };
|
|
79
|
+
const [stamp, name] = lines[0].split(" ");
|
|
80
|
+
const unix = Number(stamp);
|
|
81
|
+
if (!Number.isFinite(unix) || unix <= 0) return { count: lines.length };
|
|
82
|
+
return { source: { kind: "ref", label: name ?? "(unnamed ref)", unix }, count: lines.length };
|
|
83
|
+
}
|
|
84
|
+
function parseWorktrees(raw) {
|
|
85
|
+
if (raw === void 0) return [];
|
|
86
|
+
const entries = [];
|
|
87
|
+
let current = { detached: false };
|
|
88
|
+
const flush = () => {
|
|
89
|
+
if (current.path && current.sha) entries.push({ path: current.path, sha: current.sha, detached: current.detached });
|
|
90
|
+
current = { detached: false };
|
|
91
|
+
};
|
|
92
|
+
for (const line of raw.split("\n")) {
|
|
93
|
+
if (line.startsWith("worktree ")) {
|
|
94
|
+
flush();
|
|
95
|
+
current.path = line.slice("worktree ".length);
|
|
96
|
+
} else if (line.startsWith("HEAD ")) {
|
|
97
|
+
current.sha = line.slice("HEAD ".length).trim();
|
|
98
|
+
} else if (line === "detached") {
|
|
99
|
+
current.detached = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
flush();
|
|
103
|
+
return entries;
|
|
104
|
+
}
|
|
105
|
+
function parseWorktreeStamps(raw, entries) {
|
|
106
|
+
if (raw === void 0) return void 0;
|
|
107
|
+
let best;
|
|
108
|
+
for (const line of raw.split("\n")) {
|
|
109
|
+
const [stamp, sha] = line.trim().split(" ");
|
|
110
|
+
const unix = Number(stamp);
|
|
111
|
+
if (!Number.isFinite(unix) || unix <= 0 || !sha) continue;
|
|
112
|
+
if (best && unix <= best.unix) continue;
|
|
113
|
+
const owner = entries.find((entry) => entry.sha === sha);
|
|
114
|
+
const name = owner ? basenameOf(owner.path) : sha.slice(0, 7);
|
|
115
|
+
best = { kind: "worktree", label: owner?.detached ? `${name} (detached)` : name, unix };
|
|
116
|
+
}
|
|
117
|
+
return best;
|
|
118
|
+
}
|
|
119
|
+
function parseStatusPaths(raw) {
|
|
120
|
+
if (raw === void 0) return [];
|
|
121
|
+
const parts = raw.split("\0").filter((part) => part !== "");
|
|
122
|
+
const paths = [];
|
|
123
|
+
for (let index = 0; index < parts.length; index++) {
|
|
124
|
+
const entry = parts[index];
|
|
125
|
+
if (entry.length < 4 || entry[2] !== " ") continue;
|
|
126
|
+
paths.push(entry.slice(3));
|
|
127
|
+
if (entry[0] === "R" || entry[0] === "C") index += 1;
|
|
128
|
+
}
|
|
129
|
+
return paths;
|
|
130
|
+
}
|
|
131
|
+
function basenameOf(path) {
|
|
132
|
+
const parts = path.split("/").filter(Boolean);
|
|
133
|
+
return parts[parts.length - 1] ?? path;
|
|
134
|
+
}
|
|
135
|
+
function uncommittedSource(repo, paths) {
|
|
136
|
+
if (!paths.length) return void 0;
|
|
137
|
+
let newest = 0;
|
|
138
|
+
for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
|
|
139
|
+
try {
|
|
140
|
+
const mtime = Math.floor(statSync(join(repo, path)).mtimeMs / 1e3);
|
|
141
|
+
if (mtime > newest) newest = mtime;
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (newest <= 0) return void 0;
|
|
146
|
+
const label = paths.length === 1 ? "1 uncommitted file" : `${paths.length} uncommitted files`;
|
|
147
|
+
return { kind: "uncommitted", label, unix: newest };
|
|
148
|
+
}
|
|
149
|
+
var NO_ACTIVITY = {
|
|
150
|
+
updated: null,
|
|
151
|
+
updatedUnix: null,
|
|
152
|
+
relative: "never",
|
|
153
|
+
compact: "\u2014",
|
|
154
|
+
active: false,
|
|
155
|
+
source: null,
|
|
156
|
+
scanned: { refs: 0, worktrees: 0, dirtyFiles: 0 }
|
|
157
|
+
};
|
|
158
|
+
function emptyActivity() {
|
|
159
|
+
return { ...NO_ACTIVITY, scanned: { refs: 0, worktrees: 0, dirtyFiles: 0 } };
|
|
160
|
+
}
|
|
161
|
+
function assembleActivity(candidates, scanned, now) {
|
|
162
|
+
let winner = null;
|
|
163
|
+
for (const candidate of candidates) {
|
|
164
|
+
if (!candidate) continue;
|
|
165
|
+
if (!winner || candidate.unix >= winner.unix) winner = candidate;
|
|
166
|
+
}
|
|
167
|
+
if (!winner) return { ...NO_ACTIVITY, scanned };
|
|
168
|
+
const nowUnix = Math.floor((now?.getTime() ?? Date.now()) / 1e3);
|
|
169
|
+
const delta = nowUnix - winner.unix;
|
|
170
|
+
return {
|
|
171
|
+
updated: new Date(winner.unix * 1e3).toISOString(),
|
|
172
|
+
updatedUnix: winner.unix,
|
|
173
|
+
relative: formatRelativeAge(delta),
|
|
174
|
+
compact: formatCompactAge(delta),
|
|
175
|
+
active: delta < ACTIVE_WINDOW_SECONDS,
|
|
176
|
+
source: winner,
|
|
177
|
+
scanned
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function computeRepoActivity(repo, options = {}) {
|
|
181
|
+
if (!isGitRepo(repo)) return emptyActivity();
|
|
182
|
+
const refs = parseRefs(git(repo, REF_ARGS));
|
|
183
|
+
const worktrees = parseWorktrees(git(repo, WORKTREE_ARGS));
|
|
184
|
+
const shas = [...new Set(worktrees.map((entry) => entry.sha))];
|
|
185
|
+
const worktreeSource = shas.length ? parseWorktreeStamps(git(repo, ["show", "-s", "--format=%ct %H", ...shas]), worktrees) : void 0;
|
|
186
|
+
const paths = parseStatusPaths(git(repo, STATUS_ARGS));
|
|
187
|
+
return assembleActivity(
|
|
188
|
+
[refs.source, worktreeSource, uncommittedSource(repo, paths)],
|
|
189
|
+
{ refs: refs.count, worktrees: worktrees.length, dirtyFiles: paths.length },
|
|
190
|
+
options.now
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/project/boardUrl.ts
|
|
195
|
+
import { existsSync, readFileSync, statSync as statSync2 } from "node:fs";
|
|
196
|
+
import { homedir } from "node:os";
|
|
197
|
+
import { dirname, isAbsolute, join as join2, resolve } from "node:path";
|
|
198
|
+
var DEFAULT_PLANE_BASE = "https://plane.delo.sh";
|
|
199
|
+
var DEFAULT_PLANE_WORKSPACE = "33god";
|
|
200
|
+
function resolveTemplateConfigPath(env = process.env, home = homedir()) {
|
|
201
|
+
const fromEnv = env.HERMES_TEMPLATE_CONFIG;
|
|
202
|
+
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
203
|
+
const xdg = env.XDG_CONFIG_HOME?.trim();
|
|
204
|
+
const base = xdg && xdg.length ? xdg : join2(home, ".config");
|
|
205
|
+
return join2(base, "hermes-agent-template", "config.toml");
|
|
206
|
+
}
|
|
207
|
+
function readTomlScalar(text, section, key) {
|
|
208
|
+
let inSection = false;
|
|
209
|
+
for (const raw of text.split("\n")) {
|
|
210
|
+
const line = raw.trim();
|
|
211
|
+
if (!line || line.startsWith("#")) continue;
|
|
212
|
+
if (line.startsWith("[")) {
|
|
213
|
+
inSection = line === `[${section}]`;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!inSection) continue;
|
|
217
|
+
const eq = line.indexOf("=");
|
|
218
|
+
if (eq === -1) continue;
|
|
219
|
+
if (line.slice(0, eq).trim() !== key) continue;
|
|
220
|
+
const value = line.slice(eq + 1).trim();
|
|
221
|
+
const quoted = /^"([^"]*)"|^'([^']*)'/.exec(value);
|
|
222
|
+
if (quoted) return quoted[1] ?? quoted[2];
|
|
223
|
+
const bare = (value.split("#")[0] ?? "").trim();
|
|
224
|
+
return bare || void 0;
|
|
225
|
+
}
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
function readTemplateConfig(env, home) {
|
|
229
|
+
try {
|
|
230
|
+
const path = resolveTemplateConfigPath(env, home);
|
|
231
|
+
return existsSync(path) ? readFileSync(path, "utf8") : void 0;
|
|
232
|
+
} catch {
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function planeBase(env = process.env, home = homedir()) {
|
|
237
|
+
const fromEnv = env.PLANE_BASE?.trim();
|
|
238
|
+
if (fromEnv) return fromEnv.replace(/\/+$/, "");
|
|
239
|
+
const config = readTemplateConfig(env, home);
|
|
240
|
+
const fromConfig = config ? readTomlScalar(config, "plane", "base")?.trim() : void 0;
|
|
241
|
+
if (fromConfig) return fromConfig.replace(/\/+$/, "");
|
|
242
|
+
return DEFAULT_PLANE_BASE;
|
|
243
|
+
}
|
|
244
|
+
function planeWorkspace(provider, env, home) {
|
|
245
|
+
const fromManifest = provider.workspace?.trim();
|
|
246
|
+
if (fromManifest) return fromManifest;
|
|
247
|
+
const config = readTemplateConfig(env, home);
|
|
248
|
+
const fromConfig = config ? readTomlScalar(config, "plane", "workspace")?.trim() : void 0;
|
|
249
|
+
return fromConfig || DEFAULT_PLANE_WORKSPACE;
|
|
250
|
+
}
|
|
251
|
+
function escapeRegExp(value) {
|
|
252
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
253
|
+
}
|
|
254
|
+
function extractTicketRef(branch, identifier) {
|
|
255
|
+
if (!branch || !identifier) return void 0;
|
|
256
|
+
const ident = identifier.trim();
|
|
257
|
+
if (!ident) return void 0;
|
|
258
|
+
const match = new RegExp(`\\b${escapeRegExp(ident)}-(\\d+)\\b`, "i").exec(branch);
|
|
259
|
+
return match ? `${ident.toUpperCase()}-${match[1]}` : void 0;
|
|
260
|
+
}
|
|
261
|
+
function normalizeTicketRef(input, identifier) {
|
|
262
|
+
const value = input?.trim();
|
|
263
|
+
if (!value) return void 0;
|
|
264
|
+
if (/^\d+$/.test(value)) {
|
|
265
|
+
const ident = identifier?.trim();
|
|
266
|
+
return ident ? `${ident.toUpperCase()}-${value}` : void 0;
|
|
267
|
+
}
|
|
268
|
+
const qualified = /^([A-Za-z][A-Za-z0-9]*)-(\d+)$/.exec(value);
|
|
269
|
+
if (!qualified) return void 0;
|
|
270
|
+
return `${qualified[1].toUpperCase()}-${qualified[2]}`;
|
|
271
|
+
}
|
|
272
|
+
function resolveTicketRef(provider, options) {
|
|
273
|
+
return normalizeTicketRef(options.ref, provider.identifier) ?? extractTicketRef(options.branch, provider.identifier);
|
|
274
|
+
}
|
|
275
|
+
function boardUrl(provider, options = {}) {
|
|
276
|
+
if (!provider) return void 0;
|
|
277
|
+
const env = options.env ?? process.env;
|
|
278
|
+
const home = options.home ?? homedir();
|
|
279
|
+
const type = (provider.type || "plane").trim().toLowerCase();
|
|
280
|
+
const boardId = provider.board_id?.trim();
|
|
281
|
+
if (!boardId) return void 0;
|
|
282
|
+
if (type === "trello") {
|
|
283
|
+
return `https://trello.com/b/${boardId}`;
|
|
284
|
+
}
|
|
285
|
+
if (type !== "plane") return void 0;
|
|
286
|
+
const workspace = planeWorkspace(provider, env, home);
|
|
287
|
+
if (!workspace) return void 0;
|
|
288
|
+
const base = planeBase(env, home);
|
|
289
|
+
const ref = resolveTicketRef(provider, options);
|
|
290
|
+
return ref ? `${base}/${workspace}/browse/${ref}` : `${base}/${workspace}/projects/${boardId}/issues`;
|
|
291
|
+
}
|
|
292
|
+
function findProjectRoot(from) {
|
|
293
|
+
let dir = resolve(from);
|
|
294
|
+
for (; ; ) {
|
|
295
|
+
if (existsSync(join2(dir, ".project.json"))) return dir;
|
|
296
|
+
const parent = dirname(dir);
|
|
297
|
+
if (parent === dir) return void 0;
|
|
298
|
+
dir = parent;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function readTicketProvider(root) {
|
|
302
|
+
try {
|
|
303
|
+
const manifest = JSON.parse(readFileSync(join2(root, ".project.json"), "utf8"));
|
|
304
|
+
const provider = manifest.ticket_provider;
|
|
305
|
+
if (!provider || typeof provider !== "object") return void 0;
|
|
306
|
+
return provider;
|
|
307
|
+
} catch {
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function currentBranch(from) {
|
|
312
|
+
try {
|
|
313
|
+
let dir = resolve(from);
|
|
314
|
+
for (; ; ) {
|
|
315
|
+
const dotgit = join2(dir, ".git");
|
|
316
|
+
if (existsSync(dotgit)) {
|
|
317
|
+
let gitDir = dotgit;
|
|
318
|
+
if (statSync2(dotgit).isFile()) {
|
|
319
|
+
const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotgit, "utf8"));
|
|
320
|
+
if (!pointer) return void 0;
|
|
321
|
+
const target = pointer[1].trim();
|
|
322
|
+
gitDir = isAbsolute(target) ? target : resolve(dir, target);
|
|
323
|
+
}
|
|
324
|
+
const head = readFileSync(join2(gitDir, "HEAD"), "utf8").trim();
|
|
325
|
+
const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
|
|
326
|
+
return ref ? ref[1].trim() : void 0;
|
|
327
|
+
}
|
|
328
|
+
const parent = dirname(dir);
|
|
329
|
+
if (parent === dir) return void 0;
|
|
330
|
+
dir = parent;
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
return void 0;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function resolveBoardUrl(cwd, ref, env = process.env) {
|
|
337
|
+
const root = findProjectRoot(cwd);
|
|
338
|
+
if (!root) return void 0;
|
|
339
|
+
const provider = readTicketProvider(root);
|
|
340
|
+
if (!provider) return void 0;
|
|
341
|
+
return boardUrl(provider, { ref, branch: currentBranch(root), env });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/prompt.ts
|
|
345
|
+
function readPromptFacts(root, now) {
|
|
346
|
+
let slug = basename(root);
|
|
347
|
+
let identifier;
|
|
348
|
+
try {
|
|
349
|
+
const manifest = JSON.parse(readFileSync2(join3(root, ".project.json"), "utf8"));
|
|
350
|
+
if (typeof manifest.project_slug === "string" && manifest.project_slug) slug = manifest.project_slug;
|
|
351
|
+
const provider = manifest.ticket_provider;
|
|
352
|
+
if (provider && typeof provider.identifier === "string" && provider.identifier) identifier = provider.identifier;
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
const activity = computeRepoActivity(root, { now });
|
|
356
|
+
return {
|
|
357
|
+
root,
|
|
358
|
+
slug,
|
|
359
|
+
identifier,
|
|
360
|
+
age: activity.updatedUnix ? activity.compact : void 0,
|
|
361
|
+
active: activity.active
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function formatPromptLine(facts) {
|
|
365
|
+
const parts = [facts.slug];
|
|
366
|
+
if (facts.identifier) parts.push(`(${facts.identifier})`);
|
|
367
|
+
const head = parts.join(" ");
|
|
368
|
+
return facts.age ? `${head} \xB7 ${facts.age}` : head;
|
|
369
|
+
}
|
|
370
|
+
function promptLine(cwd, now) {
|
|
371
|
+
const root = findProjectRoot(cwd);
|
|
372
|
+
if (!root) return void 0;
|
|
373
|
+
return formatPromptLine(readPromptFacts(root, now));
|
|
374
|
+
}
|
|
375
|
+
function main() {
|
|
376
|
+
try {
|
|
377
|
+
const args = process.argv.slice(2);
|
|
378
|
+
if (args[0] === "--url") {
|
|
379
|
+
const url = resolveBoardUrl(process.cwd(), args[1]);
|
|
380
|
+
if (url) process.stdout.write(`${url}
|
|
381
|
+
`);
|
|
382
|
+
else process.exitCode = 1;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const line = promptLine(process.cwd());
|
|
386
|
+
if (line) process.stdout.write(line);
|
|
387
|
+
} catch {
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function isMainModule() {
|
|
391
|
+
if (!process.argv[1]) return false;
|
|
392
|
+
try {
|
|
393
|
+
return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
394
|
+
} catch {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (isMainModule()) main();
|
|
399
|
+
export {
|
|
400
|
+
findProjectRoot,
|
|
401
|
+
formatPromptLine,
|
|
402
|
+
promptLine,
|
|
403
|
+
readPromptFacts
|
|
404
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delorenj/pjangler",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.7",
|
|
4
4
|
"description": "Project subsystem bootstrapper CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"pjangler": "dist/index.js",
|
|
9
9
|
"pj": "dist/index.js",
|
|
10
|
-
"pjangler-mcp": "dist/mcp-server.js"
|
|
10
|
+
"pjangler-mcp": "dist/mcp-server.js",
|
|
11
|
+
"pjangler-prompt": "dist/prompt.js"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"dist",
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
"node": ">=20"
|
|
23
24
|
},
|
|
24
25
|
"scripts": {
|
|
25
|
-
"build": "esbuild src/index.ts src/mcp-server.ts --bundle --packages=external --platform=node --format=esm --outdir=dist",
|
|
26
|
+
"build": "esbuild src/index.ts src/mcp-server.ts src/prompt.ts --bundle --packages=external --platform=node --format=esm --outdir=dist",
|
|
26
27
|
"check:audit:prod": "npm audit --omit=dev",
|
|
27
28
|
"check:lock": "node scripts/check-package-lock-parity.mjs",
|
|
28
29
|
"check:submodules": "node scripts/check-submodule-contract.mjs",
|
|
@@ -31,11 +32,12 @@
|
|
|
31
32
|
"mcp": "node dist/mcp-server.js",
|
|
32
33
|
"typecheck": "tsc --noEmit",
|
|
33
34
|
"test:bmad-installer-contract": "node tests/bmad-installer-contract-regressions.mjs",
|
|
34
|
-
"test": "npm run check:lock && npm run check:submodules && npm run check:tracked-secrets && node tests/portable-test-paths-regressions.mjs && node tests/release-regressions.mjs && node tests/submodule-contract-regressions.mjs && node tests/secret-publication-gate-regressions.mjs && node tests/bmad-version-surface-regressions.mjs && node tests/bmad-transaction-regressions.mjs && node tests/parity-migrate-regressions.mjs && node tests/pjan-57-lifecycle-recipes-regressions.mjs && node tests/pjan-57-dogfood-regressions.mjs && node tests/generated-project-lifecycle-regressions.mjs && node tests/pack-flatten-regressions.mjs && node tests/pack-flatten-cross-engine-regressions.mjs && node tests/registry-cache-parity-regressions.mjs && node tests/registry-root-ladder-regressions.mjs && node tests/pjan-23-regressions.mjs && node tests/pjan-24-regressions.mjs && node tests/pjan-28-regressions.mjs && node tests/pjan-30-regressions.mjs && node tests/pjan-31a-regressions.mjs && node tests/pjan-36-regressions.mjs && node tests/pjan-43-regressions.mjs && node tests/pjan-48-regressions.mjs && node tests/pjan-49-regressions.mjs && node tests/pjan-50-regressions.mjs && node tests/skillex-init-regressions.mjs && node tests/fleet-shared-bloodbank-regressions.mjs && node tests/mcp-catalog-regressions.mjs && node tests/mcp-server-regressions.mjs && node tests/project-registry-regressions.mjs && node tests/pg-registry-regressions.mjs && node tests/momo-lifecycle-plane-regressions.mjs",
|
|
35
|
+
"test": "npm run check:lock && node tests/package-lock-parity-regressions.mjs && npm run check:submodules && npm run check:tracked-secrets && node tests/portable-test-paths-regressions.mjs && node tests/release-regressions.mjs && node tests/submodule-contract-regressions.mjs && node tests/secret-publication-gate-regressions.mjs && node tests/bmad-version-surface-regressions.mjs && node tests/bmad-transaction-regressions.mjs && node tests/parity-migrate-regressions.mjs && node tests/hermes-profile-inheritance-regressions.mjs && node tests/pjan-57-lifecycle-recipes-regressions.mjs && node tests/pjan-57-dogfood-regressions.mjs && node tests/generated-project-lifecycle-regressions.mjs && node tests/pack-flatten-regressions.mjs && node tests/pack-flatten-cross-engine-regressions.mjs && node tests/registry-cache-parity-regressions.mjs && node tests/registry-root-ladder-regressions.mjs && node tests/pjan-23-regressions.mjs && node tests/pjan-24-regressions.mjs && node tests/pjan-28-regressions.mjs && node tests/pjan-30-regressions.mjs && node tests/pjan-31a-regressions.mjs && node tests/pjan-36-regressions.mjs && node tests/pjan-43-regressions.mjs && node tests/pjan-48-regressions.mjs && node tests/pjan-49-regressions.mjs && node tests/pjan-50-regressions.mjs && node tests/pjan-65-regressions.mjs && node tests/pjan-67-lifecycle-preflight-regressions.mjs && node tests/pjan-67-regressions.mjs && node tests/pjan-67-trusted-lifecycle-regressions.mjs && node tests/pjan-71-regressions.mjs && node tests/pjan-72-regressions.mjs && node tests/pjan-75-regressions.mjs && node tests/pjan-76-regressions.mjs && node tests/skillex-init-regressions.mjs && node tests/fleet-shared-bloodbank-regressions.mjs && node tests/mcp-catalog-regressions.mjs && node tests/mcp-server-regressions.mjs && node tests/project-registry-regressions.mjs && node tests/pg-registry-regressions.mjs && node tests/momo-lifecycle-plane-regressions.mjs",
|
|
35
36
|
"migrate:up": "node-pg-migrate --migrations-dir migrations up",
|
|
36
37
|
"migrate:down": "node-pg-migrate --migrations-dir migrations down",
|
|
37
38
|
"migrate:create": "node-pg-migrate --migrations-dir migrations create",
|
|
38
|
-
"prepublishOnly": "npm run check:lock && npm run check:submodules -- --remote --recursive --archive --npm && npm run build && npm run check:tracked-secrets"
|
|
39
|
+
"prepublishOnly": "npm run check:lock && npm run check:submodules -- --remote --recursive --archive --npm && npm run build && npm run check:tracked-secrets",
|
|
40
|
+
"test:hermes-profile-inheritance": "node tests/hermes-profile-inheritance-regressions.mjs"
|
|
39
41
|
},
|
|
40
42
|
"keywords": [
|
|
41
43
|
"cli",
|
|
@@ -116,7 +116,12 @@ _exclude:
|
|
|
116
116
|
_tasks:
|
|
117
117
|
- "cp ~/.config/git/ignore .gitignore 2>/dev/null || echo '# Add ignores here' > .gitignore"
|
|
118
118
|
# Ensure secrets + per-dev agent-hook/skill overrides are ignored (appended after the base ignore).
|
|
119
|
-
|
|
119
|
+
# Each CLI root's hand-owned config is durable project state; the skill
|
|
120
|
+
# projections inside it are not — bmad-method and skills:sync rewrite them
|
|
121
|
+
# on every run, so they are re-ignored after the un-ignore lines (last
|
|
122
|
+
# match wins). _bmad/_config/manifest.yaml pins the version that
|
|
123
|
+
# reproduces them exactly.
|
|
124
|
+
- "printf '\\n# Secrets + per-dev agent-hook/skill overrides (see .agents/local.example.json)\\n.env\\n.env.*\\n!.env.op\\n.agents/local.json\\n.lastagent\\n**/.claude/settings.local.json\\n\\n# Generated CLI configurations are durable project state...\\n!.claude/\\n!.claude/**\\n!.codex/\\n!.codex/**\\n!.gemini/\\n!.gemini/**\\n!.copilot/\\n!.copilot/**\\n!.opencode/\\n!.opencode/**\\n!.kimi-code/\\n!.kimi-code/**\\n# ...but their skill projections are regenerated, so they stay out of the tree.\\n/.agents/skills/\\n.claude/skills/\\n.codex/skills/\\n.gemini/skills/\\n.copilot/skills/\\n.opencode/skills/\\n.kimi-code/skills/\\n' >> .gitignore"
|
|
120
125
|
- "ln -sf AGENTS.md CLAUDE.md"
|
|
121
126
|
- "ln -sf AGENTS.md GEMINI.md"
|
|
122
127
|
# Materialize the declared Skillex packs' manifest + symlink topology
|
|
@@ -12,11 +12,11 @@ master writes zero bytes).
|
|
|
12
12
|
## TL;DR
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
mise run hooks
|
|
16
|
-
mise run hooks
|
|
17
|
-
mise run hooks
|
|
18
|
-
mise run skills
|
|
19
|
-
mise run hindsight
|
|
15
|
+
mise run hooks:sync # fan out the master -> claude + codex + hermes
|
|
16
|
+
mise run hooks:check # drift gate (read-only; used in CI)
|
|
17
|
+
mise run hooks:uninstall # remove codex + hermes injections
|
|
18
|
+
mise run skills:sync # sync .agents/skills.json -> local CLI skill dirs
|
|
19
|
+
mise run hindsight:setup # one-time: pull the shared CAF Hindsight key into .env
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
You normally run nothing — `mise` does it on directory enter/leave (see
|
|
@@ -93,10 +93,10 @@ merged in automatically.
|
|
|
93
93
|
|
|
94
94
|
- Project-local skills with the same name shadow the global ones.
|
|
95
95
|
- No `defer_to_global` flag or bash cleanup scripts are needed anymore.
|
|
96
|
-
- The manifest is the only hand-edited skill SSOT; run `mise run skills
|
|
96
|
+
- The manifest is the only hand-edited skill SSOT; run `mise run skills:sync`
|
|
97
97
|
after changing it, or just re-enter the repo.
|
|
98
98
|
|
|
99
|
-
## Hindsight credentials — `mise run hindsight
|
|
99
|
+
## Hindsight credentials — `mise run hindsight:setup`
|
|
100
100
|
|
|
101
101
|
The hooks shell out to `hindsight`, which needs an API key for the self-hosted
|
|
102
102
|
instance. Strategy: **one shared CAF-scoped key in 1Password**. `hindsight-setup`
|
|
@@ -105,7 +105,7 @@ gitignored `.env` (mise loads it, env vars outrank `~/.hindsight/config`). Point
|
|
|
105
105
|
it at the right item if the default ref is wrong:
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight
|
|
108
|
+
HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight:setup
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
Without a key the hooks **no-op gracefully** — recall/retain just do nothing.
|
|
@@ -113,10 +113,10 @@ Without a key the hooks **no-op gracefully** — recall/retain just do nothing.
|
|
|
113
113
|
## Adding / changing a hook
|
|
114
114
|
|
|
115
115
|
1. Edit `hooks.master.json`.
|
|
116
|
-
2. `mise run hooks
|
|
116
|
+
2. `mise run hooks:sync`.
|
|
117
117
|
3. Commit the master **and** the regenerated `.claude/settings.json` together
|
|
118
118
|
(codex/hermes targets are per-dev/per-deployment, not committed).
|
|
119
|
-
4. CI runs `mise run hooks
|
|
119
|
+
4. CI runs `mise run hooks:check` — fails if the committed Claude settings drift.
|
|
120
120
|
|
|
121
121
|
## Files
|
|
122
122
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "SINGLE SOURCE OF TRUTH for this repo's project-scoped agent hooks. Hand-edit ONLY this file, then run `mise run hooks
|
|
2
|
+
"$comment": "SINGLE SOURCE OF TRUTH for this repo's project-scoped agent hooks. Hand-edit ONLY this file, then run `mise run hooks:sync` (or re-cd into the repo) to regenerate every per-agent config. Generated files are NOT hand-edited. Pattern: the /ssot-fanout skill. See .agents/hooks/README.md.",
|
|
3
3
|
"version": 1,
|
|
4
4
|
"marker": "project-agent-hooks",
|
|
5
5
|
"hooks": [
|
|
@@ -515,7 +515,7 @@ def cmd_check(master: dict) -> int:
|
|
|
515
515
|
current = target.read_text() if target.exists() else None
|
|
516
516
|
if current != desired:
|
|
517
517
|
warn(f"DRIFT: {target.relative_to(REPO_ROOT)} differs from hooks.master.json "
|
|
518
|
-
f"(run `mise run hooks
|
|
518
|
+
f"(run `mise run hooks:sync`)")
|
|
519
519
|
rc = 1
|
|
520
520
|
else:
|
|
521
521
|
log(f"claude: in sync ({target.relative_to(REPO_ROOT)})")
|
|
@@ -538,7 +538,7 @@ def cmd_check(master: dict) -> int:
|
|
|
538
538
|
]
|
|
539
539
|
if missing:
|
|
540
540
|
warn(f"DRIFT: codex hooks.json missing {len(missing)} project hook(s) "
|
|
541
|
-
f"(run `mise run hooks
|
|
541
|
+
f"(run `mise run hooks:sync`)")
|
|
542
542
|
rc = 1
|
|
543
543
|
else:
|
|
544
544
|
log(f"codex: in sync ({ct})")
|
|
@@ -558,7 +558,7 @@ def cmd_check(master: dict) -> int:
|
|
|
558
558
|
missing = [c for c in want_cmds if c not in present]
|
|
559
559
|
if begin not in present or missing:
|
|
560
560
|
warn(f"DRIFT: kimi config.toml missing the project hooks block "
|
|
561
|
-
f"(run `mise run hooks
|
|
561
|
+
f"(run `mise run hooks:sync`)")
|
|
562
562
|
rc = 1
|
|
563
563
|
else:
|
|
564
564
|
log(f"kimi: in sync ({kt})")
|
|
@@ -574,7 +574,7 @@ def cmd_check(master: dict) -> int:
|
|
|
574
574
|
missing = [c for _ev, c, _t in hermes_commands(master) if c not in present]
|
|
575
575
|
if missing:
|
|
576
576
|
warn(f"DRIFT: hermes config.yaml missing {len(missing)} adapter hook(s) "
|
|
577
|
-
f"(run `mise run hooks
|
|
577
|
+
f"(run `mise run hooks:sync`)")
|
|
578
578
|
rc = 1
|
|
579
579
|
else:
|
|
580
580
|
log(f"hermes: in sync ({hcfg.relative_to(REPO_ROOT)})")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "PER-DEV LOCAL OVERRIDES — copy this file to .agents/local.json (gitignored) and edit. Lets you opt out of individual hooks or whole injected agents without touching committed config. Hooks self-skip at RUNTIME via .agents/hooks/lib/hook-guard.sh (so even Claude's committed hooks honor it); codex/hermes injections additionally skip at INSTALL time. Re-run `mise run hooks
|
|
2
|
+
"$comment": "PER-DEV LOCAL OVERRIDES — copy this file to .agents/local.json (gitignored) and edit. Lets you opt out of individual hooks or whole injected agents without touching committed config. Hooks self-skip at RUNTIME via .agents/hooks/lib/hook-guard.sh (so even Claude's committed hooks honor it); codex/hermes injections additionally skip at INSTALL time. Re-run `mise run hooks:sync` (or re-cd into the repo) after editing.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"$disabled_help": "Hook ids from .agents/hooks/hooks.master.json: skill-check-reminder, hindsight-recall, hindsight-retain, hindsight-session-end. Listed ids are skipped at runtime across ALL agents.",
|
|
5
5
|
"disabled": [],
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# .env untouched. Never prints the secret.
|
|
12
12
|
#
|
|
13
13
|
# Override the 1Password reference if the item path differs:
|
|
14
|
-
# HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight
|
|
14
|
+
# HINDSIGHT_OP_KEY_REF="op://DeLoSecrets/<item>/<field>" mise run hindsight:setup
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
|
|
17
17
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|