@starterculture/devkeep-actions 0.5.0 → 0.5.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/dist/consolidate-release.js +71 -18
- package/dist/crypting-candidates-scan.js +17 -10
- package/dist/draft-log-entry.js +71 -18
- package/dist/draft-release-notes.js +94 -41
- package/dist/sync-onboarding.js +143 -80
- package/package.json +1 -1
|
@@ -25,9 +25,41 @@ async function retryOnConflict(attempt, maxAttempts = 3) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// ../../modules/core/dist/tokenUsage.js
|
|
28
|
+
function usageForModel(model, tokens) {
|
|
29
|
+
return { ...tokens, byModel: { [model]: tokens } };
|
|
30
|
+
}
|
|
28
31
|
var ZERO_USAGE = { inputTokens: 0, outputTokens: 0 };
|
|
29
32
|
function addTokenUsage(a, b) {
|
|
30
|
-
|
|
33
|
+
const cacheReadTokens = (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0);
|
|
34
|
+
const cacheWriteTokens = (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0);
|
|
35
|
+
const byModel = mergeByModel(a.byModel, b.byModel);
|
|
36
|
+
return {
|
|
37
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
38
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
39
|
+
// Kept absent rather than zero when nothing was cached, so a ledger
|
|
40
|
+
// entry from before caching and one from a run that cached nothing
|
|
41
|
+
// look the same.
|
|
42
|
+
...cacheReadTokens > 0 ? { cacheReadTokens } : {},
|
|
43
|
+
...cacheWriteTokens > 0 ? { cacheWriteTokens } : {},
|
|
44
|
+
// Absent for the same reason: a record with no model attribution and
|
|
45
|
+
// one with an empty map should not read differently.
|
|
46
|
+
...byModel ? { byModel } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function mergeByModel(a, b) {
|
|
50
|
+
if (!a && !b)
|
|
51
|
+
return void 0;
|
|
52
|
+
const merged = { ...a };
|
|
53
|
+
for (const [model, tokens] of Object.entries(b ?? {})) {
|
|
54
|
+
const existing = merged[model];
|
|
55
|
+
merged[model] = existing ? {
|
|
56
|
+
inputTokens: existing.inputTokens + tokens.inputTokens,
|
|
57
|
+
outputTokens: existing.outputTokens + tokens.outputTokens,
|
|
58
|
+
...(existing.cacheReadTokens ?? 0) + (tokens.cacheReadTokens ?? 0) > 0 ? { cacheReadTokens: (existing.cacheReadTokens ?? 0) + (tokens.cacheReadTokens ?? 0) } : {},
|
|
59
|
+
...(existing.cacheWriteTokens ?? 0) + (tokens.cacheWriteTokens ?? 0) > 0 ? { cacheWriteTokens: (existing.cacheWriteTokens ?? 0) + (tokens.cacheWriteTokens ?? 0) } : {}
|
|
60
|
+
} : tokens;
|
|
61
|
+
}
|
|
62
|
+
return merged;
|
|
31
63
|
}
|
|
32
64
|
|
|
33
65
|
// ../../modules/core/dist/rollover.js
|
|
@@ -89,10 +121,10 @@ async function createCompleteMessage(claude, params, { maxContinuations = DEFAUL
|
|
|
89
121
|
const message = requestOptions ? await claude.messages.create({ ...params, messages }, requestOptions) : await claude.messages.create({ ...params, messages });
|
|
90
122
|
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
91
123
|
fullText += text;
|
|
92
|
-
usage = addTokenUsage(usage, {
|
|
124
|
+
usage = addTokenUsage(usage, usageForModel(params.model, {
|
|
93
125
|
inputTokens: message.usage?.input_tokens ?? 0,
|
|
94
126
|
outputTokens: message.usage?.output_tokens ?? 0
|
|
95
|
-
});
|
|
127
|
+
}));
|
|
96
128
|
if (message.stop_reason !== "max_tokens") {
|
|
97
129
|
return { text: fullText, usage };
|
|
98
130
|
}
|
|
@@ -104,19 +136,19 @@ async function createCompleteMessage(claude, params, { maxContinuations = DEFAUL
|
|
|
104
136
|
var MODEL_REGISTRY = {
|
|
105
137
|
fast: {
|
|
106
138
|
description: "Quick, cheap work where depth matters least.",
|
|
107
|
-
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5" }]
|
|
139
|
+
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5", contextTokens: 2e5 }]
|
|
108
140
|
},
|
|
109
141
|
balanced: {
|
|
110
142
|
description: "The everyday tier: chat, log entries, release notes, the daily digest. Frequent enough that cost matters.",
|
|
111
|
-
models: [{ id: "claude-sonnet-5", label: "Sonnet 5" }]
|
|
143
|
+
models: [{ id: "claude-sonnet-5", label: "Sonnet 5", contextTokens: 2e5 }]
|
|
112
144
|
},
|
|
113
145
|
capable: {
|
|
114
146
|
description: "Deeper reasoning for infrequent, high-value work where getting it right outweighs the cost.",
|
|
115
|
-
models: [{ id: "claude-opus-5", label: "Opus 5" }]
|
|
147
|
+
models: [{ id: "claude-opus-5", label: "Opus 5", contextTokens: 2e5 }]
|
|
116
148
|
},
|
|
117
149
|
max: {
|
|
118
150
|
description: "Maximum depth, for long-running agentic work. Opt-in \u2014 the most expensive tier.",
|
|
119
|
-
models: [{ id: "claude-fable-5", label: "Fable 5" }]
|
|
151
|
+
models: [{ id: "claude-fable-5", label: "Fable 5", contextTokens: 2e5 }]
|
|
120
152
|
}
|
|
121
153
|
};
|
|
122
154
|
function defaultModelFor(tier) {
|
|
@@ -232,17 +264,15 @@ function currentRunUrl(env) {
|
|
|
232
264
|
// ../../modules/core/dist/ciEntrypoint.js
|
|
233
265
|
var VAULT_BRANCH = "main";
|
|
234
266
|
function healthParams(env) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (!projectName || !vaultRepo || !token) {
|
|
239
|
-
return null;
|
|
267
|
+
const missing = ["PROJECT_NAME", "VAULT_REPO", "DEVKEEP_GITHUB_TOKEN"].filter((name) => !env[name]);
|
|
268
|
+
if (missing.length > 0) {
|
|
269
|
+
return { missing: [...missing] };
|
|
240
270
|
}
|
|
241
|
-
const [owner, repo] =
|
|
271
|
+
const [owner, repo] = env.VAULT_REPO.split("/");
|
|
242
272
|
if (!owner || !repo) {
|
|
243
|
-
return
|
|
273
|
+
return { missing: ["VAULT_REPO (expected owner/repo)"] };
|
|
244
274
|
}
|
|
245
|
-
return { owner, repo, projectName, branch: VAULT_BRANCH };
|
|
275
|
+
return { params: { owner, repo, projectName: env.PROJECT_NAME, branch: VAULT_BRANCH } };
|
|
246
276
|
}
|
|
247
277
|
var defaultDeps = {
|
|
248
278
|
env: process.env,
|
|
@@ -253,7 +283,11 @@ var defaultDeps = {
|
|
|
253
283
|
exit: (code) => process.exit(code)
|
|
254
284
|
};
|
|
255
285
|
async function runCiEntrypoint(source, main2, deps = defaultDeps) {
|
|
256
|
-
const
|
|
286
|
+
const resolved = healthParams(deps.env);
|
|
287
|
+
const params = "params" in resolved ? resolved.params : null;
|
|
288
|
+
if (!params) {
|
|
289
|
+
deps.logError(`Health not recorded for "${source}": ${resolved.missing.join(", ")} not set.`);
|
|
290
|
+
}
|
|
257
291
|
const octokit = params ? deps.createGithubClient(deps.env.DEVKEEP_GITHUB_TOKEN) : null;
|
|
258
292
|
try {
|
|
259
293
|
await main2();
|
|
@@ -278,7 +312,7 @@ async function runCiEntrypoint(source, main2, deps = defaultDeps) {
|
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
314
|
|
|
281
|
-
// ../../modules/
|
|
315
|
+
// ../../modules/core/dist/ciUsage.js
|
|
282
316
|
var EMPTY_PROJECT_USAGE = {
|
|
283
317
|
draftLogEntry: ZERO_USAGE,
|
|
284
318
|
consolidateRelease: ZERO_USAGE,
|
|
@@ -289,6 +323,7 @@ var EMPTY_PROJECT_USAGE = {
|
|
|
289
323
|
syncOnboarding: ZERO_USAGE,
|
|
290
324
|
releaseNotes: ZERO_USAGE,
|
|
291
325
|
captureBaseline: ZERO_USAGE,
|
|
326
|
+
devseerReview: ZERO_USAGE,
|
|
292
327
|
rollover: ZERO_ROLLOVER_USAGE
|
|
293
328
|
};
|
|
294
329
|
function usagePath(projectName) {
|
|
@@ -315,14 +350,24 @@ async function getUsageFile(octokit, { owner, repo, projectName }) {
|
|
|
315
350
|
}
|
|
316
351
|
}
|
|
317
352
|
function addUsage(current, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
353
|
+
const bySource = current.rolloverBySource ?? {};
|
|
318
354
|
return {
|
|
319
355
|
...current,
|
|
320
356
|
[source]: addTokenUsage(current[source], delta),
|
|
321
|
-
|
|
357
|
+
// Both: the combined bucket stays the total it always was, and the
|
|
358
|
+
// per-source split is what lets the dashboard attribute it.
|
|
359
|
+
rollover: addRolloverUsage(current.rollover, delta, now),
|
|
360
|
+
rolloverBySource: {
|
|
361
|
+
...bySource,
|
|
362
|
+
[source]: addRolloverUsage(bySource[source] ?? ZERO_ROLLOVER_USAGE, delta, now)
|
|
363
|
+
}
|
|
322
364
|
};
|
|
323
365
|
}
|
|
324
366
|
async function recordUsage(octokit, params, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
325
367
|
const { owner, repo, projectName, branch } = params;
|
|
368
|
+
if (delta.inputTokens === 0 && delta.outputTokens === 0) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
326
371
|
await retryOnConflict(async () => {
|
|
327
372
|
const { usage: current, sha } = await getUsageFile(octokit, { owner, repo, projectName });
|
|
328
373
|
const updated = addUsage(current, source, delta, now);
|
|
@@ -374,6 +419,14 @@ function isNotFound2(error) {
|
|
|
374
419
|
}
|
|
375
420
|
|
|
376
421
|
// ../../modules/devlore/dist/prompt.js
|
|
422
|
+
var SUPERSEDED_GUIDANCE = [
|
|
423
|
+
"These documents record how the thinking changed over time, so they contain claims that were later corrected, and the corrected version is still physically present.",
|
|
424
|
+
"**A correction outranks what it corrects.** Where two passages disagree, the later-dated one is what is true now. A passage struck through, marked superseded, described as decided against, or sitting under a heading that says the earlier view was wrong, records what was believed \u2014 never what is. Do not restate any of it as current."
|
|
425
|
+
].join("\n");
|
|
426
|
+
var SUPERSEDED_GUIDANCE_WITH_LESSONS = [
|
|
427
|
+
SUPERSEDED_GUIDANCE,
|
|
428
|
+
"A reversed decision is still worth telling a newcomer about \u2014 but as something the project learned, never as a rule they should follow. Say what is true now first, and mention the earlier version only where the fact that it changed is itself the lesson."
|
|
429
|
+
].join("\n");
|
|
377
430
|
function buildConsolidationPrompt(input) {
|
|
378
431
|
const entriesText = input.logEntries.map((entry) => `--- ${entry.path} ---
|
|
379
432
|
${entry.content}`).join("\n\n");
|
|
@@ -129,17 +129,15 @@ function currentRunUrl(env) {
|
|
|
129
129
|
// ../../modules/core/dist/ciEntrypoint.js
|
|
130
130
|
var VAULT_BRANCH = "main";
|
|
131
131
|
function healthParams(env) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!projectName || !vaultRepo || !token) {
|
|
136
|
-
return null;
|
|
132
|
+
const missing = ["PROJECT_NAME", "VAULT_REPO", "DEVKEEP_GITHUB_TOKEN"].filter((name) => !env[name]);
|
|
133
|
+
if (missing.length > 0) {
|
|
134
|
+
return { missing: [...missing] };
|
|
137
135
|
}
|
|
138
|
-
const [owner, repo] =
|
|
136
|
+
const [owner, repo] = env.VAULT_REPO.split("/");
|
|
139
137
|
if (!owner || !repo) {
|
|
140
|
-
return
|
|
138
|
+
return { missing: ["VAULT_REPO (expected owner/repo)"] };
|
|
141
139
|
}
|
|
142
|
-
return { owner, repo, projectName, branch: VAULT_BRANCH };
|
|
140
|
+
return { params: { owner, repo, projectName: env.PROJECT_NAME, branch: VAULT_BRANCH } };
|
|
143
141
|
}
|
|
144
142
|
var defaultDeps = {
|
|
145
143
|
env: process.env,
|
|
@@ -150,7 +148,11 @@ var defaultDeps = {
|
|
|
150
148
|
exit: (code) => process.exit(code)
|
|
151
149
|
};
|
|
152
150
|
async function runCiEntrypoint(source, main2, deps = defaultDeps) {
|
|
153
|
-
const
|
|
151
|
+
const resolved = healthParams(deps.env);
|
|
152
|
+
const params = "params" in resolved ? resolved.params : null;
|
|
153
|
+
if (!params) {
|
|
154
|
+
deps.logError(`Health not recorded for "${source}": ${resolved.missing.join(", ")} not set.`);
|
|
155
|
+
}
|
|
154
156
|
const octokit = params ? deps.createGithubClient(deps.env.DEVKEEP_GITHUB_TOKEN) : null;
|
|
155
157
|
try {
|
|
156
158
|
await main2();
|
|
@@ -497,7 +499,12 @@ async function main(deps = defaultDeps2) {
|
|
|
497
499
|
const headSha = requireEnv(env, "HEAD_SHA");
|
|
498
500
|
const projectOctokit = deps.createGithubClient(requireEnv(env, "GITHUB_TOKEN"));
|
|
499
501
|
const vaultOctokit = deps.createGithubClient(requireEnv(env, "DEVKEEP_GITHUB_TOKEN"));
|
|
500
|
-
const
|
|
502
|
+
const rawKey = env.DEVCRYPT_BLIND_INDEX_KEY;
|
|
503
|
+
if (!rawKey) {
|
|
504
|
+
deps.log("No DEVCRYPT_BLIND_INDEX_KEY for this project \u2014 Devcrypt has not been used against this vault, so there is nothing to scan for. Re-link once it has been.");
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const blindIndexKey = Buffer.from(rawKey, "base64");
|
|
501
508
|
const result = await deps.scanCryptingCandidates({
|
|
502
509
|
projectOctokit,
|
|
503
510
|
vaultOctokit,
|
package/dist/draft-log-entry.js
CHANGED
|
@@ -25,9 +25,41 @@ async function retryOnConflict(attempt, maxAttempts = 3) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// ../../modules/core/dist/tokenUsage.js
|
|
28
|
+
function usageForModel(model, tokens) {
|
|
29
|
+
return { ...tokens, byModel: { [model]: tokens } };
|
|
30
|
+
}
|
|
28
31
|
var ZERO_USAGE = { inputTokens: 0, outputTokens: 0 };
|
|
29
32
|
function addTokenUsage(a, b) {
|
|
30
|
-
|
|
33
|
+
const cacheReadTokens = (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0);
|
|
34
|
+
const cacheWriteTokens = (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0);
|
|
35
|
+
const byModel = mergeByModel(a.byModel, b.byModel);
|
|
36
|
+
return {
|
|
37
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
38
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
39
|
+
// Kept absent rather than zero when nothing was cached, so a ledger
|
|
40
|
+
// entry from before caching and one from a run that cached nothing
|
|
41
|
+
// look the same.
|
|
42
|
+
...cacheReadTokens > 0 ? { cacheReadTokens } : {},
|
|
43
|
+
...cacheWriteTokens > 0 ? { cacheWriteTokens } : {},
|
|
44
|
+
// Absent for the same reason: a record with no model attribution and
|
|
45
|
+
// one with an empty map should not read differently.
|
|
46
|
+
...byModel ? { byModel } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function mergeByModel(a, b) {
|
|
50
|
+
if (!a && !b)
|
|
51
|
+
return void 0;
|
|
52
|
+
const merged = { ...a };
|
|
53
|
+
for (const [model, tokens] of Object.entries(b ?? {})) {
|
|
54
|
+
const existing = merged[model];
|
|
55
|
+
merged[model] = existing ? {
|
|
56
|
+
inputTokens: existing.inputTokens + tokens.inputTokens,
|
|
57
|
+
outputTokens: existing.outputTokens + tokens.outputTokens,
|
|
58
|
+
...(existing.cacheReadTokens ?? 0) + (tokens.cacheReadTokens ?? 0) > 0 ? { cacheReadTokens: (existing.cacheReadTokens ?? 0) + (tokens.cacheReadTokens ?? 0) } : {},
|
|
59
|
+
...(existing.cacheWriteTokens ?? 0) + (tokens.cacheWriteTokens ?? 0) > 0 ? { cacheWriteTokens: (existing.cacheWriteTokens ?? 0) + (tokens.cacheWriteTokens ?? 0) } : {}
|
|
60
|
+
} : tokens;
|
|
61
|
+
}
|
|
62
|
+
return merged;
|
|
31
63
|
}
|
|
32
64
|
|
|
33
65
|
// ../../modules/core/dist/rollover.js
|
|
@@ -89,10 +121,10 @@ async function createCompleteMessage(claude, params, { maxContinuations = DEFAUL
|
|
|
89
121
|
const message = requestOptions ? await claude.messages.create({ ...params, messages }, requestOptions) : await claude.messages.create({ ...params, messages });
|
|
90
122
|
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
91
123
|
fullText += text;
|
|
92
|
-
usage = addTokenUsage(usage, {
|
|
124
|
+
usage = addTokenUsage(usage, usageForModel(params.model, {
|
|
93
125
|
inputTokens: message.usage?.input_tokens ?? 0,
|
|
94
126
|
outputTokens: message.usage?.output_tokens ?? 0
|
|
95
|
-
});
|
|
127
|
+
}));
|
|
96
128
|
if (message.stop_reason !== "max_tokens") {
|
|
97
129
|
return { text: fullText, usage };
|
|
98
130
|
}
|
|
@@ -104,19 +136,19 @@ async function createCompleteMessage(claude, params, { maxContinuations = DEFAUL
|
|
|
104
136
|
var MODEL_REGISTRY = {
|
|
105
137
|
fast: {
|
|
106
138
|
description: "Quick, cheap work where depth matters least.",
|
|
107
|
-
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5" }]
|
|
139
|
+
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5", contextTokens: 2e5 }]
|
|
108
140
|
},
|
|
109
141
|
balanced: {
|
|
110
142
|
description: "The everyday tier: chat, log entries, release notes, the daily digest. Frequent enough that cost matters.",
|
|
111
|
-
models: [{ id: "claude-sonnet-5", label: "Sonnet 5" }]
|
|
143
|
+
models: [{ id: "claude-sonnet-5", label: "Sonnet 5", contextTokens: 2e5 }]
|
|
112
144
|
},
|
|
113
145
|
capable: {
|
|
114
146
|
description: "Deeper reasoning for infrequent, high-value work where getting it right outweighs the cost.",
|
|
115
|
-
models: [{ id: "claude-opus-5", label: "Opus 5" }]
|
|
147
|
+
models: [{ id: "claude-opus-5", label: "Opus 5", contextTokens: 2e5 }]
|
|
116
148
|
},
|
|
117
149
|
max: {
|
|
118
150
|
description: "Maximum depth, for long-running agentic work. Opt-in \u2014 the most expensive tier.",
|
|
119
|
-
models: [{ id: "claude-fable-5", label: "Fable 5" }]
|
|
151
|
+
models: [{ id: "claude-fable-5", label: "Fable 5", contextTokens: 2e5 }]
|
|
120
152
|
}
|
|
121
153
|
};
|
|
122
154
|
function defaultModelFor(tier) {
|
|
@@ -232,17 +264,15 @@ function currentRunUrl(env) {
|
|
|
232
264
|
// ../../modules/core/dist/ciEntrypoint.js
|
|
233
265
|
var VAULT_BRANCH = "main";
|
|
234
266
|
function healthParams(env) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (!projectName || !vaultRepo || !token) {
|
|
239
|
-
return null;
|
|
267
|
+
const missing = ["PROJECT_NAME", "VAULT_REPO", "DEVKEEP_GITHUB_TOKEN"].filter((name) => !env[name]);
|
|
268
|
+
if (missing.length > 0) {
|
|
269
|
+
return { missing: [...missing] };
|
|
240
270
|
}
|
|
241
|
-
const [owner, repo] =
|
|
271
|
+
const [owner, repo] = env.VAULT_REPO.split("/");
|
|
242
272
|
if (!owner || !repo) {
|
|
243
|
-
return
|
|
273
|
+
return { missing: ["VAULT_REPO (expected owner/repo)"] };
|
|
244
274
|
}
|
|
245
|
-
return { owner, repo, projectName, branch: VAULT_BRANCH };
|
|
275
|
+
return { params: { owner, repo, projectName: env.PROJECT_NAME, branch: VAULT_BRANCH } };
|
|
246
276
|
}
|
|
247
277
|
var defaultDeps = {
|
|
248
278
|
env: process.env,
|
|
@@ -253,7 +283,11 @@ var defaultDeps = {
|
|
|
253
283
|
exit: (code) => process.exit(code)
|
|
254
284
|
};
|
|
255
285
|
async function runCiEntrypoint(source, main2, deps = defaultDeps) {
|
|
256
|
-
const
|
|
286
|
+
const resolved = healthParams(deps.env);
|
|
287
|
+
const params = "params" in resolved ? resolved.params : null;
|
|
288
|
+
if (!params) {
|
|
289
|
+
deps.logError(`Health not recorded for "${source}": ${resolved.missing.join(", ")} not set.`);
|
|
290
|
+
}
|
|
257
291
|
const octokit = params ? deps.createGithubClient(deps.env.DEVKEEP_GITHUB_TOKEN) : null;
|
|
258
292
|
try {
|
|
259
293
|
await main2();
|
|
@@ -278,7 +312,7 @@ async function runCiEntrypoint(source, main2, deps = defaultDeps) {
|
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
314
|
|
|
281
|
-
// ../../modules/
|
|
315
|
+
// ../../modules/core/dist/ciUsage.js
|
|
282
316
|
var EMPTY_PROJECT_USAGE = {
|
|
283
317
|
draftLogEntry: ZERO_USAGE,
|
|
284
318
|
consolidateRelease: ZERO_USAGE,
|
|
@@ -289,6 +323,7 @@ var EMPTY_PROJECT_USAGE = {
|
|
|
289
323
|
syncOnboarding: ZERO_USAGE,
|
|
290
324
|
releaseNotes: ZERO_USAGE,
|
|
291
325
|
captureBaseline: ZERO_USAGE,
|
|
326
|
+
devseerReview: ZERO_USAGE,
|
|
292
327
|
rollover: ZERO_ROLLOVER_USAGE
|
|
293
328
|
};
|
|
294
329
|
function usagePath(projectName) {
|
|
@@ -315,14 +350,24 @@ async function getUsageFile(octokit, { owner, repo, projectName }) {
|
|
|
315
350
|
}
|
|
316
351
|
}
|
|
317
352
|
function addUsage(current, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
353
|
+
const bySource = current.rolloverBySource ?? {};
|
|
318
354
|
return {
|
|
319
355
|
...current,
|
|
320
356
|
[source]: addTokenUsage(current[source], delta),
|
|
321
|
-
|
|
357
|
+
// Both: the combined bucket stays the total it always was, and the
|
|
358
|
+
// per-source split is what lets the dashboard attribute it.
|
|
359
|
+
rollover: addRolloverUsage(current.rollover, delta, now),
|
|
360
|
+
rolloverBySource: {
|
|
361
|
+
...bySource,
|
|
362
|
+
[source]: addRolloverUsage(bySource[source] ?? ZERO_ROLLOVER_USAGE, delta, now)
|
|
363
|
+
}
|
|
322
364
|
};
|
|
323
365
|
}
|
|
324
366
|
async function recordUsage(octokit, params, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
325
367
|
const { owner, repo, projectName, branch } = params;
|
|
368
|
+
if (delta.inputTokens === 0 && delta.outputTokens === 0) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
326
371
|
await retryOnConflict(async () => {
|
|
327
372
|
const { usage: current, sha } = await getUsageFile(octokit, { owner, repo, projectName });
|
|
328
373
|
const updated = addUsage(current, source, delta, now);
|
|
@@ -501,6 +546,14 @@ function isNotFound3(error) {
|
|
|
501
546
|
|
|
502
547
|
// ../../modules/devlore/dist/prompt.js
|
|
503
548
|
var MAX_DIFF_CHARS = 2e4;
|
|
549
|
+
var SUPERSEDED_GUIDANCE = [
|
|
550
|
+
"These documents record how the thinking changed over time, so they contain claims that were later corrected, and the corrected version is still physically present.",
|
|
551
|
+
"**A correction outranks what it corrects.** Where two passages disagree, the later-dated one is what is true now. A passage struck through, marked superseded, described as decided against, or sitting under a heading that says the earlier view was wrong, records what was believed \u2014 never what is. Do not restate any of it as current."
|
|
552
|
+
].join("\n");
|
|
553
|
+
var SUPERSEDED_GUIDANCE_WITH_LESSONS = [
|
|
554
|
+
SUPERSEDED_GUIDANCE,
|
|
555
|
+
"A reversed decision is still worth telling a newcomer about \u2014 but as something the project learned, never as a rule they should follow. Say what is true now first, and mention the earlier version only where the fact that it changed is itself the lesson."
|
|
556
|
+
].join("\n");
|
|
504
557
|
function buildLogEntryPrompt(input) {
|
|
505
558
|
const diffText = input.files.map((file) => `--- ${file.filename} (${file.status}) ---
|
|
506
559
|
${file.patch}`).join("\n\n");
|
|
@@ -25,9 +25,41 @@ async function retryOnConflict(attempt, maxAttempts = 3) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// ../../modules/core/dist/tokenUsage.js
|
|
28
|
+
function usageForModel(model, tokens) {
|
|
29
|
+
return { ...tokens, byModel: { [model]: tokens } };
|
|
30
|
+
}
|
|
28
31
|
var ZERO_USAGE = { inputTokens: 0, outputTokens: 0 };
|
|
29
32
|
function addTokenUsage(a, b) {
|
|
30
|
-
|
|
33
|
+
const cacheReadTokens = (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0);
|
|
34
|
+
const cacheWriteTokens = (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0);
|
|
35
|
+
const byModel = mergeByModel(a.byModel, b.byModel);
|
|
36
|
+
return {
|
|
37
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
38
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
39
|
+
// Kept absent rather than zero when nothing was cached, so a ledger
|
|
40
|
+
// entry from before caching and one from a run that cached nothing
|
|
41
|
+
// look the same.
|
|
42
|
+
...cacheReadTokens > 0 ? { cacheReadTokens } : {},
|
|
43
|
+
...cacheWriteTokens > 0 ? { cacheWriteTokens } : {},
|
|
44
|
+
// Absent for the same reason: a record with no model attribution and
|
|
45
|
+
// one with an empty map should not read differently.
|
|
46
|
+
...byModel ? { byModel } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function mergeByModel(a, b) {
|
|
50
|
+
if (!a && !b)
|
|
51
|
+
return void 0;
|
|
52
|
+
const merged = { ...a };
|
|
53
|
+
for (const [model, tokens] of Object.entries(b ?? {})) {
|
|
54
|
+
const existing = merged[model];
|
|
55
|
+
merged[model] = existing ? {
|
|
56
|
+
inputTokens: existing.inputTokens + tokens.inputTokens,
|
|
57
|
+
outputTokens: existing.outputTokens + tokens.outputTokens,
|
|
58
|
+
...(existing.cacheReadTokens ?? 0) + (tokens.cacheReadTokens ?? 0) > 0 ? { cacheReadTokens: (existing.cacheReadTokens ?? 0) + (tokens.cacheReadTokens ?? 0) } : {},
|
|
59
|
+
...(existing.cacheWriteTokens ?? 0) + (tokens.cacheWriteTokens ?? 0) > 0 ? { cacheWriteTokens: (existing.cacheWriteTokens ?? 0) + (tokens.cacheWriteTokens ?? 0) } : {}
|
|
60
|
+
} : tokens;
|
|
61
|
+
}
|
|
62
|
+
return merged;
|
|
31
63
|
}
|
|
32
64
|
|
|
33
65
|
// ../../modules/core/dist/rollover.js
|
|
@@ -89,10 +121,10 @@ async function createCompleteMessage(claude, params, { maxContinuations = DEFAUL
|
|
|
89
121
|
const message = requestOptions ? await claude.messages.create({ ...params, messages }, requestOptions) : await claude.messages.create({ ...params, messages });
|
|
90
122
|
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
91
123
|
fullText += text;
|
|
92
|
-
usage = addTokenUsage(usage, {
|
|
124
|
+
usage = addTokenUsage(usage, usageForModel(params.model, {
|
|
93
125
|
inputTokens: message.usage?.input_tokens ?? 0,
|
|
94
126
|
outputTokens: message.usage?.output_tokens ?? 0
|
|
95
|
-
});
|
|
127
|
+
}));
|
|
96
128
|
if (message.stop_reason !== "max_tokens") {
|
|
97
129
|
return { text: fullText, usage };
|
|
98
130
|
}
|
|
@@ -104,19 +136,19 @@ async function createCompleteMessage(claude, params, { maxContinuations = DEFAUL
|
|
|
104
136
|
var MODEL_REGISTRY = {
|
|
105
137
|
fast: {
|
|
106
138
|
description: "Quick, cheap work where depth matters least.",
|
|
107
|
-
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5" }]
|
|
139
|
+
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5", contextTokens: 2e5 }]
|
|
108
140
|
},
|
|
109
141
|
balanced: {
|
|
110
142
|
description: "The everyday tier: chat, log entries, release notes, the daily digest. Frequent enough that cost matters.",
|
|
111
|
-
models: [{ id: "claude-sonnet-5", label: "Sonnet 5" }]
|
|
143
|
+
models: [{ id: "claude-sonnet-5", label: "Sonnet 5", contextTokens: 2e5 }]
|
|
112
144
|
},
|
|
113
145
|
capable: {
|
|
114
146
|
description: "Deeper reasoning for infrequent, high-value work where getting it right outweighs the cost.",
|
|
115
|
-
models: [{ id: "claude-opus-5", label: "Opus 5" }]
|
|
147
|
+
models: [{ id: "claude-opus-5", label: "Opus 5", contextTokens: 2e5 }]
|
|
116
148
|
},
|
|
117
149
|
max: {
|
|
118
150
|
description: "Maximum depth, for long-running agentic work. Opt-in \u2014 the most expensive tier.",
|
|
119
|
-
models: [{ id: "claude-fable-5", label: "Fable 5" }]
|
|
151
|
+
models: [{ id: "claude-fable-5", label: "Fable 5", contextTokens: 2e5 }]
|
|
120
152
|
}
|
|
121
153
|
};
|
|
122
154
|
function defaultModelFor(tier) {
|
|
@@ -232,17 +264,15 @@ function currentRunUrl(env) {
|
|
|
232
264
|
// ../../modules/core/dist/ciEntrypoint.js
|
|
233
265
|
var VAULT_BRANCH = "main";
|
|
234
266
|
function healthParams(env) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (!projectName || !vaultRepo || !token) {
|
|
239
|
-
return null;
|
|
267
|
+
const missing = ["PROJECT_NAME", "VAULT_REPO", "DEVKEEP_GITHUB_TOKEN"].filter((name) => !env[name]);
|
|
268
|
+
if (missing.length > 0) {
|
|
269
|
+
return { missing: [...missing] };
|
|
240
270
|
}
|
|
241
|
-
const [owner, repo] =
|
|
271
|
+
const [owner, repo] = env.VAULT_REPO.split("/");
|
|
242
272
|
if (!owner || !repo) {
|
|
243
|
-
return
|
|
273
|
+
return { missing: ["VAULT_REPO (expected owner/repo)"] };
|
|
244
274
|
}
|
|
245
|
-
return { owner, repo, projectName, branch: VAULT_BRANCH };
|
|
275
|
+
return { params: { owner, repo, projectName: env.PROJECT_NAME, branch: VAULT_BRANCH } };
|
|
246
276
|
}
|
|
247
277
|
var defaultDeps = {
|
|
248
278
|
env: process.env,
|
|
@@ -253,7 +283,11 @@ var defaultDeps = {
|
|
|
253
283
|
exit: (code) => process.exit(code)
|
|
254
284
|
};
|
|
255
285
|
async function runCiEntrypoint(source, main2, deps = defaultDeps) {
|
|
256
|
-
const
|
|
286
|
+
const resolved = healthParams(deps.env);
|
|
287
|
+
const params = "params" in resolved ? resolved.params : null;
|
|
288
|
+
if (!params) {
|
|
289
|
+
deps.logError(`Health not recorded for "${source}": ${resolved.missing.join(", ")} not set.`);
|
|
290
|
+
}
|
|
257
291
|
const octokit = params ? deps.createGithubClient(deps.env.DEVKEEP_GITHUB_TOKEN) : null;
|
|
258
292
|
try {
|
|
259
293
|
await main2();
|
|
@@ -278,30 +312,7 @@ async function runCiEntrypoint(source, main2, deps = defaultDeps) {
|
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
314
|
|
|
281
|
-
// ../../modules/
|
|
282
|
-
function buildReleaseNotesPrompt(input) {
|
|
283
|
-
return [
|
|
284
|
-
`Project: ${input.projectName}`,
|
|
285
|
-
`Release: ${input.tag}`,
|
|
286
|
-
input.previousTag ? `Previous release: ${input.previousTag}. The entries below are the work done since then.` : "This is the first release, so the entries below cover everything so far.",
|
|
287
|
-
"",
|
|
288
|
-
"Below are the decision log entries Devkeep recorded for the work in this release.",
|
|
289
|
-
"",
|
|
290
|
-
...input.logEntries,
|
|
291
|
-
"",
|
|
292
|
-
"Write release notes for the people who use this product. Lead with what they can now do that they could not before, and what changed in behaviour they will notice. Group related changes rather than listing entries one by one.",
|
|
293
|
-
"",
|
|
294
|
-
"Plain language, not commit-history jargon: no file paths, no function names, no internal module names unless the user genuinely interacts with them by name.",
|
|
295
|
-
"",
|
|
296
|
-
'If this release contains nothing user-facing \u2014 internal refactoring, tooling, workflow changes \u2014 say exactly that in a sentence or two. Do not manufacture user-facing significance that is not there; an honest "this release contains internal changes only" is the correct output, and inventing a feature to fill the page is worse than a short note.',
|
|
297
|
-
"",
|
|
298
|
-
"Do NOT include a top-level heading \u2014 one is added automatically. Start directly with the content.",
|
|
299
|
-
"",
|
|
300
|
-
"Return the notes only \u2014 no preamble, no meta-commentary."
|
|
301
|
-
].join("\n");
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ../../modules/devlore/dist/ciUsage.js
|
|
315
|
+
// ../../modules/core/dist/ciUsage.js
|
|
305
316
|
var EMPTY_PROJECT_USAGE = {
|
|
306
317
|
draftLogEntry: ZERO_USAGE,
|
|
307
318
|
consolidateRelease: ZERO_USAGE,
|
|
@@ -312,6 +323,7 @@ var EMPTY_PROJECT_USAGE = {
|
|
|
312
323
|
syncOnboarding: ZERO_USAGE,
|
|
313
324
|
releaseNotes: ZERO_USAGE,
|
|
314
325
|
captureBaseline: ZERO_USAGE,
|
|
326
|
+
devseerReview: ZERO_USAGE,
|
|
315
327
|
rollover: ZERO_ROLLOVER_USAGE
|
|
316
328
|
};
|
|
317
329
|
function usagePath(projectName) {
|
|
@@ -338,14 +350,24 @@ async function getUsageFile(octokit, { owner, repo, projectName }) {
|
|
|
338
350
|
}
|
|
339
351
|
}
|
|
340
352
|
function addUsage(current, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
353
|
+
const bySource = current.rolloverBySource ?? {};
|
|
341
354
|
return {
|
|
342
355
|
...current,
|
|
343
356
|
[source]: addTokenUsage(current[source], delta),
|
|
344
|
-
|
|
357
|
+
// Both: the combined bucket stays the total it always was, and the
|
|
358
|
+
// per-source split is what lets the dashboard attribute it.
|
|
359
|
+
rollover: addRolloverUsage(current.rollover, delta, now),
|
|
360
|
+
rolloverBySource: {
|
|
361
|
+
...bySource,
|
|
362
|
+
[source]: addRolloverUsage(bySource[source] ?? ZERO_ROLLOVER_USAGE, delta, now)
|
|
363
|
+
}
|
|
345
364
|
};
|
|
346
365
|
}
|
|
347
366
|
async function recordUsage(octokit, params, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
348
367
|
const { owner, repo, projectName, branch } = params;
|
|
368
|
+
if (delta.inputTokens === 0 && delta.outputTokens === 0) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
349
371
|
await retryOnConflict(async () => {
|
|
350
372
|
const { usage: current, sha } = await getUsageFile(octokit, { owner, repo, projectName });
|
|
351
373
|
const updated = addUsage(current, source, delta, now);
|
|
@@ -364,6 +386,37 @@ function isNotFound(error) {
|
|
|
364
386
|
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
365
387
|
}
|
|
366
388
|
|
|
389
|
+
// ../../modules/devlore/dist/prompt.js
|
|
390
|
+
var SUPERSEDED_GUIDANCE = [
|
|
391
|
+
"These documents record how the thinking changed over time, so they contain claims that were later corrected, and the corrected version is still physically present.",
|
|
392
|
+
"**A correction outranks what it corrects.** Where two passages disagree, the later-dated one is what is true now. A passage struck through, marked superseded, described as decided against, or sitting under a heading that says the earlier view was wrong, records what was believed \u2014 never what is. Do not restate any of it as current."
|
|
393
|
+
].join("\n");
|
|
394
|
+
var SUPERSEDED_GUIDANCE_WITH_LESSONS = [
|
|
395
|
+
SUPERSEDED_GUIDANCE,
|
|
396
|
+
"A reversed decision is still worth telling a newcomer about \u2014 but as something the project learned, never as a rule they should follow. Say what is true now first, and mention the earlier version only where the fact that it changed is itself the lesson."
|
|
397
|
+
].join("\n");
|
|
398
|
+
function buildReleaseNotesPrompt(input) {
|
|
399
|
+
return [
|
|
400
|
+
`Project: ${input.projectName}`,
|
|
401
|
+
`Release: ${input.tag}`,
|
|
402
|
+
input.previousTag ? `Previous release: ${input.previousTag}. The entries below are the work done since then.` : "This is the first release, so the entries below cover everything so far.",
|
|
403
|
+
"",
|
|
404
|
+
"Below are the decision log entries Devkeep recorded for the work in this release.",
|
|
405
|
+
"",
|
|
406
|
+
...input.logEntries,
|
|
407
|
+
"",
|
|
408
|
+
"Write release notes for the people who use this product. Lead with what they can now do that they could not before, and what changed in behaviour they will notice. Group related changes rather than listing entries one by one.",
|
|
409
|
+
"",
|
|
410
|
+
"Plain language, not commit-history jargon: no file paths, no function names, no internal module names unless the user genuinely interacts with them by name.",
|
|
411
|
+
"",
|
|
412
|
+
'If this release contains nothing user-facing \u2014 internal refactoring, tooling, workflow changes \u2014 say exactly that in a sentence or two. Do not manufacture user-facing significance that is not there; an honest "this release contains internal changes only" is the correct output, and inventing a feature to fill the page is worse than a short note.',
|
|
413
|
+
"",
|
|
414
|
+
"Do NOT include a top-level heading \u2014 one is added automatically. Start directly with the content.",
|
|
415
|
+
"",
|
|
416
|
+
"Return the notes only \u2014 no preamble, no meta-commentary."
|
|
417
|
+
].join("\n");
|
|
418
|
+
}
|
|
419
|
+
|
|
367
420
|
// ../../modules/devlore/dist/logEntries.js
|
|
368
421
|
function logEntriesDir(projectName) {
|
|
369
422
|
return `projects/${projectName}/log`;
|
package/dist/sync-onboarding.js
CHANGED
|
@@ -25,9 +25,41 @@ async function retryOnConflict(attempt, maxAttempts = 3) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// ../../modules/core/dist/tokenUsage.js
|
|
28
|
+
function usageForModel(model, tokens) {
|
|
29
|
+
return { ...tokens, byModel: { [model]: tokens } };
|
|
30
|
+
}
|
|
28
31
|
var ZERO_USAGE = { inputTokens: 0, outputTokens: 0 };
|
|
29
32
|
function addTokenUsage(a, b) {
|
|
30
|
-
|
|
33
|
+
const cacheReadTokens = (a.cacheReadTokens ?? 0) + (b.cacheReadTokens ?? 0);
|
|
34
|
+
const cacheWriteTokens = (a.cacheWriteTokens ?? 0) + (b.cacheWriteTokens ?? 0);
|
|
35
|
+
const byModel = mergeByModel(a.byModel, b.byModel);
|
|
36
|
+
return {
|
|
37
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
38
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
39
|
+
// Kept absent rather than zero when nothing was cached, so a ledger
|
|
40
|
+
// entry from before caching and one from a run that cached nothing
|
|
41
|
+
// look the same.
|
|
42
|
+
...cacheReadTokens > 0 ? { cacheReadTokens } : {},
|
|
43
|
+
...cacheWriteTokens > 0 ? { cacheWriteTokens } : {},
|
|
44
|
+
// Absent for the same reason: a record with no model attribution and
|
|
45
|
+
// one with an empty map should not read differently.
|
|
46
|
+
...byModel ? { byModel } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function mergeByModel(a, b) {
|
|
50
|
+
if (!a && !b)
|
|
51
|
+
return void 0;
|
|
52
|
+
const merged = { ...a };
|
|
53
|
+
for (const [model, tokens] of Object.entries(b ?? {})) {
|
|
54
|
+
const existing = merged[model];
|
|
55
|
+
merged[model] = existing ? {
|
|
56
|
+
inputTokens: existing.inputTokens + tokens.inputTokens,
|
|
57
|
+
outputTokens: existing.outputTokens + tokens.outputTokens,
|
|
58
|
+
...(existing.cacheReadTokens ?? 0) + (tokens.cacheReadTokens ?? 0) > 0 ? { cacheReadTokens: (existing.cacheReadTokens ?? 0) + (tokens.cacheReadTokens ?? 0) } : {},
|
|
59
|
+
...(existing.cacheWriteTokens ?? 0) + (tokens.cacheWriteTokens ?? 0) > 0 ? { cacheWriteTokens: (existing.cacheWriteTokens ?? 0) + (tokens.cacheWriteTokens ?? 0) } : {}
|
|
60
|
+
} : tokens;
|
|
61
|
+
}
|
|
62
|
+
return merged;
|
|
31
63
|
}
|
|
32
64
|
|
|
33
65
|
// ../../modules/core/dist/rollover.js
|
|
@@ -89,10 +121,10 @@ async function createCompleteMessage(claude, params, { maxContinuations = DEFAUL
|
|
|
89
121
|
const message = requestOptions ? await claude.messages.create({ ...params, messages }, requestOptions) : await claude.messages.create({ ...params, messages });
|
|
90
122
|
const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
91
123
|
fullText += text;
|
|
92
|
-
usage = addTokenUsage(usage, {
|
|
124
|
+
usage = addTokenUsage(usage, usageForModel(params.model, {
|
|
93
125
|
inputTokens: message.usage?.input_tokens ?? 0,
|
|
94
126
|
outputTokens: message.usage?.output_tokens ?? 0
|
|
95
|
-
});
|
|
127
|
+
}));
|
|
96
128
|
if (message.stop_reason !== "max_tokens") {
|
|
97
129
|
return { text: fullText, usage };
|
|
98
130
|
}
|
|
@@ -104,19 +136,19 @@ async function createCompleteMessage(claude, params, { maxContinuations = DEFAUL
|
|
|
104
136
|
var MODEL_REGISTRY = {
|
|
105
137
|
fast: {
|
|
106
138
|
description: "Quick, cheap work where depth matters least.",
|
|
107
|
-
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5" }]
|
|
139
|
+
models: [{ id: "claude-haiku-4-5-20251001", label: "Haiku 4.5", contextTokens: 2e5 }]
|
|
108
140
|
},
|
|
109
141
|
balanced: {
|
|
110
142
|
description: "The everyday tier: chat, log entries, release notes, the daily digest. Frequent enough that cost matters.",
|
|
111
|
-
models: [{ id: "claude-sonnet-5", label: "Sonnet 5" }]
|
|
143
|
+
models: [{ id: "claude-sonnet-5", label: "Sonnet 5", contextTokens: 2e5 }]
|
|
112
144
|
},
|
|
113
145
|
capable: {
|
|
114
146
|
description: "Deeper reasoning for infrequent, high-value work where getting it right outweighs the cost.",
|
|
115
|
-
models: [{ id: "claude-opus-5", label: "Opus 5" }]
|
|
147
|
+
models: [{ id: "claude-opus-5", label: "Opus 5", contextTokens: 2e5 }]
|
|
116
148
|
},
|
|
117
149
|
max: {
|
|
118
150
|
description: "Maximum depth, for long-running agentic work. Opt-in \u2014 the most expensive tier.",
|
|
119
|
-
models: [{ id: "claude-fable-5", label: "Fable 5" }]
|
|
151
|
+
models: [{ id: "claude-fable-5", label: "Fable 5", contextTokens: 2e5 }]
|
|
120
152
|
}
|
|
121
153
|
};
|
|
122
154
|
function defaultModelFor(tier) {
|
|
@@ -232,17 +264,15 @@ function currentRunUrl(env) {
|
|
|
232
264
|
// ../../modules/core/dist/ciEntrypoint.js
|
|
233
265
|
var VAULT_BRANCH = "main";
|
|
234
266
|
function healthParams(env) {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (!projectName || !vaultRepo || !token) {
|
|
239
|
-
return null;
|
|
267
|
+
const missing = ["PROJECT_NAME", "VAULT_REPO", "DEVKEEP_GITHUB_TOKEN"].filter((name) => !env[name]);
|
|
268
|
+
if (missing.length > 0) {
|
|
269
|
+
return { missing: [...missing] };
|
|
240
270
|
}
|
|
241
|
-
const [owner, repo] =
|
|
271
|
+
const [owner, repo] = env.VAULT_REPO.split("/");
|
|
242
272
|
if (!owner || !repo) {
|
|
243
|
-
return
|
|
273
|
+
return { missing: ["VAULT_REPO (expected owner/repo)"] };
|
|
244
274
|
}
|
|
245
|
-
return { owner, repo, projectName, branch: VAULT_BRANCH };
|
|
275
|
+
return { params: { owner, repo, projectName: env.PROJECT_NAME, branch: VAULT_BRANCH } };
|
|
246
276
|
}
|
|
247
277
|
var defaultDeps = {
|
|
248
278
|
env: process.env,
|
|
@@ -253,7 +283,11 @@ var defaultDeps = {
|
|
|
253
283
|
exit: (code) => process.exit(code)
|
|
254
284
|
};
|
|
255
285
|
async function runCiEntrypoint(source, main2, deps = defaultDeps) {
|
|
256
|
-
const
|
|
286
|
+
const resolved = healthParams(deps.env);
|
|
287
|
+
const params = "params" in resolved ? resolved.params : null;
|
|
288
|
+
if (!params) {
|
|
289
|
+
deps.logError(`Health not recorded for "${source}": ${resolved.missing.join(", ")} not set.`);
|
|
290
|
+
}
|
|
257
291
|
const octokit = params ? deps.createGithubClient(deps.env.DEVKEEP_GITHUB_TOKEN) : null;
|
|
258
292
|
try {
|
|
259
293
|
await main2();
|
|
@@ -278,66 +312,7 @@ async function runCiEntrypoint(source, main2, deps = defaultDeps) {
|
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
314
|
|
|
281
|
-
// ../../modules/
|
|
282
|
-
function decisionEntriesDir(projectName) {
|
|
283
|
-
return `projects/${projectName}/decisions`;
|
|
284
|
-
}
|
|
285
|
-
async function listDecisionEntries(octokit, { owner, repo, projectName }) {
|
|
286
|
-
const dir = decisionEntriesDir(projectName);
|
|
287
|
-
let files;
|
|
288
|
-
try {
|
|
289
|
-
const { data } = await octokit.rest.repos.getContent({ owner, repo, path: dir });
|
|
290
|
-
if (!Array.isArray(data)) {
|
|
291
|
-
return [];
|
|
292
|
-
}
|
|
293
|
-
files = data;
|
|
294
|
-
} catch (error) {
|
|
295
|
-
if (isNotFound(error)) {
|
|
296
|
-
return [];
|
|
297
|
-
}
|
|
298
|
-
throw error;
|
|
299
|
-
}
|
|
300
|
-
const entries = await Promise.all(files.filter((file) => file.type === "file").map(async (file) => {
|
|
301
|
-
const { data } = await octokit.rest.repos.getContent({ owner, repo, path: file.path });
|
|
302
|
-
if (Array.isArray(data) || data.type !== "file") {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
return { path: file.path, content: Buffer.from(data.content, "base64").toString("utf-8") };
|
|
306
|
-
}));
|
|
307
|
-
return entries.filter((entry) => entry !== null).sort((a, b) => a.path.localeCompare(b.path));
|
|
308
|
-
}
|
|
309
|
-
function isNotFound(error) {
|
|
310
|
-
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ../../modules/devlore/dist/prompt.js
|
|
314
|
-
function buildOnboardingGuidePrompt(input) {
|
|
315
|
-
return [
|
|
316
|
-
`Project: ${input.projectName}`,
|
|
317
|
-
"",
|
|
318
|
-
'Write a "start here" guide for a developer joining this project. Not an end-user manual, and not a changelog \u2014 an orientation for someone about to read and change the code.',
|
|
319
|
-
"",
|
|
320
|
-
input.currentState ? `--- Current state ---
|
|
321
|
-
${input.currentState}` : "",
|
|
322
|
-
input.canonicalDocs ? `--- Canonical docs ---
|
|
323
|
-
${input.canonicalDocs}` : "",
|
|
324
|
-
input.decisions.length > 0 ? `--- Decisions ---
|
|
325
|
-
${input.decisions.join("\n\n")}` : "",
|
|
326
|
-
"",
|
|
327
|
-
"Cover, in whatever order serves a newcomer best:",
|
|
328
|
-
"",
|
|
329
|
-
"- Where the important logic lives, and what to read first.",
|
|
330
|
-
"- Why the significant past decisions were made \u2014 a newcomer who does not know the reasoning will undo it.",
|
|
331
|
-
"- Common gotchas: the things that look wrong but are deliberate, and the mistakes this codebase has actually made before.",
|
|
332
|
-
"- How to get from a clean checkout to a working change.",
|
|
333
|
-
"",
|
|
334
|
-
"Draw only on what the documents above actually say. Where they do not cover something a newcomer would need, say so plainly rather than inventing a plausible answer \u2014 this guide is read by someone with no way to tell the difference.",
|
|
335
|
-
"",
|
|
336
|
-
"Return the guide only \u2014 no preamble, no meta-commentary."
|
|
337
|
-
].filter((line) => line !== "").join("\n");
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// ../../modules/devlore/dist/ciUsage.js
|
|
315
|
+
// ../../modules/core/dist/ciUsage.js
|
|
341
316
|
var EMPTY_PROJECT_USAGE = {
|
|
342
317
|
draftLogEntry: ZERO_USAGE,
|
|
343
318
|
consolidateRelease: ZERO_USAGE,
|
|
@@ -348,6 +323,7 @@ var EMPTY_PROJECT_USAGE = {
|
|
|
348
323
|
syncOnboarding: ZERO_USAGE,
|
|
349
324
|
releaseNotes: ZERO_USAGE,
|
|
350
325
|
captureBaseline: ZERO_USAGE,
|
|
326
|
+
devseerReview: ZERO_USAGE,
|
|
351
327
|
rollover: ZERO_ROLLOVER_USAGE
|
|
352
328
|
};
|
|
353
329
|
function usagePath(projectName) {
|
|
@@ -367,21 +343,31 @@ async function getUsageFile(octokit, { owner, repo, projectName }) {
|
|
|
367
343
|
const usage = { ...EMPTY_PROJECT_USAGE, ...parsed };
|
|
368
344
|
return { usage, sha: data.sha };
|
|
369
345
|
} catch (error) {
|
|
370
|
-
if (
|
|
346
|
+
if (isNotFound(error)) {
|
|
371
347
|
return { usage: EMPTY_PROJECT_USAGE };
|
|
372
348
|
}
|
|
373
349
|
throw error;
|
|
374
350
|
}
|
|
375
351
|
}
|
|
376
352
|
function addUsage(current, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
353
|
+
const bySource = current.rolloverBySource ?? {};
|
|
377
354
|
return {
|
|
378
355
|
...current,
|
|
379
356
|
[source]: addTokenUsage(current[source], delta),
|
|
380
|
-
|
|
357
|
+
// Both: the combined bucket stays the total it always was, and the
|
|
358
|
+
// per-source split is what lets the dashboard attribute it.
|
|
359
|
+
rollover: addRolloverUsage(current.rollover, delta, now),
|
|
360
|
+
rolloverBySource: {
|
|
361
|
+
...bySource,
|
|
362
|
+
[source]: addRolloverUsage(bySource[source] ?? ZERO_ROLLOVER_USAGE, delta, now)
|
|
363
|
+
}
|
|
381
364
|
};
|
|
382
365
|
}
|
|
383
366
|
async function recordUsage(octokit, params, source, delta, now = /* @__PURE__ */ new Date()) {
|
|
384
367
|
const { owner, repo, projectName, branch } = params;
|
|
368
|
+
if (delta.inputTokens === 0 && delta.outputTokens === 0) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
385
371
|
await retryOnConflict(async () => {
|
|
386
372
|
const { usage: current, sha } = await getUsageFile(octokit, { owner, repo, projectName });
|
|
387
373
|
const updated = addUsage(current, source, delta, now);
|
|
@@ -396,10 +382,78 @@ async function recordUsage(octokit, params, source, delta, now = /* @__PURE__ */
|
|
|
396
382
|
});
|
|
397
383
|
});
|
|
398
384
|
}
|
|
385
|
+
function isNotFound(error) {
|
|
386
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ../../modules/devlore/dist/decisionEntries.js
|
|
390
|
+
function decisionEntriesDir(projectName) {
|
|
391
|
+
return `projects/${projectName}/decisions`;
|
|
392
|
+
}
|
|
393
|
+
async function listDecisionEntries(octokit, { owner, repo, projectName }) {
|
|
394
|
+
const dir = decisionEntriesDir(projectName);
|
|
395
|
+
let files;
|
|
396
|
+
try {
|
|
397
|
+
const { data } = await octokit.rest.repos.getContent({ owner, repo, path: dir });
|
|
398
|
+
if (!Array.isArray(data)) {
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
files = data;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (isNotFound2(error)) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
throw error;
|
|
407
|
+
}
|
|
408
|
+
const entries = await Promise.all(files.filter((file) => file.type === "file").map(async (file) => {
|
|
409
|
+
const { data } = await octokit.rest.repos.getContent({ owner, repo, path: file.path });
|
|
410
|
+
if (Array.isArray(data) || data.type !== "file") {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
return { path: file.path, content: Buffer.from(data.content, "base64").toString("utf-8") };
|
|
414
|
+
}));
|
|
415
|
+
return entries.filter((entry) => entry !== null).sort((a, b) => a.path.localeCompare(b.path));
|
|
416
|
+
}
|
|
399
417
|
function isNotFound2(error) {
|
|
400
418
|
return typeof error === "object" && error !== null && "status" in error && error.status === 404;
|
|
401
419
|
}
|
|
402
420
|
|
|
421
|
+
// ../../modules/devlore/dist/prompt.js
|
|
422
|
+
var SUPERSEDED_GUIDANCE = [
|
|
423
|
+
"These documents record how the thinking changed over time, so they contain claims that were later corrected, and the corrected version is still physically present.",
|
|
424
|
+
"**A correction outranks what it corrects.** Where two passages disagree, the later-dated one is what is true now. A passage struck through, marked superseded, described as decided against, or sitting under a heading that says the earlier view was wrong, records what was believed \u2014 never what is. Do not restate any of it as current."
|
|
425
|
+
].join("\n");
|
|
426
|
+
var SUPERSEDED_GUIDANCE_WITH_LESSONS = [
|
|
427
|
+
SUPERSEDED_GUIDANCE,
|
|
428
|
+
"A reversed decision is still worth telling a newcomer about \u2014 but as something the project learned, never as a rule they should follow. Say what is true now first, and mention the earlier version only where the fact that it changed is itself the lesson."
|
|
429
|
+
].join("\n");
|
|
430
|
+
function buildOnboardingGuidePrompt(input) {
|
|
431
|
+
return [
|
|
432
|
+
`Project: ${input.projectName}`,
|
|
433
|
+
"",
|
|
434
|
+
'Write a "start here" guide for a developer joining this project. Not an end-user manual, and not a changelog \u2014 an orientation for someone about to read and change the code.',
|
|
435
|
+
"",
|
|
436
|
+
input.currentState ? `--- Current state ---
|
|
437
|
+
${input.currentState}` : "",
|
|
438
|
+
input.canonicalDocs ? `--- Canonical docs ---
|
|
439
|
+
${input.canonicalDocs}` : "",
|
|
440
|
+
input.decisions.length > 0 ? `--- Decisions ---
|
|
441
|
+
${input.decisions.join("\n\n")}` : "",
|
|
442
|
+
"",
|
|
443
|
+
"Cover, in whatever order serves a newcomer best:",
|
|
444
|
+
"",
|
|
445
|
+
"- Where the important logic lives, and what to read first.",
|
|
446
|
+
"- Why the significant past decisions were made \u2014 a newcomer who does not know the reasoning will undo it.",
|
|
447
|
+
"- Common gotchas: the things that look wrong but are deliberate, and the mistakes this codebase has actually made before.",
|
|
448
|
+
"- How to get from a clean checkout to a working change.",
|
|
449
|
+
"",
|
|
450
|
+
"Draw only on what the documents above actually say. Where they do not cover something a newcomer would need, say so plainly rather than inventing a plausible answer \u2014 this guide is read by someone with no way to tell the difference.",
|
|
451
|
+
SUPERSEDED_GUIDANCE_WITH_LESSONS,
|
|
452
|
+
"",
|
|
453
|
+
"Return the guide only \u2014 no preamble, no meta-commentary."
|
|
454
|
+
].filter((line) => line !== "").join("\n");
|
|
455
|
+
}
|
|
456
|
+
|
|
403
457
|
// ../../modules/devlore/dist/canonicalDocs.js
|
|
404
458
|
function canonicalDocsPath(projectName) {
|
|
405
459
|
return `projects/${projectName}/canonical/README.md`;
|
|
@@ -509,11 +563,14 @@ async function syncOnboarding({ projectOctokit, vaultOctokit, claude, projectOwn
|
|
|
509
563
|
${text.trim()}
|
|
510
564
|
`;
|
|
511
565
|
const existing = await getExistingOnboarding(projectOctokit, { owner: projectOwner, repo: projectRepoName });
|
|
566
|
+
const releaseTag = releaseTagFromEnvironment();
|
|
567
|
+
const wrote = existing ? "Update" : "Add";
|
|
568
|
+
const message = releaseTag ? `${wrote} onboarding guide (release ${releaseTag})` : `${wrote} onboarding guide`;
|
|
512
569
|
await projectOctokit.rest.repos.createOrUpdateFileContents({
|
|
513
570
|
owner: projectOwner,
|
|
514
571
|
repo: projectRepoName,
|
|
515
572
|
path: ONBOARDING_PATH,
|
|
516
|
-
message
|
|
573
|
+
message,
|
|
517
574
|
content: Buffer.from(content, "utf-8").toString("base64"),
|
|
518
575
|
sha: existing?.sha
|
|
519
576
|
});
|
|
@@ -529,7 +586,7 @@ ${text.trim()}
|
|
|
529
586
|
owner: vaultOwner,
|
|
530
587
|
repo: vaultRepoName,
|
|
531
588
|
path: vaultPath,
|
|
532
|
-
message: `Mirror onboarding guide: ${projectName}`,
|
|
589
|
+
message: releaseTag ? `Mirror onboarding guide: ${projectName} (release ${releaseTag})` : `Mirror onboarding guide: ${projectName}`,
|
|
533
590
|
content: Buffer.from(mirrored, "utf-8").toString("base64"),
|
|
534
591
|
branch: VAULT_BRANCH2,
|
|
535
592
|
sha: existingVault.sha
|
|
@@ -540,6 +597,12 @@ ${text.trim()}
|
|
|
540
597
|
}
|
|
541
598
|
return { path: ONBOARDING_PATH, vaultPath, content, usage };
|
|
542
599
|
}
|
|
600
|
+
function releaseTagFromEnvironment() {
|
|
601
|
+
if (process.env.GITHUB_REF_TYPE !== "tag")
|
|
602
|
+
return null;
|
|
603
|
+
const name = process.env.GITHUB_REF_NAME?.trim();
|
|
604
|
+
return name ? name : null;
|
|
605
|
+
}
|
|
543
606
|
|
|
544
607
|
// ../../modules/devlore/dist/bin/syncOnboardingCli.js
|
|
545
608
|
function requireEnv(env, name) {
|