kibi-opencode 0.14.0 → 0.15.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.
Files changed (62) hide show
  1. package/README.md +18 -12
  2. package/dist/config.d.ts +0 -3
  3. package/dist/config.js +0 -9
  4. package/dist/e2e-coverage-signals.js +1 -3
  5. package/dist/enforcement-policy.d.ts +2 -0
  6. package/dist/enforcement-policy.js +18 -3
  7. package/dist/enforcement-scope.js +1 -3
  8. package/dist/file-entity-links.js +2 -6
  9. package/dist/file-operation-reminders.d.ts +4 -4
  10. package/dist/file-operation-state.js +4 -2
  11. package/dist/init-kibi-alias.js +0 -1
  12. package/dist/init-kibi-capability.js +1 -5
  13. package/dist/kb-freshness-state.d.ts +62 -0
  14. package/dist/kb-freshness-state.js +215 -0
  15. package/dist/kibi-checkpoint-runner.d.ts +1 -1
  16. package/dist/kibi-checkpoint-runner.js +6 -3
  17. package/dist/knowledge-classifier.js +2 -1
  18. package/dist/meaningful-change-classifier.d.ts +30 -0
  19. package/dist/meaningful-change-classifier.js +103 -0
  20. package/dist/plugin-startup.js +0 -1
  21. package/dist/plugin.js +101 -391
  22. package/dist/prompt.d.ts +6 -5
  23. package/dist/prompt.js +48 -84
  24. package/dist/reconcile-engine.d.ts +15 -1
  25. package/dist/reconcile-engine.js +1 -3
  26. package/dist/scheduler.js +16 -4
  27. package/dist/session-edit-state.js +2 -1
  28. package/dist/source-linked-guidance.js +3 -1
  29. package/dist/startup-notifier.d.ts +7 -1
  30. package/dist/startup-notifier.js +51 -17
  31. package/dist/tui.js +1 -403
  32. package/dist/tui.jsx +1 -113
  33. package/dist/version-metadata.d.ts +13 -0
  34. package/dist/version-metadata.js +78 -0
  35. package/dist/version-metadata.json +1 -0
  36. package/package.json +5 -7
  37. package/dist/brief-delivery-reasons.d.ts +0 -12
  38. package/dist/brief-delivery-reasons.js +0 -132
  39. package/dist/brief-intent.d.ts +0 -41
  40. package/dist/brief-intent.js +0 -142
  41. package/dist/briefing-runtime.d.ts +0 -24
  42. package/dist/briefing-runtime.js +0 -277
  43. package/dist/graph-narrator.d.ts +0 -25
  44. package/dist/graph-narrator.js +0 -408
  45. package/dist/idle-brief-audit.d.ts +0 -36
  46. package/dist/idle-brief-audit.js +0 -186
  47. package/dist/idle-brief-paths.d.ts +0 -6
  48. package/dist/idle-brief-paths.js +0 -120
  49. package/dist/idle-brief-reader.d.ts +0 -37
  50. package/dist/idle-brief-reader.js +0 -163
  51. package/dist/idle-brief-runtime.d.ts +0 -55
  52. package/dist/idle-brief-runtime.js +0 -496
  53. package/dist/idle-brief-store.d.ts +0 -113
  54. package/dist/idle-brief-store.js +0 -262
  55. package/dist/session-fingerprint.d.ts +0 -11
  56. package/dist/session-fingerprint.js +0 -21
  57. package/dist/tui-brief-delivery.d.ts +0 -67
  58. package/dist/tui-brief-delivery.js +0 -279
  59. package/dist/tui-brief-view-model.d.ts +0 -63
  60. package/dist/tui-brief-view-model.js +0 -209
  61. package/dist/utils/brief-marker.d.ts +0 -19
  62. package/dist/utils/brief-marker.js +0 -101
@@ -1,408 +0,0 @@
1
- // implements REQ-opencode-kibi-briefing-v6
2
- function asRecord(value) {
3
- return typeof value === "object" && value !== null
4
- ? value
5
- : null;
6
- }
7
- function asString(value) {
8
- return typeof value === "string" ? value : "";
9
- }
10
- function asStringArray(value) {
11
- return Array.isArray(value)
12
- ? value.filter((entry) => typeof entry === "string")
13
- : [];
14
- }
15
- function getSessionApi(client) {
16
- const root = asRecord(client);
17
- const session = asRecord(root?.session);
18
- if (!session) {
19
- return null;
20
- }
21
- const create = session.create;
22
- const prompt = session.prompt;
23
- if (typeof create !== "function" || typeof prompt !== "function") {
24
- return null;
25
- }
26
- return {
27
- create: create,
28
- prompt: prompt,
29
- };
30
- }
31
- function extractSessionId(response) {
32
- const root = asRecord(response);
33
- if (!root) {
34
- return null;
35
- }
36
- const directId = asString(root.id).trim();
37
- if (directId) {
38
- return directId;
39
- }
40
- const data = asRecord(root.data);
41
- return asString(data?.id).trim() || null;
42
- }
43
- function extractPromptResponseJson(response) {
44
- const root = asRecord(response);
45
- if (!root)
46
- return null;
47
- const data = asRecord(root.data);
48
- const parts = Array.isArray(data?.parts)
49
- ? data.parts
50
- : Array.isArray(root.parts)
51
- ? root.parts
52
- : null;
53
- if (!parts)
54
- return null;
55
- for (const part of parts) {
56
- const record = asRecord(part);
57
- if (record?.type !== "text") {
58
- continue;
59
- }
60
- const text = asString(record.text);
61
- if (!text) {
62
- continue;
63
- }
64
- try {
65
- return JSON.parse(text);
66
- }
67
- catch {
68
- return null;
69
- }
70
- }
71
- return null;
72
- }
73
- async function invokeTool(sessionApi, sessionID, tool, args) {
74
- const response = await sessionApi.prompt({
75
- sessionID,
76
- parts: [{ type: "text", text: JSON.stringify({ tool, args }) }],
77
- tools: { [tool]: true },
78
- format: {
79
- type: "json_schema",
80
- schema: {
81
- type: ["object", "array"],
82
- },
83
- },
84
- });
85
- return extractPromptResponseJson(response);
86
- }
87
- function inferTypeFromId(id) {
88
- const prefix = id.split("-")[0]?.toLowerCase() ?? "entity";
89
- switch (prefix) {
90
- case "req":
91
- case "scenario":
92
- case "test":
93
- case "adr":
94
- case "flag":
95
- case "event":
96
- case "symbol":
97
- case "fact":
98
- return prefix;
99
- case "scen":
100
- return "scenario";
101
- case "sym":
102
- return "symbol";
103
- default:
104
- return prefix;
105
- }
106
- }
107
- function humanizeType(type, count = 1) {
108
- const singular = {
109
- req: "requirement",
110
- scenario: "scenario",
111
- test: "test",
112
- adr: "ADR",
113
- flag: "flag",
114
- event: "event",
115
- symbol: "symbol",
116
- fact: "fact",
117
- }[type] ?? type;
118
- if (count === 1) {
119
- return singular;
120
- }
121
- if (singular === "ADR") {
122
- return "ADRs";
123
- }
124
- return singular.endsWith("s") ? singular : `${singular}s`;
125
- }
126
- function humanizeDomain(domain) {
127
- const normalized = domain.trim().toLowerCase();
128
- switch (normalized) {
129
- case "opencode":
130
- return "OpenCode";
131
- case "mcp":
132
- return "MCP";
133
- case "cli":
134
- return "CLI";
135
- case "vscode":
136
- return "VSCode";
137
- case "core":
138
- return "Core";
139
- case "requirements":
140
- return "Requirements";
141
- default:
142
- return normalized ? normalized.charAt(0).toUpperCase() + normalized.slice(1) : "Changes";
143
- }
144
- }
145
- function formatList(items) {
146
- if (items.length === 0)
147
- return "";
148
- if (items.length === 1)
149
- return items[0] ?? "";
150
- if (items.length === 2)
151
- return `${items[0]} and ${items[1]}`;
152
- return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
153
- }
154
- function normalizeEntity(record, idFallback) {
155
- const properties = asRecord(record.properties) ?? {};
156
- const id = asString(record.id || properties.id).trim() || idFallback;
157
- const type = asString(record.type).trim() ||
158
- asString(record.entityType).trim() ||
159
- inferTypeFromId(id);
160
- const title = asString(record.title).trim() ||
161
- asString(properties.title).trim() ||
162
- id;
163
- const status = asString(record.status).trim() ||
164
- asString(properties.status).trim() ||
165
- asString(properties.change_kind).trim();
166
- const source = asString(record.source).trim() ||
167
- asString(properties.source).trim();
168
- const tags = [
169
- ...asStringArray(record.tags),
170
- ...asStringArray(properties.tags),
171
- ];
172
- const factKind = asString(record.fact_kind).trim() ||
173
- asString(properties.fact_kind).trim();
174
- return {
175
- id,
176
- type,
177
- title,
178
- status,
179
- source,
180
- tags,
181
- factKind,
182
- removed: false,
183
- };
184
- }
185
- function normalizeGraph(value) {
186
- const root = asRecord(value);
187
- if (!root) {
188
- return null;
189
- }
190
- const edges = Array.isArray(root.edges)
191
- ? root.edges
192
- .map((edge) => {
193
- const record = asRecord(edge);
194
- if (!record)
195
- return null;
196
- const from = asString(record.from).trim();
197
- const to = asString(record.to).trim();
198
- const type = asString(record.type).trim();
199
- return from && to && type ? { from, to, type } : null;
200
- })
201
- .filter((edge) => edge !== null)
202
- : [];
203
- return { edges };
204
- }
205
- function describeEntity(entity, id) {
206
- if (!entity) {
207
- return id;
208
- }
209
- return `${entity.title} (${entity.id})`;
210
- }
211
- function inferDomain(entity) {
212
- const source = entity.source.replaceAll("\\", "/");
213
- const packageMatch = source.match(/packages\/([^/]+)\//);
214
- if (packageMatch?.[1]) {
215
- return humanizeDomain(packageMatch[1]);
216
- }
217
- const tagDomain = entity.tags.find((tag) => ["cli", "mcp", "vscode", "opencode", "core"].includes(tag.toLowerCase()));
218
- if (tagDomain) {
219
- return humanizeDomain(tagDomain);
220
- }
221
- if (["req", "scenario", "test"].includes(entity.type)) {
222
- return "Requirements";
223
- }
224
- return humanizeDomain(entity.type);
225
- }
226
- function classifyRelationship(relationship, entities) {
227
- const fromEntity = entities.get(relationship.from);
228
- const toEntity = entities.get(relationship.to);
229
- const fromText = describeEntity(fromEntity, relationship.from);
230
- const toText = describeEntity(toEntity, relationship.to);
231
- switch (relationship.type) {
232
- case "supersedes":
233
- return `${toText} was superseded by ${fromText}`;
234
- case "implements":
235
- return `${fromText} now implements ${toText}`;
236
- case "covered_by":
237
- return `${fromText} gained test coverage via ${toText}`;
238
- case "verified_by":
239
- return `${fromText} is verified by ${toText}`;
240
- case "specified_by":
241
- return `${fromText} is specified by ${toText}`;
242
- case "requires_property":
243
- return `${fromText} constrains property ${toText}`;
244
- case "constrains":
245
- if (toEntity?.type === "fact" && toEntity.factKind === "subject") {
246
- return `${fromText} is linked to fact ${toText}`;
247
- }
248
- return `${fromText} is linked to ${toText}`;
249
- default:
250
- return null;
251
- }
252
- }
253
- function uniqueStrings(items) {
254
- return [...new Set(items.filter(Boolean))];
255
- }
256
- function buildEntityChange(entity, relationships, graph) {
257
- const label = describeEntity(entity, entity.id);
258
- if (entity.removed) {
259
- return `${label} was removed`;
260
- }
261
- if (entity.status === "superseded") {
262
- return `${label} was marked superseded`;
263
- }
264
- const hasRelationshipChange = relationships.some((relationship) => relationship.from === entity.id || relationship.to === entity.id);
265
- const hasGraphEdges = (graph?.edges.length ?? 0) > 0;
266
- if (!hasRelationshipChange && !hasGraphEdges) {
267
- return `${label} was created`;
268
- }
269
- return `${label} was updated`;
270
- }
271
- function summarizeTypes(entities) {
272
- const counts = new Map();
273
- for (const entity of entities) {
274
- counts.set(entity.type, (counts.get(entity.type) ?? 0) + 1);
275
- }
276
- return formatList([...counts.entries()].map(([type, count]) => `${count} ${humanizeType(type, count)}`));
277
- }
278
- function summarizeTopChanges(domainChanges, relationshipChanges) {
279
- const focus = relationshipChanges.length > 0 ? relationshipChanges : domainChanges;
280
- const selected = focus.slice(0, 2);
281
- return selected.length > 0 ? `Key changes: ${selected.join("; ")}.` : "";
282
- }
283
- function validationStatusText(checkResult) {
284
- if (checkResult.count <= 0) {
285
- return "All checks pass";
286
- }
287
- return `${checkResult.count} validation issue${checkResult.count === 1 ? "" : "s"}`;
288
- }
289
- async function loadEntity(sessionApi, sessionID, id) {
290
- const response = await invokeTool(sessionApi, sessionID, "kb_query", { id, limit: 1 });
291
- if (!Array.isArray(response)) {
292
- throw new Error(`kb_query returned unsupported payload for ${id}`);
293
- }
294
- if (response.length === 0) {
295
- return {
296
- id,
297
- type: inferTypeFromId(id),
298
- title: id,
299
- status: "removed",
300
- source: "",
301
- tags: [],
302
- factKind: "",
303
- removed: true,
304
- };
305
- }
306
- const record = asRecord(response[0]);
307
- if (!record) {
308
- throw new Error(`kb_query returned invalid entity for ${id}`);
309
- }
310
- return normalizeEntity(record, id);
311
- }
312
- async function loadGraph(sessionApi, sessionID, id) {
313
- const response = await invokeTool(sessionApi, sessionID, "kb_graph", {
314
- seedIds: [id],
315
- direction: "both",
316
- depth: 2,
317
- maxNodes: 100,
318
- maxEdges: 200,
319
- });
320
- return normalizeGraph(response);
321
- }
322
- // implements REQ-opencode-kibi-briefing-v2
323
- export async function generateGraphNarrative(client, workspaceCtx, changedEntityIds, changedRelationships, checkResult) {
324
- const uniqueIds = uniqueStrings(changedEntityIds);
325
- if (uniqueIds.length === 0) {
326
- return null;
327
- }
328
- const sessionApi = getSessionApi(client);
329
- if (!sessionApi) {
330
- return null;
331
- }
332
- let sessionID = null;
333
- try {
334
- const worker = await sessionApi.create({
335
- directory: workspaceCtx.workspaceRoot,
336
- title: `Kibi Graph Narrator (${workspaceCtx.branch})`,
337
- });
338
- sessionID = extractSessionId(worker);
339
- }
340
- catch {
341
- return null;
342
- }
343
- if (!sessionID) {
344
- return null;
345
- }
346
- const entities = new Map();
347
- const graphs = new Map();
348
- for (const id of uniqueIds) {
349
- let entity;
350
- try {
351
- entity = await loadEntity(sessionApi, sessionID, id);
352
- }
353
- catch {
354
- continue;
355
- }
356
- if (!entity) {
357
- continue;
358
- }
359
- entities.set(id, entity);
360
- if (entity.removed) {
361
- graphs.set(id, null);
362
- continue;
363
- }
364
- try {
365
- graphs.set(id, await loadGraph(sessionApi, sessionID, id));
366
- }
367
- catch {
368
- graphs.set(id, null);
369
- }
370
- }
371
- const entityList = [...entities.values()];
372
- if (entityList.length === 0) {
373
- return null;
374
- }
375
- const relationshipChanges = uniqueStrings(changedRelationships
376
- .map((relationship) => classifyRelationship(relationship, entities))
377
- .filter((sentence) => !!sentence));
378
- const domainMap = new Map();
379
- for (const entity of entityList) {
380
- const domain = inferDomain(entity);
381
- const changes = domainMap.get(domain) ?? [];
382
- changes.push(buildEntityChange(entity, changedRelationships, graphs.get(entity.id) ?? null));
383
- domainMap.set(domain, changes);
384
- }
385
- const domains = [...domainMap.entries()]
386
- .map(([name, changes]) => ({ name, changes: uniqueStrings(changes) }))
387
- .sort((left, right) => left.name.localeCompare(right.name));
388
- const domainNames = domains.map((domain) => domain.name);
389
- const headline = `${summarizeTypes(entityList)} changed${domainNames.length > 1 ? ` across ${formatList(domainNames)} domains` : ""}.`;
390
- const allDomainChanges = domains.flatMap((domain) => domain.changes);
391
- const validationStatus = validationStatusText(checkResult);
392
- const tldr = [
393
- headline,
394
- summarizeTopChanges(allDomainChanges, relationshipChanges),
395
- validationStatus === "All checks pass"
396
- ? "Validation remains clean."
397
- : `${validationStatus}.`,
398
- ]
399
- .filter(Boolean)
400
- .join(" ");
401
- return {
402
- headline,
403
- tldr,
404
- domains,
405
- relationshipChanges,
406
- validationStatus,
407
- };
408
- }
@@ -1,36 +0,0 @@
1
- export interface AuditCursor {
2
- lastTimestamp: string;
3
- lastOperation: string;
4
- entryCount: number;
5
- fileSize: number;
6
- }
7
- export interface AuditDelta {
8
- hasChanges: boolean;
9
- entries: AuditEntry[];
10
- newCursor: AuditCursor;
11
- contentHash: string;
12
- }
13
- export interface AuditEntry {
14
- timestamp: string;
15
- operation: string;
16
- entityId: string;
17
- payload?: AuditEntityPayload | AuditRelationshipPayload | null;
18
- }
19
- export interface AuditEntityPayload {
20
- kind: "entity";
21
- entityType: string;
22
- changeKind?: "created" | "updated";
23
- title?: string;
24
- source?: string;
25
- textRef?: string;
26
- properties: Record<string, unknown>;
27
- }
28
- export interface AuditRelationshipPayload {
29
- kind: "relationship";
30
- relationshipType: string;
31
- properties: Record<string, unknown>;
32
- }
33
- export declare function computeAuditDelta(workspaceRoot: string, branch: string, previousCursor: AuditCursor | null): AuditDelta;
34
- export declare function getLatestAuditCursor(workspaceRoot: string, branch: string): AuditCursor | null;
35
- export declare function getAuditTailCursor(workspaceRoot: string, branch: string): AuditCursor | null;
36
- export declare function guardBranchChanged(startBranch: string, currentBranch: string): boolean;
@@ -1,186 +0,0 @@
1
- import * as crypto from "node:crypto";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { parsePrologValue, parsePropertyList, splitTopLevelGeneral, } from "kibi-cli/prolog/codec";
5
- import { resolveAuditLogPath } from "./idle-brief-paths.js";
6
- function asOptionalString(value) {
7
- return typeof value === "string" ? value : undefined;
8
- }
9
- // Parse a single changeset line from the audit log
10
- function parseChangesetLine(line) {
11
- const trimmedLine = line.trim();
12
- if (!trimmedLine.startsWith("changeset(") || !trimmedLine.endsWith(").")) {
13
- return null;
14
- }
15
- const argsLiteral = trimmedLine.slice("changeset(".length, -2);
16
- const parts = splitTopLevelGeneral(argsLiteral, ",").map((part) => part.trim());
17
- if (parts.length < 4) {
18
- return null;
19
- }
20
- const timestamp = parsePrologValue(parts[0] ?? "");
21
- const operation = parsePrologValue(parts[1] ?? "");
22
- const entityId = parsePrologValue(parts[2] ?? "");
23
- if (typeof timestamp !== "string" ||
24
- typeof operation !== "string" ||
25
- typeof entityId !== "string") {
26
- return null;
27
- }
28
- const rawPayload = parts.slice(3).join(",");
29
- const payload = parsePayload(rawPayload.trim());
30
- return {
31
- timestamp,
32
- operation,
33
- entityId,
34
- ...(payload === undefined ? {} : { payload }),
35
- };
36
- }
37
- function parsePayload(rawPayload) {
38
- if (rawPayload === "null")
39
- return null;
40
- const match = rawPayload.match(/^([A-Za-z0-9_]+)-(.+)$/s);
41
- if (!match)
42
- return null;
43
- const [, payloadType = "unknown", rawProps = ""] = match;
44
- const properties = parsePropertyList(rawProps);
45
- if (payloadType === "rel") {
46
- return {
47
- kind: "relationship",
48
- relationshipType: payloadType,
49
- properties,
50
- };
51
- }
52
- const title = asOptionalString(properties.title);
53
- const source = asOptionalString(properties.source);
54
- const textRef = asOptionalString(properties.text_ref);
55
- const changeKindRaw = properties.change_kind;
56
- const changeKind = changeKindRaw === "created" || changeKindRaw === "updated"
57
- ? changeKindRaw
58
- : undefined;
59
- return {
60
- kind: "entity",
61
- entityType: payloadType,
62
- ...(changeKind ? { changeKind } : {}),
63
- ...(title ? { title } : {}),
64
- ...(source ? { source } : {}),
65
- ...(textRef ? { textRef } : {}),
66
- properties,
67
- };
68
- }
69
- // implements REQ-opencode-kibi-briefing-v4
70
- // Read audit log and compute delta since last cursor
71
- export function computeAuditDelta(workspaceRoot, branch, previousCursor) {
72
- const auditPath = resolveAuditLogPath(workspaceRoot, branch);
73
- if (!fs.existsSync(auditPath)) {
74
- return {
75
- hasChanges: false,
76
- entries: [],
77
- newCursor: previousCursor ?? {
78
- lastTimestamp: "",
79
- lastOperation: "",
80
- entryCount: 0,
81
- fileSize: 0,
82
- },
83
- contentHash: "",
84
- };
85
- }
86
- const content = fs.readFileSync(auditPath, "utf-8");
87
- const lines = content
88
- .split("\n")
89
- .filter((l) => l.trim().includes("changeset("));
90
- const fileSize = Buffer.byteLength(content, "utf-8");
91
- // If no previous cursor or file hasn't grown, check if content changed
92
- if (previousCursor &&
93
- fileSize === previousCursor.fileSize &&
94
- lines.length === previousCursor.entryCount) {
95
- return {
96
- hasChanges: false,
97
- entries: [],
98
- newCursor: previousCursor,
99
- contentHash: computeSimpleHash(lines),
100
- };
101
- }
102
- // Parse all entries
103
- const entries = lines
104
- .map(parseChangesetLine)
105
- .filter((e) => e !== null)
106
- .filter((e) => ["upsert", "upsert_rel", "delete"].includes(e.operation));
107
- // If we have a previous cursor, filter to only new entries
108
- let newEntries = entries;
109
- if (previousCursor?.lastTimestamp) {
110
- const lastIdx = entries.findIndex((e) => e.timestamp === previousCursor.lastTimestamp &&
111
- e.operation === previousCursor.lastOperation);
112
- if (lastIdx >= 0) {
113
- newEntries = entries.slice(lastIdx + 1);
114
- }
115
- }
116
- const lastEntry = entries[entries.length - 1];
117
- const newCursor = {
118
- lastTimestamp: lastEntry?.timestamp ?? "",
119
- lastOperation: lastEntry?.operation ?? "",
120
- entryCount: lines.length,
121
- fileSize,
122
- };
123
- return {
124
- hasChanges: newEntries.length > 0,
125
- entries: newEntries,
126
- newCursor,
127
- contentHash: computeSimpleHash(lines),
128
- };
129
- }
130
- function computeSimpleHash(lines) {
131
- return crypto
132
- .createHash("sha256")
133
- .update(lines.join("\n"))
134
- .digest("hex")
135
- .slice(0, 16);
136
- }
137
- // implements REQ-opencode-kibi-briefing-v4
138
- // Extract the latest audit cursor from the most recent brief for this branch
139
- export function getLatestAuditCursor(workspaceRoot, branch) {
140
- // Read .kb/briefs/ directory and find the latest brief for this branch
141
- const briefsDir = path.join(workspaceRoot, ".kb", "briefs");
142
- if (!fs.existsSync(briefsDir))
143
- return null;
144
- const files = fs
145
- .readdirSync(briefsDir)
146
- .filter((f) => f.endsWith("_brief.json") && !f.endsWith(".tmp"))
147
- .map((f) => {
148
- const fullPath = path.join(briefsDir, f);
149
- const [rawTimestamp = "0"] = f.split("_");
150
- const timestamp = Number.parseInt(rawTimestamp, 10);
151
- return {
152
- path: fullPath,
153
- timestamp: Number.isNaN(timestamp) ? 0 : timestamp,
154
- };
155
- })
156
- .sort((a, b) => b.timestamp - a.timestamp);
157
- for (const file of files) {
158
- try {
159
- const brief = JSON.parse(fs.readFileSync(file.path, "utf-8"));
160
- if (brief.branch === branch && brief.auditCursor) {
161
- return brief.auditCursor;
162
- }
163
- }
164
- catch {
165
- // skip invalid JSON
166
- }
167
- }
168
- return null;
169
- }
170
- export function getAuditTailCursor(
171
- // implements REQ-opencode-kibi-briefing-v6
172
- workspaceRoot, branch) {
173
- const auditPath = resolveAuditLogPath(workspaceRoot, branch);
174
- if (!fs.existsSync(auditPath)) {
175
- return null;
176
- }
177
- const delta = computeAuditDelta(workspaceRoot, branch, null);
178
- return delta.newCursor.entryCount > 0 || delta.newCursor.fileSize > 0
179
- ? delta.newCursor
180
- : null;
181
- }
182
- // implements REQ-opencode-kibi-briefing-v4
183
- // Guard: abort if branch changed since idle-start
184
- export function guardBranchChanged(startBranch, currentBranch) {
185
- return startBranch !== currentBranch;
186
- }
@@ -1,6 +0,0 @@
1
- export declare function resolveBriefsDir(workspaceRoot: string): string;
2
- export declare function resolveAuditLogPath(workspaceRoot: string, branch: string): string;
3
- export declare function resolveBriefFilePath(workspaceRoot: string, timestamp: number): string;
4
- export declare function resolveTempBriefPath(workspaceRoot: string, timestamp: number): string;
5
- export declare function atomicWriteBrief(workspaceRoot: string, timestamp: number, content: string): void;
6
- export declare function pruneOldBriefs(workspaceRoot: string, branch: string): void;