@starterculture/devkeep-actions 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/README.md +16 -0
- package/dist/consolidate-release.js +450 -0
- package/dist/crypting-candidates-scan.js +371 -0
- package/dist/draft-log-entry.js +718 -0
- package/package.json +31 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../../modules/core/dist/github/client.js
|
|
4
|
+
import { Octokit } from "@octokit/rest";
|
|
5
|
+
function createGithubClient(token) {
|
|
6
|
+
if (!token) {
|
|
7
|
+
throw new Error("GitHub token is required");
|
|
8
|
+
}
|
|
9
|
+
return new Octokit({ auth: token });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ../../modules/core/dist/github/retryOnConflict.js
|
|
13
|
+
async function retryOnConflict(attempt, maxAttempts = 3) {
|
|
14
|
+
for (let tryNumber = 1; ; tryNumber++) {
|
|
15
|
+
try {
|
|
16
|
+
return await attempt();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const status = error.status;
|
|
19
|
+
if (tryNumber >= maxAttempts || status !== 409 && status !== 422) {
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, 300 * tryNumber));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ../../modules/core/dist/tokenUsage.js
|
|
28
|
+
var ZERO_USAGE = { inputTokens: 0, outputTokens: 0 };
|
|
29
|
+
function addTokenUsage(a, b) {
|
|
30
|
+
return { inputTokens: a.inputTokens + b.inputTokens, outputTokens: a.outputTokens + b.outputTokens };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ../../modules/core/dist/rollover.js
|
|
34
|
+
var ZERO_ROLLOVER_USAGE = {
|
|
35
|
+
today: { key: "", usage: ZERO_USAGE },
|
|
36
|
+
month: { key: "", usage: ZERO_USAGE }
|
|
37
|
+
};
|
|
38
|
+
function todayKey(now = /* @__PURE__ */ new Date()) {
|
|
39
|
+
const year = now.getFullYear();
|
|
40
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
41
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
42
|
+
return `${year}-${month}-${day}`;
|
|
43
|
+
}
|
|
44
|
+
function monthKey(now = /* @__PURE__ */ new Date()) {
|
|
45
|
+
return todayKey(now).slice(0, 7);
|
|
46
|
+
}
|
|
47
|
+
function addRolloverUsage(current, delta, now = /* @__PURE__ */ new Date()) {
|
|
48
|
+
const nowTodayKey = todayKey(now);
|
|
49
|
+
const nowMonthKey = monthKey(now);
|
|
50
|
+
return {
|
|
51
|
+
today: {
|
|
52
|
+
key: nowTodayKey,
|
|
53
|
+
usage: addTokenUsage(current.today.key === nowTodayKey ? current.today.usage : ZERO_USAGE, delta)
|
|
54
|
+
},
|
|
55
|
+
month: {
|
|
56
|
+
key: nowMonthKey,
|
|
57
|
+
usage: addTokenUsage(current.month.key === nowMonthKey ? current.month.usage : ZERO_USAGE, delta)
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ../../modules/core/dist/claude/client.js
|
|
63
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
64
|
+
function createClaudeClient(apiKey) {
|
|
65
|
+
const key = apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
66
|
+
if (!key) {
|
|
67
|
+
throw new Error("ANTHROPIC_API_KEY is not set");
|
|
68
|
+
}
|
|
69
|
+
return new Anthropic({ apiKey: key });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ../../modules/core/dist/claude/complete.js
|
|
73
|
+
var DEFAULT_MAX_CONTINUATIONS = 5;
|
|
74
|
+
var CONTINUATION_USER_MESSAGE = "Continue exactly where you left off. Do not repeat any text already written, and do not add any preamble or commentary \u2014 resume the previous response verbatim from the exact cutoff point.";
|
|
75
|
+
async function createCompleteMessage(claude, params, { maxContinuations = DEFAULT_MAX_CONTINUATIONS, timeoutMs, signal } = {}) {
|
|
76
|
+
let fullText = "";
|
|
77
|
+
let usage = ZERO_USAGE;
|
|
78
|
+
const requestOptions = timeoutMs !== void 0 || signal !== void 0 ? { timeout: timeoutMs, signal } : void 0;
|
|
79
|
+
for (let attempt = 0; attempt <= maxContinuations; attempt++) {
|
|
80
|
+
const messages = fullText ? [
|
|
81
|
+
...params.messages,
|
|
82
|
+
{ role: "assistant", content: fullText },
|
|
83
|
+
{ role: "user", content: CONTINUATION_USER_MESSAGE }
|
|
84
|
+
] : params.messages;
|
|
85
|
+
const message = requestOptions ? await claude.messages.create({ ...params, messages }, requestOptions) : await claude.messages.create({ ...params, messages });
|
|
86
|
+
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
87
|
+
fullText += text;
|
|
88
|
+
usage = addTokenUsage(usage, {
|
|
89
|
+
inputTokens: message.usage?.input_tokens ?? 0,
|
|
90
|
+
outputTokens: message.usage?.output_tokens ?? 0
|
|
91
|
+
});
|
|
92
|
+
if (message.stop_reason !== "max_tokens") {
|
|
93
|
+
return { text: fullText, usage };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Claude response still hit max_tokens after ${maxContinuations} continuation attempts \u2014 refusing to return truncated content.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ../../modules/core/dist/modelRegistry.js
|
|
100
|
+
var MODEL_REGISTRY = {
|
|
101
|
+
fast: {
|
|
102
|
+
description: "Quick, cheap work where depth matters least.",
|
|
103
|
+
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5" }]
|
|
104
|
+
},
|
|
105
|
+
balanced: {
|
|
106
|
+
description: "The everyday tier: chat, log entries, release notes, the daily digest. Frequent enough that cost matters.",
|
|
107
|
+
models: [{ id: "claude-sonnet-5", label: "Sonnet 5" }]
|
|
108
|
+
},
|
|
109
|
+
capable: {
|
|
110
|
+
description: "Deeper reasoning for infrequent, high-value work where getting it right outweighs the cost.",
|
|
111
|
+
models: [{ id: "claude-opus-5", label: "Opus 5" }]
|
|
112
|
+
},
|
|
113
|
+
max: {
|
|
114
|
+
description: "Maximum depth, for long-running agentic work. Opt-in \u2014 the most expensive tier.",
|
|
115
|
+
models: [{ id: "claude-fable-5", label: "Fable 5" }]
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function defaultModelFor(tier) {
|
|
119
|
+
const [current] = MODEL_REGISTRY[tier].models;
|
|
120
|
+
if (!current) {
|
|
121
|
+
throw new Error(`Model tier "${tier}" has no models registered`);
|
|
122
|
+
}
|
|
123
|
+
return current.id;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ../../modules/devlore/dist/ciUsage.js
|
|
127
|
+
var EMPTY_PROJECT_USAGE = {
|
|
128
|
+
draftLogEntry: ZERO_USAGE,
|
|
129
|
+
consolidateRelease: ZERO_USAGE,
|
|
130
|
+
syncUserManual: ZERO_USAGE,
|
|
131
|
+
rollover: ZERO_ROLLOVER_USAGE
|
|
132
|
+
};
|
|
133
|
+
function usagePath(projectName) {
|
|
134
|
+
return `projects/${projectName}/usage.json`;
|
|
135
|
+
}
|
|
136
|
+
async function getUsageFile(octokit, { owner, repo, projectName }) {
|
|
137
|
+
try {
|
|
138
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
139
|
+
owner,
|
|
140
|
+
repo,
|
|
141
|
+
path: usagePath(projectName)
|
|
142
|
+
});
|
|
143
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
144
|
+
return { usage: EMPTY_PROJECT_USAGE };
|
|
145
|
+
}
|
|
146
|
+
const parsed = JSON.parse(Buffer.from(data.content, "base64").toString("utf-8"));
|
|
147
|
+
const usage = { ...EMPTY_PROJECT_USAGE, ...parsed };
|
|
148
|
+
return { usage, sha: data.sha };
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (isNotFound(error)) {
|
|
151
|
+
return { usage: EMPTY_PROJECT_USAGE };
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function addUsage(current, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
157
|
+
return {
|
|
158
|
+
...current,
|
|
159
|
+
[source]: addTokenUsage(current[source], delta),
|
|
160
|
+
rollover: addRolloverUsage(current.rollover, delta, now)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async function recordUsage(octokit, params, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
164
|
+
const { owner, repo, projectName, branch } = params;
|
|
165
|
+
await retryOnConflict(async () => {
|
|
166
|
+
const { usage: current, sha } = await getUsageFile(octokit, { owner, repo, projectName });
|
|
167
|
+
const updated = addUsage(current, source, delta, now);
|
|
168
|
+
await octokit.rest.repos.createOrUpdateFileContents({
|
|
169
|
+
owner,
|
|
170
|
+
repo,
|
|
171
|
+
path: usagePath(projectName),
|
|
172
|
+
message: `Record ${source} token usage: ${projectName}`,
|
|
173
|
+
content: Buffer.from(JSON.stringify(updated, null, 2), "utf-8").toString("base64"),
|
|
174
|
+
branch,
|
|
175
|
+
sha
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function isNotFound(error) {
|
|
180
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ../../modules/devlore/dist/decisionEntries.js
|
|
184
|
+
function decisionEntriesDir(projectName) {
|
|
185
|
+
return `projects/${projectName}/decisions`;
|
|
186
|
+
}
|
|
187
|
+
function decisionEntryPath(projectName, commitSha, moduleId) {
|
|
188
|
+
const filename = moduleId ? `${commitSha}-${moduleId}.md` : `${commitSha}.md`;
|
|
189
|
+
return `${decisionEntriesDir(projectName)}/${filename}`;
|
|
190
|
+
}
|
|
191
|
+
var DECISION_MARKER = "<!-- devlore:decision -->";
|
|
192
|
+
function parseLogEntryResponse(text) {
|
|
193
|
+
const markerIndex = text.indexOf(DECISION_MARKER);
|
|
194
|
+
if (markerIndex === -1) {
|
|
195
|
+
return { logEntry: text.trim(), decisionEntry: null };
|
|
196
|
+
}
|
|
197
|
+
const logEntry = text.slice(0, markerIndex).trim();
|
|
198
|
+
const decisionEntry = text.slice(markerIndex + DECISION_MARKER.length).trim();
|
|
199
|
+
return { logEntry, decisionEntry: decisionEntry || null };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ../../modules/devlore/dist/diff.js
|
|
203
|
+
function toDiffFile(file) {
|
|
204
|
+
return {
|
|
205
|
+
filename: file.filename,
|
|
206
|
+
status: file.status,
|
|
207
|
+
patch: file.patch ?? "(no patch available)"
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async function getProjectDiff(octokit, { owner, repo, base, head }) {
|
|
211
|
+
try {
|
|
212
|
+
const { data } = await octokit.rest.repos.compareCommitsWithBasehead({
|
|
213
|
+
owner,
|
|
214
|
+
repo,
|
|
215
|
+
basehead: `${base}...${head}`
|
|
216
|
+
});
|
|
217
|
+
return {
|
|
218
|
+
commitMessages: data.commits.map((commit) => commit.commit.message),
|
|
219
|
+
files: (data.files ?? []).map(toDiffFile)
|
|
220
|
+
};
|
|
221
|
+
} catch {
|
|
222
|
+
const { data } = await octokit.rest.repos.getCommit({ owner, repo, ref: head });
|
|
223
|
+
return {
|
|
224
|
+
commitMessages: [data.commit.message],
|
|
225
|
+
files: (data.files ?? []).map(toDiffFile)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ../../modules/devlore/dist/masterDocs.js
|
|
231
|
+
function masterDocsPath(projectName) {
|
|
232
|
+
return `projects/${projectName}/master/README.md`;
|
|
233
|
+
}
|
|
234
|
+
async function getMasterDocs(octokit, params) {
|
|
235
|
+
return (await getMasterDocsFile(octokit, params)).content;
|
|
236
|
+
}
|
|
237
|
+
async function getMasterDocsFile(octokit, { owner, repo, projectName }) {
|
|
238
|
+
try {
|
|
239
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
240
|
+
owner,
|
|
241
|
+
repo,
|
|
242
|
+
path: masterDocsPath(projectName)
|
|
243
|
+
});
|
|
244
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
245
|
+
return { content: "" };
|
|
246
|
+
}
|
|
247
|
+
return { content: Buffer.from(data.content, "base64").toString("utf-8"), sha: data.sha };
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (isNotFound2(error)) {
|
|
250
|
+
return { content: "" };
|
|
251
|
+
}
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function isNotFound2(error) {
|
|
256
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ../../modules/devlore/dist/modules.js
|
|
260
|
+
var MODULES_MANIFEST_PATH = "docs/MODULES.md";
|
|
261
|
+
function parseModulesManifest(yamlText) {
|
|
262
|
+
const lines = yamlText.split("\n");
|
|
263
|
+
let shellProductDoc = null;
|
|
264
|
+
let shellName = null;
|
|
265
|
+
const modules = [];
|
|
266
|
+
let current = null;
|
|
267
|
+
let inShell = false;
|
|
268
|
+
function flushCurrent() {
|
|
269
|
+
if (current && current.id && current.name && current.productDoc) {
|
|
270
|
+
modules.push(current);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const rawLine of lines) {
|
|
274
|
+
const line = rawLine.replace(/#.*$/, "").trimEnd();
|
|
275
|
+
if (!line.trim())
|
|
276
|
+
continue;
|
|
277
|
+
if (/^shell:\s*$/.test(line)) {
|
|
278
|
+
inShell = true;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (/^modules:\s*$/.test(line)) {
|
|
282
|
+
inShell = false;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (inShell) {
|
|
286
|
+
const productDocMatch2 = line.match(/^\s+productDoc:\s*(.+)$/);
|
|
287
|
+
if (productDocMatch2)
|
|
288
|
+
shellProductDoc = productDocMatch2[1].trim();
|
|
289
|
+
const nameMatch2 = line.match(/^\s+name:\s*(.+)$/);
|
|
290
|
+
if (nameMatch2)
|
|
291
|
+
shellName = nameMatch2[1].trim();
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const itemStart = line.match(/^\s*-\s*id:\s*(.+)$/);
|
|
295
|
+
if (itemStart) {
|
|
296
|
+
flushCurrent();
|
|
297
|
+
current = { id: itemStart[1].trim() };
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const nameMatch = line.match(/^\s+name:\s*(.+)$/);
|
|
301
|
+
if (nameMatch && current) {
|
|
302
|
+
current.name = nameMatch[1].trim();
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const productDocMatch = line.match(/^\s+productDoc:\s*(.+)$/);
|
|
306
|
+
if (productDocMatch && current) {
|
|
307
|
+
current.productDoc = productDocMatch[1].trim();
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
flushCurrent();
|
|
312
|
+
if (!shellProductDoc) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return { shell: { productDoc: shellProductDoc, ...shellName ? { name: shellName } : {} }, modules };
|
|
316
|
+
}
|
|
317
|
+
async function readModulesManifest(octokit, { owner, repo, ref }) {
|
|
318
|
+
try {
|
|
319
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
320
|
+
owner,
|
|
321
|
+
repo,
|
|
322
|
+
path: MODULES_MANIFEST_PATH,
|
|
323
|
+
ref
|
|
324
|
+
});
|
|
325
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const text = Buffer.from(data.content, "base64").toString("utf-8");
|
|
329
|
+
return parseModulesManifest(text);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (isNotFound3(error)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function isNotFound3(error) {
|
|
338
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ../../modules/devlore/dist/prompt.js
|
|
342
|
+
var MAX_DIFF_CHARS = 2e4;
|
|
343
|
+
function buildLogEntryPrompt(input) {
|
|
344
|
+
const diffText = input.files.map((file) => `--- ${file.filename} (${file.status}) ---
|
|
345
|
+
${file.patch}`).join("\n\n");
|
|
346
|
+
const diff = diffText.length > MAX_DIFF_CHARS ? `${diffText.slice(0, MAX_DIFF_CHARS)}
|
|
347
|
+
|
|
348
|
+
[diff truncated]` : diffText;
|
|
349
|
+
return [
|
|
350
|
+
`Project: ${input.projectName}`,
|
|
351
|
+
...input.moduleName ? [
|
|
352
|
+
`Module: ${input.moduleName}`,
|
|
353
|
+
`This diff is only this module's own slice of a larger push that touched more than one module \u2014 draft the log entry scoped to this module alone, not the whole push.`
|
|
354
|
+
] : [],
|
|
355
|
+
`Date: ${input.date}`,
|
|
356
|
+
"",
|
|
357
|
+
"Commit messages:",
|
|
358
|
+
input.commitMessages.map((message) => `- ${message}`).join("\n"),
|
|
359
|
+
"",
|
|
360
|
+
"Current master docs (may be empty if none exist yet):",
|
|
361
|
+
input.masterDocs || "(none yet)",
|
|
362
|
+
"",
|
|
363
|
+
"Diff:",
|
|
364
|
+
diff,
|
|
365
|
+
"",
|
|
366
|
+
`Draft a concise log entry in markdown dated ${input.date} covering: what changed, why (infer from the commits/diff), and files touched. This is a draft trail, not the official record \u2014 keep it factual and brief.`,
|
|
367
|
+
"",
|
|
368
|
+
"Separately, judge whether this push represents a genuine architectural or strategic DECISION \u2014 not routine work, even important routine work. It counts only if a real fork existed and one path was chosen over real alternatives, in a way that shapes how future work in this area gets done:",
|
|
369
|
+
"- A technology, architecture, or approach chosen over a real alternative.",
|
|
370
|
+
"- A previously-taken approach reversed, replaced, or explicitly rejected.",
|
|
371
|
+
"- A dependency or service adopted or dropped for a stated reason.",
|
|
372
|
+
"- A cross-cutting convention established that future work is expected to follow.",
|
|
373
|
+
"",
|
|
374
|
+
"It does NOT count, even when the change is real and important, if it's:",
|
|
375
|
+
"- A bug fix, even a significant one \u2014 correcting behavior to match an already-decided design isn't a new decision.",
|
|
376
|
+
"- A new feature or requirement implemented by following an already-established pattern.",
|
|
377
|
+
"- A refactor, rename, or reorganization that doesn't change any actual approach.",
|
|
378
|
+
"- A test, tooling, formatting, or lint fix.",
|
|
379
|
+
"",
|
|
380
|
+
`The test to apply: if someone joins this project a year from now and asks "why does it work this way instead of some other way," would this diff be the answer? If the honest answer is "because that's just what implementing the feature required," it isn't a decision. If genuinely borderline, treat it as not a decision.`,
|
|
381
|
+
"",
|
|
382
|
+
`If \u2014 and only if \u2014 this push is a real decision by that bar, add this after the log entry, on its own line: ${DECISION_MARKER}`,
|
|
383
|
+
"Then, below it, write the decision entry in this format:",
|
|
384
|
+
"",
|
|
385
|
+
"## <Short decision title>",
|
|
386
|
+
"**Context:** <what prompted this \u2014 the problem or constraint that made a choice necessary>",
|
|
387
|
+
"**Decision:** <what was chosen>",
|
|
388
|
+
"**Alternatives considered:** <other real options and why they weren't chosen \u2014 omit this line entirely if none were genuinely on the table>",
|
|
389
|
+
"**Consequences:** <what this commits future work to, or rules out>",
|
|
390
|
+
"",
|
|
391
|
+
"If this push is not a decision by that bar, do not include the marker or a decision section at all \u2014 just the log entry."
|
|
392
|
+
].join("\n");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ../../modules/devlore/dist/productDoc.js
|
|
396
|
+
var SOURCE_PRODUCT_DOC_PATH = "docs/PRODUCT.md";
|
|
397
|
+
function productDocPath(projectName, moduleId) {
|
|
398
|
+
return moduleId ? `projects/${projectName}/product-${moduleId}.md` : `projects/${projectName}/product.md`;
|
|
399
|
+
}
|
|
400
|
+
async function getSourceProductDoc(octokit, { owner, repo, ref, path = SOURCE_PRODUCT_DOC_PATH }) {
|
|
401
|
+
try {
|
|
402
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
403
|
+
owner,
|
|
404
|
+
repo,
|
|
405
|
+
path,
|
|
406
|
+
ref
|
|
407
|
+
});
|
|
408
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (isNotFound4(error)) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async function getVaultProductDoc(octokit, { owner, repo, projectName, moduleId }) {
|
|
420
|
+
try {
|
|
421
|
+
const { data } = await octokit.rest.repos.getContent({
|
|
422
|
+
owner,
|
|
423
|
+
repo,
|
|
424
|
+
path: productDocPath(projectName, moduleId)
|
|
425
|
+
});
|
|
426
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
427
|
+
return { content: "" };
|
|
428
|
+
}
|
|
429
|
+
return { content: Buffer.from(data.content, "base64").toString("utf-8"), sha: data.sha };
|
|
430
|
+
} catch (error) {
|
|
431
|
+
if (isNotFound4(error)) {
|
|
432
|
+
return { content: "" };
|
|
433
|
+
}
|
|
434
|
+
throw error;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function isNotFound4(error) {
|
|
438
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ../../modules/devlore/dist/pushRouting.js
|
|
442
|
+
var PRODUCT_DOC_SUFFIX = "/docs/PRODUCT.md";
|
|
443
|
+
function moduleDir(productDocPath2) {
|
|
444
|
+
return productDocPath2.endsWith(PRODUCT_DOC_SUFFIX) ? productDocPath2.slice(0, -PRODUCT_DOC_SUFFIX.length) : null;
|
|
445
|
+
}
|
|
446
|
+
function routeDiffByModule(files, manifest) {
|
|
447
|
+
if (!manifest) {
|
|
448
|
+
return [{ moduleId: null, moduleName: null, productDocPath: "docs/PRODUCT.md", files }];
|
|
449
|
+
}
|
|
450
|
+
const candidates = manifest.modules.map((m) => ({ id: m.id, name: m.name, productDoc: m.productDoc, dir: moduleDir(m.productDoc) })).filter((m) => m.dir !== null);
|
|
451
|
+
const filesByModuleId = /* @__PURE__ */ new Map();
|
|
452
|
+
const shellFiles = [];
|
|
453
|
+
for (const file of files) {
|
|
454
|
+
const match = candidates.find((m) => file.filename === m.dir || file.filename.startsWith(`${m.dir}/`));
|
|
455
|
+
if (!match) {
|
|
456
|
+
shellFiles.push(file);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const bucket = filesByModuleId.get(match.id) ?? [];
|
|
460
|
+
bucket.push(file);
|
|
461
|
+
filesByModuleId.set(match.id, bucket);
|
|
462
|
+
}
|
|
463
|
+
const buckets = [];
|
|
464
|
+
for (const candidate of candidates) {
|
|
465
|
+
const bucketFiles = filesByModuleId.get(candidate.id);
|
|
466
|
+
if (bucketFiles && bucketFiles.length > 0) {
|
|
467
|
+
buckets.push({ moduleId: candidate.id, moduleName: candidate.name, productDocPath: candidate.productDoc, files: bucketFiles });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (shellFiles.length > 0 || buckets.length === 0) {
|
|
471
|
+
buckets.push({ moduleId: null, moduleName: null, productDocPath: manifest.shell.productDoc, files: shellFiles });
|
|
472
|
+
}
|
|
473
|
+
return buckets;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ../../modules/devlore/dist/redact.js
|
|
477
|
+
var REDACTED = "[REDACTED]";
|
|
478
|
+
var KNOWN_SECRET_PATTERNS = [
|
|
479
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
480
|
+
/gh[pousr]_[A-Za-z0-9]{36,}/g,
|
|
481
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
482
|
+
/sk-ant-[A-Za-z0-9_-]{20,}/g,
|
|
483
|
+
/sk-[A-Za-z0-9]{20,}/g,
|
|
484
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
485
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
486
|
+
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g
|
|
487
|
+
];
|
|
488
|
+
var SECRET_KEYWORD = "(?:API[_-]?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CREDENTIAL)";
|
|
489
|
+
var ASSIGNMENT_PATTERN = new RegExp(`((?:[A-Za-z0-9]+[_-])*${SECRET_KEYWORD}(?:[_-][A-Za-z0-9]+)*\\s*[:=]\\s*)(["']?)([^"'\`\\s]+)\\2`, "gi");
|
|
490
|
+
var SENSITIVE_FILENAME_PATTERNS = [
|
|
491
|
+
/(^|\/)\.env(\..+)?$/,
|
|
492
|
+
/\.pem$/,
|
|
493
|
+
/\.key$/,
|
|
494
|
+
/\.p12$/,
|
|
495
|
+
/\.pfx$/,
|
|
496
|
+
/(^|\/)id_rsa(\.pub)?$/,
|
|
497
|
+
/(^|\/)id_ed25519(\.pub)?$/,
|
|
498
|
+
/credentials\.json$/,
|
|
499
|
+
/secrets?\.(json|ya?ml|toml)$/
|
|
500
|
+
];
|
|
501
|
+
function redactText(text) {
|
|
502
|
+
let result = text;
|
|
503
|
+
for (const pattern of KNOWN_SECRET_PATTERNS) {
|
|
504
|
+
result = result.replace(pattern, REDACTED);
|
|
505
|
+
}
|
|
506
|
+
result = result.replace(ASSIGNMENT_PATTERN, (_match, prefix, quote) => `${prefix}${quote}${REDACTED}${quote}`);
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
function isSensitiveFilename(filename) {
|
|
510
|
+
return SENSITIVE_FILENAME_PATTERNS.some((pattern) => pattern.test(filename));
|
|
511
|
+
}
|
|
512
|
+
function redactDiffFile(file) {
|
|
513
|
+
if (isSensitiveFilename(file.filename)) {
|
|
514
|
+
return { ...file, patch: `(patch redacted: ${file.filename} may contain secrets)` };
|
|
515
|
+
}
|
|
516
|
+
return { ...file, patch: redactText(file.patch) };
|
|
517
|
+
}
|
|
518
|
+
function redactProjectDiff(diff) {
|
|
519
|
+
return {
|
|
520
|
+
commitMessages: diff.commitMessages.map(redactText),
|
|
521
|
+
files: diff.files.map(redactDiffFile)
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ../../modules/devlore/dist/actions/draftLogEntry.js
|
|
526
|
+
var VAULT_BRANCH = "main";
|
|
527
|
+
var MODEL_TIER = "balanced";
|
|
528
|
+
var MAX_TOKENS = 2048;
|
|
529
|
+
function logEntryPath(projectName, commitSha, moduleId) {
|
|
530
|
+
const filename = moduleId ? `${commitSha}-${moduleId}.md` : `${commitSha}.md`;
|
|
531
|
+
return `projects/${projectName}/log/${filename}`;
|
|
532
|
+
}
|
|
533
|
+
async function syncProductDoc(projectOctokit, vaultOctokit, { projectOwner, projectRepoName, vaultOwner, vaultRepoName, projectName, headSha, sourceProductDocPath, moduleId }) {
|
|
534
|
+
const sourceDoc = await getSourceProductDoc(projectOctokit, {
|
|
535
|
+
owner: projectOwner,
|
|
536
|
+
repo: projectRepoName,
|
|
537
|
+
ref: headSha,
|
|
538
|
+
path: sourceProductDocPath
|
|
539
|
+
});
|
|
540
|
+
if (sourceDoc === null) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const redactedDoc = redactText(sourceDoc);
|
|
544
|
+
await retryOnConflict(async () => {
|
|
545
|
+
const existing = await getVaultProductDoc(vaultOctokit, {
|
|
546
|
+
owner: vaultOwner,
|
|
547
|
+
repo: vaultRepoName,
|
|
548
|
+
projectName,
|
|
549
|
+
moduleId
|
|
550
|
+
});
|
|
551
|
+
if (existing.content === redactedDoc) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
await vaultOctokit.rest.repos.createOrUpdateFileContents({
|
|
555
|
+
owner: vaultOwner,
|
|
556
|
+
repo: vaultRepoName,
|
|
557
|
+
path: productDocPath(projectName, moduleId),
|
|
558
|
+
message: `Sync PRODUCT.md: ${projectName}@${headSha.slice(0, 7)}`,
|
|
559
|
+
content: Buffer.from(redactedDoc, "utf-8").toString("base64"),
|
|
560
|
+
branch: VAULT_BRANCH,
|
|
561
|
+
sha: existing.sha
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
async function draftBucketLogEntry({ vaultOctokit, claude, vaultOwner, vaultRepoName, projectName, headSha, date, commitMessages, files, masterDocs, moduleId, moduleName }) {
|
|
566
|
+
const prompt = buildLogEntryPrompt({
|
|
567
|
+
projectName,
|
|
568
|
+
date,
|
|
569
|
+
commitMessages,
|
|
570
|
+
files,
|
|
571
|
+
masterDocs,
|
|
572
|
+
moduleName: moduleName ?? void 0
|
|
573
|
+
});
|
|
574
|
+
const { text, usage } = await createCompleteMessage(claude, {
|
|
575
|
+
model: defaultModelFor(MODEL_TIER),
|
|
576
|
+
max_tokens: MAX_TOKENS,
|
|
577
|
+
messages: [{ role: "user", content: prompt }]
|
|
578
|
+
});
|
|
579
|
+
const { logEntry: entryBody, decisionEntry } = parseLogEntryResponse(text);
|
|
580
|
+
const shortSha = headSha.slice(0, 7);
|
|
581
|
+
const entryTitle = moduleName ? `${projectName} (${moduleName}) \u2014 ${shortSha}` : `${projectName} \u2014 ${shortSha}`;
|
|
582
|
+
const entryContent = `# ${entryTitle}
|
|
583
|
+
|
|
584
|
+
${entryBody}
|
|
585
|
+
`;
|
|
586
|
+
const entryPath = logEntryPath(projectName, headSha, moduleId ?? void 0);
|
|
587
|
+
await retryOnConflict(() => vaultOctokit.rest.repos.createOrUpdateFileContents({
|
|
588
|
+
owner: vaultOwner,
|
|
589
|
+
repo: vaultRepoName,
|
|
590
|
+
path: entryPath,
|
|
591
|
+
message: `Draft log entry: ${projectName}@${shortSha}${moduleName ? ` (${moduleName})` : ""}`,
|
|
592
|
+
content: Buffer.from(entryContent, "utf-8").toString("base64"),
|
|
593
|
+
branch: VAULT_BRANCH
|
|
594
|
+
}));
|
|
595
|
+
let decisionPath = null;
|
|
596
|
+
if (decisionEntry) {
|
|
597
|
+
decisionPath = decisionEntryPath(projectName, headSha, moduleId ?? void 0);
|
|
598
|
+
const decisionContent = `# ${entryTitle}
|
|
599
|
+
|
|
600
|
+
${decisionEntry}
|
|
601
|
+
`;
|
|
602
|
+
await retryOnConflict(() => vaultOctokit.rest.repos.createOrUpdateFileContents({
|
|
603
|
+
owner: vaultOwner,
|
|
604
|
+
repo: vaultRepoName,
|
|
605
|
+
path: decisionPath,
|
|
606
|
+
message: `Draft decision entry: ${projectName}@${shortSha}${moduleName ? ` (${moduleName})` : ""}`,
|
|
607
|
+
content: Buffer.from(decisionContent, "utf-8").toString("base64"),
|
|
608
|
+
branch: VAULT_BRANCH
|
|
609
|
+
}));
|
|
610
|
+
}
|
|
611
|
+
return { moduleId, logEntryPath: entryPath, decisionEntryPath: decisionPath, usage };
|
|
612
|
+
}
|
|
613
|
+
async function draftLogEntry({ projectOctokit, vaultOctokit, claude, projectOwner, projectRepoName, vaultOwner, vaultRepoName, projectName, baseSha, headSha }) {
|
|
614
|
+
const diff = redactProjectDiff(await getProjectDiff(projectOctokit, {
|
|
615
|
+
owner: projectOwner,
|
|
616
|
+
repo: projectRepoName,
|
|
617
|
+
base: baseSha,
|
|
618
|
+
head: headSha
|
|
619
|
+
}));
|
|
620
|
+
const manifest = await readModulesManifest(projectOctokit, {
|
|
621
|
+
owner: projectOwner,
|
|
622
|
+
repo: projectRepoName,
|
|
623
|
+
ref: headSha
|
|
624
|
+
});
|
|
625
|
+
const buckets = routeDiffByModule(diff.files, manifest);
|
|
626
|
+
const masterDocs = await getMasterDocs(vaultOctokit, {
|
|
627
|
+
owner: vaultOwner,
|
|
628
|
+
repo: vaultRepoName,
|
|
629
|
+
projectName
|
|
630
|
+
});
|
|
631
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
632
|
+
const results = [];
|
|
633
|
+
for (const bucket of buckets) {
|
|
634
|
+
const result = await draftBucketLogEntry({
|
|
635
|
+
vaultOctokit,
|
|
636
|
+
claude,
|
|
637
|
+
vaultOwner,
|
|
638
|
+
vaultRepoName,
|
|
639
|
+
projectName,
|
|
640
|
+
headSha,
|
|
641
|
+
date,
|
|
642
|
+
commitMessages: diff.commitMessages,
|
|
643
|
+
files: bucket.files,
|
|
644
|
+
masterDocs,
|
|
645
|
+
moduleId: bucket.moduleId,
|
|
646
|
+
moduleName: bucket.moduleName
|
|
647
|
+
});
|
|
648
|
+
results.push(result);
|
|
649
|
+
await syncProductDoc(projectOctokit, vaultOctokit, {
|
|
650
|
+
projectOwner,
|
|
651
|
+
projectRepoName,
|
|
652
|
+
vaultOwner,
|
|
653
|
+
vaultRepoName,
|
|
654
|
+
projectName,
|
|
655
|
+
headSha,
|
|
656
|
+
sourceProductDocPath: bucket.productDocPath,
|
|
657
|
+
moduleId: bucket.moduleId ?? void 0
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
const totalUsage = results.reduce((total, r) => addTokenUsage(total, r.usage), ZERO_USAGE);
|
|
661
|
+
await recordUsage(vaultOctokit, { owner: vaultOwner, repo: vaultRepoName, projectName, branch: VAULT_BRANCH }, "draftLogEntry", totalUsage);
|
|
662
|
+
return results;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ../../modules/devlore/dist/bin/draftLogEntryCli.js
|
|
666
|
+
function requireEnv(env, name) {
|
|
667
|
+
const value = env[name];
|
|
668
|
+
if (!value) {
|
|
669
|
+
throw new Error(`${name} is not set`);
|
|
670
|
+
}
|
|
671
|
+
return value;
|
|
672
|
+
}
|
|
673
|
+
var defaultDeps = {
|
|
674
|
+
env: process.env,
|
|
675
|
+
draftLogEntry,
|
|
676
|
+
createGithubClient,
|
|
677
|
+
createClaudeClient,
|
|
678
|
+
log: console.log
|
|
679
|
+
};
|
|
680
|
+
async function main(deps = defaultDeps) {
|
|
681
|
+
const { env } = deps;
|
|
682
|
+
const projectName = requireEnv(env, "PROJECT_NAME");
|
|
683
|
+
const [projectOwner, projectRepoName] = requireEnv(env, "PROJECT_REPO").split("/");
|
|
684
|
+
const [vaultOwner, vaultRepoName] = requireEnv(env, "VAULT_REPO").split("/");
|
|
685
|
+
const baseSha = requireEnv(env, "BASE_SHA");
|
|
686
|
+
const headSha = requireEnv(env, "HEAD_SHA");
|
|
687
|
+
const projectOctokit = deps.createGithubClient(requireEnv(env, "GITHUB_TOKEN"));
|
|
688
|
+
const vaultOctokit = deps.createGithubClient(requireEnv(env, "DEVKEEP_GITHUB_TOKEN"));
|
|
689
|
+
const claude = deps.createClaudeClient(requireEnv(env, "ANTHROPIC_API_KEY"));
|
|
690
|
+
const results = await deps.draftLogEntry({
|
|
691
|
+
projectOctokit,
|
|
692
|
+
vaultOctokit,
|
|
693
|
+
claude,
|
|
694
|
+
projectOwner,
|
|
695
|
+
projectRepoName,
|
|
696
|
+
vaultOwner,
|
|
697
|
+
vaultRepoName,
|
|
698
|
+
projectName,
|
|
699
|
+
baseSha,
|
|
700
|
+
headSha
|
|
701
|
+
});
|
|
702
|
+
for (const result of results) {
|
|
703
|
+
deps.log(`Drafted log entry: ${result.logEntryPath}`);
|
|
704
|
+
if (result.decisionEntryPath) {
|
|
705
|
+
deps.log(`Drafted decision entry: ${result.decisionEntryPath}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
710
|
+
main().catch((error) => {
|
|
711
|
+
console.error(error);
|
|
712
|
+
process.exit(1);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
export {
|
|
716
|
+
main,
|
|
717
|
+
requireEnv
|
|
718
|
+
};
|