@delorenj/pjangler 1.3.0 → 1.4.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 +38 -5
- package/README.md +92 -0
- package/dist/assets/project-notebook-skill/SHA256SUMS +10 -0
- package/dist/assets/project-notebook-skill/SKILL.md +64 -0
- package/dist/assets/project-notebook-skill/agents/openai.yaml +6 -0
- package/dist/assets/project-notebook-skill/export-manifest.json +56 -0
- package/dist/assets/project-notebook-skill/hooks/claude.settings.json +26 -0
- package/dist/assets/project-notebook-skill/hooks/hooks.master.json +26 -0
- package/dist/assets/project-notebook-skill/hooks/session-end.sh +228 -0
- package/dist/assets/project-notebook-skill/hooks/session-start.sh +228 -0
- package/dist/assets/project-notebook-skill/references/configuration.md +93 -0
- package/dist/assets/project-notebook-skill/references/recovery.md +54 -0
- package/dist/assets/project-notebook-skill/scripts/project-hooks.py +865 -0
- package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +848 -0
- package/dist/index.js +11307 -2809
- package/dist/mcp-server.js +9637 -2094
- package/dist/prompt.js +404 -0
- package/package.json +8 -5
- package/templates/commonproject/copier.yml +19 -5
- package/templates/commonproject/template/.mise/scripts/link-agentfiles.sh +38 -5
- package/templates/commonproject/template/.mise/scripts/provision-packs.py +74 -52
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +479 -24
- package/templates/commonproject/template/mise.toml.jinja +12 -6
- package/templates/hermes-agent/copier.yml +8 -11
- package/templates/hermes-agent/template/.gitignore.jinja +1 -0
- package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +44 -0
- package/templates/hermes-agent/template/.scripts/01-config.sh +9 -0
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +18 -28
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +68 -4
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +77 -43
- package/templates/hermes-agent/template/.scripts/80-registry.sh +6 -0
- package/templates/hermes-agent/template/.scripts/_lib.sh +116 -16
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +29 -1
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +13 -1
- package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +202 -0
- package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +761 -0
- package/templates/hermes-agent/template/.scripts/lifecycle.sh +126 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +19 -3
- package/templates/hermes-agent/template/SOUL.md.jinja +44 -8
- package/templates/hermes-agent/template/hermes.jinja +20 -8
- package/templates/hermes-agent/template/momo.jinja +177 -0
- package/templates/hermes-agent/template/role.yaml.jinja +19 -19
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.4.2",
|
|
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 && node scripts/export-project-notebook-skill.mjs",
|
|
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,13 @@
|
|
|
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": "
|
|
35
|
+
"test:pjan-77": "node tests/pjan-77-notebook-domain-regressions.mjs && node tests/pjan-77-notebook-adapter-contract.mjs && node tests/pjan-77-notebook-lifecycle-regressions.mjs && node tests/pjan-77-notebook-cli-contract.mjs && node tests/pjan-77-notebook-hooks-capture.mjs && node tests/pjan-77-notebook-security-isolation.mjs && node tests/pjan-77-notebook-release-gates.mjs",
|
|
36
|
+
"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/pjan-84-global-scope-regressions.mjs && node tests/pjan-84-registry-flag-regressions.mjs && node tests/pjan-84-finding-scope-regressions.mjs && node tests/pjan-84-orphan-adoption-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 && npm run test:pjan-77",
|
|
35
37
|
"migrate:up": "node-pg-migrate --migrations-dir migrations up",
|
|
36
38
|
"migrate:down": "node-pg-migrate --migrations-dir migrations down",
|
|
37
39
|
"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"
|
|
40
|
+
"prepublishOnly": "npm run check:lock && npm run check:submodules -- --remote --recursive --archive --npm && npm run build && npm run check:tracked-secrets",
|
|
41
|
+
"test:hermes-profile-inheritance": "node tests/hermes-profile-inheritance-regressions.mjs"
|
|
39
42
|
},
|
|
40
43
|
"keywords": [
|
|
41
44
|
"cli",
|
|
@@ -114,13 +114,27 @@ _exclude:
|
|
|
114
114
|
# into the caller's shared per-user CLI configs. Scaffolding must never mutate
|
|
115
115
|
# global state as a side effect — the operator trusts/enters the repo themselves.
|
|
116
116
|
_tasks:
|
|
117
|
-
-
|
|
117
|
+
# PJAN-82: seed the base ignore only when there is nothing to lose. `cp` over
|
|
118
|
+
# an existing .gitignore silently discarded whatever the repo already had,
|
|
119
|
+
# every time the template was re-rendered into a live project.
|
|
120
|
+
- "[ -s .gitignore ] || cp ~/.config/git/ignore .gitignore 2>/dev/null || [ -s .gitignore ] || echo '# Add ignores here' > .gitignore"
|
|
118
121
|
# Ensure secrets + per-dev agent-hook/skill overrides are ignored (appended after the base ignore).
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
+
# Each CLI root's hand-owned config is durable project state; the skill
|
|
123
|
+
# projections inside it are not — bmad-method and skills:sync rewrite them
|
|
124
|
+
# on every run, so they are re-ignored after the un-ignore lines (last
|
|
125
|
+
# match wins). _bmad/_config/manifest.yaml pins the version that
|
|
126
|
+
# reproduces them exactly.
|
|
127
|
+
# PJAN-82: appended once (a re-render used to duplicate the whole block), and
|
|
128
|
+
# the skill patterns carry no trailing slash so a SYMLINKED projection is
|
|
129
|
+
# ignored too — .gemini/.copilot/.kimi-code get a symlink, not a directory.
|
|
130
|
+
- "grep -q '^# Generated CLI configurations are durable project state' .gitignore 2>/dev/null || 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# No trailing slash: some CLIs get a real directory here and some a symlink.\\n/.agents/skills\\n.claude/skills\\n.codex/skills\\n.gemini/skills\\n.copilot/skills\\n.opencode/skills\\n.kimi-code/skills\\n' >> .gitignore"
|
|
131
|
+
# PJAN-82: use the managed script, which refuses to replace a REAL CLAUDE.md.
|
|
132
|
+
# `ln -sf` cannot distinguish a stale link it owns from a hand-written file it
|
|
133
|
+
# must not touch, so a re-render into a live project destroyed the file and
|
|
134
|
+
# reported success.
|
|
135
|
+
- "./.mise/scripts/link-agentfiles.sh \"$(pwd)\""
|
|
122
136
|
# Materialize the declared Skillex packs' manifest + symlink topology
|
|
123
137
|
# without copying pack content or disturbing project-specific skills.
|
|
124
|
-
- "python3 .mise/scripts/provision-packs.py"
|
|
138
|
+
- "python3 .mise/scripts/provision-packs.py --root \"$(pwd)\""
|
|
125
139
|
# Stamp the repo's absolute path into .project.json (the SOT) post-render.
|
|
126
140
|
- "python3 -c \"import json,os,pathlib; p=pathlib.Path('.project.json'); d=json.loads(p.read_text()); d['repo_path']=os.getcwd(); p.write_text(json.dumps(d,indent=2)+chr(10))\""
|
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
+
set -eu
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
# Symlink the per-CLI agent instruction files at AGENTS.md.
|
|
5
|
+
#
|
|
6
|
+
# A mise ENTER hook runs with cwd set to the directory the user cd'd into, NOT
|
|
7
|
+
# to config_root — measured on mise 2026.8.10, and true even for a parent config
|
|
8
|
+
# when the entered directory is a nested child. `mise run <task>` does run at
|
|
9
|
+
# config_root, which is why the old cwd-relative version looked correct for
|
|
10
|
+
# years: only the enter-hook path was wrong. So take the root explicitly, and
|
|
11
|
+
# fall back to this script's own location — never to cwd.
|
|
12
|
+
root="${1:-${MISE_CONFIG_ROOT:-}}"
|
|
13
|
+
own_root=$(CDPATH= cd "$(dirname "$0")/../.." && pwd -P)
|
|
14
|
+
if [ -z "$root" ]; then
|
|
15
|
+
root="$own_root"
|
|
7
16
|
else
|
|
8
|
-
|
|
17
|
+
root=$(CDPATH= cd "$root" && pwd -P)
|
|
9
18
|
fi
|
|
19
|
+
if [ "$root" != "$own_root" ]; then
|
|
20
|
+
echo "link-agentfiles: refusing to act on $root; this script belongs to $own_root" >&2
|
|
21
|
+
echo "link-agentfiles: a nested repo must ship its own .mise/scripts copy" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
CDPATH= cd "$root"
|
|
26
|
+
if [ ! -f AGENTS.md ]; then
|
|
27
|
+
echo "No AGENTS.md in $root. Nothing to symlink."
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# `ln -sf` cannot tell a stale link it owns from a real file it must not touch:
|
|
32
|
+
# -f means "unlink whatever is there". A hand-written CLAUDE.md was silently
|
|
33
|
+
# replaced by a symlink, and the script printed a green checkmark while doing it.
|
|
34
|
+
for name in CLAUDE.md GEMINI.md; do
|
|
35
|
+
if [ -e "$name" ] && [ ! -L "$name" ]; then
|
|
36
|
+
echo "link-agentfiles: refusing to replace the real file $root/$name with a symlink" >&2
|
|
37
|
+
echo "link-agentfiles: move its content into AGENTS.md, then delete $name" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
ln -sfn AGENTS.md "$name"
|
|
41
|
+
done
|
|
42
|
+
echo "✅ AGENTS.md links verified in $root"
|