@fingerskier/augment 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -5
- package/dist/cli.d.ts +9 -6
- package/dist/cli.js +9 -43
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -1
- package/dist/daemon/client.d.ts +4 -2
- package/dist/daemon/client.js +30 -14
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/http.js +141 -50
- package/dist/daemon/http.js.map +1 -1
- package/dist/daemon/lifetime-lock.d.ts +22 -0
- package/dist/daemon/lifetime-lock.js +94 -0
- package/dist/daemon/lifetime-lock.js.map +1 -0
- package/dist/daemon/main.js +2 -1
- package/dist/daemon/main.js.map +1 -1
- package/dist/db.d.ts +3 -1
- package/dist/db.js +47 -10
- package/dist/db.js.map +1 -1
- package/dist/install.d.ts +2 -0
- package/dist/install.js +68 -5
- package/dist/install.js.map +1 -1
- package/dist/mcp/apps.js +0 -16
- package/dist/mcp/apps.js.map +1 -1
- package/dist/mcp/insights.d.ts +4 -6
- package/dist/mcp/insights.js +4 -11
- package/dist/mcp/insights.js.map +1 -1
- package/dist/mcp/server.js +43 -23
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory/files.js +11 -0
- package/dist/memory/files.js.map +1 -1
- package/dist/memory/git.d.ts +5 -1
- package/dist/memory/git.js +7 -3
- package/dist/memory/git.js.map +1 -1
- package/dist/opencode/plugin.d.ts +22 -0
- package/dist/opencode/plugin.js +135 -0
- package/dist/opencode/plugin.js.map +1 -0
- package/dist/pi/extension.d.ts +60 -0
- package/dist/pi/extension.js +414 -0
- package/dist/pi/extension.js.map +1 -0
- package/dist/proposals.d.ts +42 -0
- package/dist/proposals.js +179 -0
- package/dist/proposals.js.map +1 -0
- package/dist/recall-log.d.ts +21 -0
- package/dist/recall-log.js +24 -0
- package/dist/recall-log.js.map +1 -0
- package/dist/recall.d.ts +1 -1
- package/dist/service.d.ts +64 -0
- package/dist/service.js +213 -3
- package/dist/service.js.map +1 -1
- package/dist/types.d.ts +29 -5
- package/docs/FEATURES.md +141 -26
- package/integrations/claude/skills/augment-context/SKILL.md +74 -0
- package/integrations/claude/skills/augment-sleep/SKILL.md +26 -0
- package/integrations/pi/skills/augment-context/SKILL.md +59 -0
- package/package.json +38 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { normalizeKind, normalizeLinkType } from "./memory/files.js";
|
|
5
|
+
import { safeSegment } from "./memory/project.js";
|
|
6
|
+
/** sleep -> `<stateDir>/sleeps`; dream-memory | dream-link -> `<stateDir>/dreams`. */
|
|
7
|
+
export function proposalsDir(stateDir, type) {
|
|
8
|
+
return path.join(stateDir, type === "sleep" ? "sleeps" : "dreams");
|
|
9
|
+
}
|
|
10
|
+
export async function writeProposal(stateDir, fm, body) {
|
|
11
|
+
const dir = proposalsDir(stateDir, fm.type);
|
|
12
|
+
await mkdir(dir, { recursive: true });
|
|
13
|
+
const now = new Date().toISOString();
|
|
14
|
+
// Conditionally spread optional keys so an unset field is omitted from the
|
|
15
|
+
// object entirely (and thus from the written YAML) instead of being
|
|
16
|
+
// written as an explicit `null` -- mirrors normalizeFrontmatter's
|
|
17
|
+
// consolidated_into/derived_from handling in memory/files.ts.
|
|
18
|
+
const frontmatter = {
|
|
19
|
+
type: fm.type,
|
|
20
|
+
project: fm.project,
|
|
21
|
+
status: "proposed",
|
|
22
|
+
...(fm.kind !== undefined ? { kind: fm.kind } : {}),
|
|
23
|
+
...(fm.name !== undefined ? { name: fm.name } : {}),
|
|
24
|
+
derived_from: fm.derived_from,
|
|
25
|
+
...(fm.link !== undefined ? { link: fm.link } : {}),
|
|
26
|
+
...(fm.rationale !== undefined ? { rationale: fm.rationale } : {}),
|
|
27
|
+
created_at: now,
|
|
28
|
+
updated_at: now,
|
|
29
|
+
};
|
|
30
|
+
const stamp = timestampSegment(new Date());
|
|
31
|
+
const base = `${stamp}-${safeSegment(fm.name ?? "link")}`;
|
|
32
|
+
const filename = await uniqueFilename(dir, base);
|
|
33
|
+
const trimmedBody = body.trim();
|
|
34
|
+
await writeFile(path.join(dir, filename), matter.stringify(`${trimmedBody}\n`, frontmatter), "utf8");
|
|
35
|
+
return { frontmatter, body: trimmedBody, filename };
|
|
36
|
+
}
|
|
37
|
+
export async function readProposal(stateDir, type, filename) {
|
|
38
|
+
assertSafeFilename(filename);
|
|
39
|
+
const dir = proposalsDir(stateDir, type);
|
|
40
|
+
const raw = await readFile(path.join(dir, filename), "utf8");
|
|
41
|
+
const parsed = matter(raw);
|
|
42
|
+
return {
|
|
43
|
+
frontmatter: normalizeProposalFrontmatter(parsed.data),
|
|
44
|
+
body: parsed.content.trim(),
|
|
45
|
+
filename,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export async function listProposals(stateDir, type, status) {
|
|
49
|
+
const dir = proposalsDir(stateDir, type);
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(dir);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === "ENOENT") {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const filenames = entries.filter((entry) => entry.endsWith(".md")).sort();
|
|
61
|
+
const proposals = [];
|
|
62
|
+
for (const filename of filenames) {
|
|
63
|
+
try {
|
|
64
|
+
const proposal = await readProposal(stateDir, type, filename);
|
|
65
|
+
if (!status || proposal.frontmatter.status === status) {
|
|
66
|
+
proposals.push(proposal);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn(`Skipping unparseable proposal ${path.join(dir, filename)}: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return proposals;
|
|
74
|
+
}
|
|
75
|
+
export async function setProposalStatus(stateDir, type, filename, status) {
|
|
76
|
+
const existing = await readProposal(stateDir, type, filename);
|
|
77
|
+
const frontmatter = {
|
|
78
|
+
...existing.frontmatter,
|
|
79
|
+
status,
|
|
80
|
+
updated_at: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
const dir = proposalsDir(stateDir, type);
|
|
83
|
+
await writeFile(path.join(dir, filename), matter.stringify(`${existing.body}\n`, frontmatter), "utf8");
|
|
84
|
+
return { frontmatter, body: existing.body, filename };
|
|
85
|
+
}
|
|
86
|
+
export function renderProposalsText(proposals, type) {
|
|
87
|
+
if (proposals.length === 0) {
|
|
88
|
+
return `No ${type} proposals.\n`;
|
|
89
|
+
}
|
|
90
|
+
let out = `${type} proposals (${proposals.length}):\n`;
|
|
91
|
+
proposals.forEach((proposal, index) => {
|
|
92
|
+
const fm = proposal.frontmatter;
|
|
93
|
+
const subject = fm.link
|
|
94
|
+
? `link ${fm.link.from} -> ${fm.link.to} (${fm.link.type})`
|
|
95
|
+
: `${fm.kind ?? "?"} ${fm.name ?? "?"}`;
|
|
96
|
+
out += `\n[${index + 1}] ${proposal.filename} status:${fm.status} project:${fm.project}\n`;
|
|
97
|
+
out += ` ${subject}\n`;
|
|
98
|
+
out += ` derived_from: ${fm.derived_from.length > 0 ? fm.derived_from.join(", ") : "(none)"}\n`;
|
|
99
|
+
if (fm.rationale) {
|
|
100
|
+
out += ` rationale: ${fm.rationale}\n`;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
// yyyymmdd-hhmmss in UTC, so lexical filename sort matches chronological order
|
|
106
|
+
// regardless of the host machine's local timezone.
|
|
107
|
+
function timestampSegment(date) {
|
|
108
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
109
|
+
const y = date.getUTCFullYear();
|
|
110
|
+
const mo = pad(date.getUTCMonth() + 1);
|
|
111
|
+
const d = pad(date.getUTCDate());
|
|
112
|
+
const h = pad(date.getUTCHours());
|
|
113
|
+
const mi = pad(date.getUTCMinutes());
|
|
114
|
+
const s = pad(date.getUTCSeconds());
|
|
115
|
+
return `${y}${mo}${d}-${h}${mi}${s}`;
|
|
116
|
+
}
|
|
117
|
+
async function uniqueFilename(dir, base) {
|
|
118
|
+
let candidate = `${base}.md`;
|
|
119
|
+
let attempt = 2;
|
|
120
|
+
while (await pathExists(path.join(dir, candidate))) {
|
|
121
|
+
candidate = `${base}-${attempt}.md`;
|
|
122
|
+
attempt += 1;
|
|
123
|
+
}
|
|
124
|
+
return candidate;
|
|
125
|
+
}
|
|
126
|
+
async function pathExists(absolutePath) {
|
|
127
|
+
try {
|
|
128
|
+
await stat(absolutePath);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Security: filenames come from callers (including MCP tool args in later
|
|
136
|
+
// tasks) and are joined directly onto proposalsDir(). Reject anything that
|
|
137
|
+
// could escape that directory -- a path separator (either slash, since this
|
|
138
|
+
// repo runs on Windows too) or a bare "." / ".." segment.
|
|
139
|
+
function assertSafeFilename(filename) {
|
|
140
|
+
if (!filename || /[\\/]/.test(filename) || filename === "." || filename === "..") {
|
|
141
|
+
throw new Error(`Invalid proposal filename: ${filename}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Defensive re-parse of raw YAML frontmatter data. Proposals are
|
|
145
|
+
// user-editable on disk, and gray-matter's underlying YAML parser will turn
|
|
146
|
+
// an unquoted ISO-looking date into a native Date object rather than a
|
|
147
|
+
// string -- coerce dates back to strings the same way
|
|
148
|
+
// memory/files.ts#normalizeFrontmatter does. kind/link.type are revalidated
|
|
149
|
+
// through the existing normalizeKind/normalizeLinkType so a hand-edited typo
|
|
150
|
+
// fails loudly instead of silently corrupting the frontmatter.
|
|
151
|
+
function normalizeProposalFrontmatter(data) {
|
|
152
|
+
const frontmatter = {
|
|
153
|
+
type: data.type,
|
|
154
|
+
project: String(data.project ?? ""),
|
|
155
|
+
status: data.status,
|
|
156
|
+
derived_from: Array.isArray(data.derived_from) ? data.derived_from.map(String) : [],
|
|
157
|
+
created_at: String(data.created_at ?? ""),
|
|
158
|
+
updated_at: String(data.updated_at ?? ""),
|
|
159
|
+
};
|
|
160
|
+
if (data.kind !== undefined) {
|
|
161
|
+
frontmatter.kind = normalizeKind(String(data.kind));
|
|
162
|
+
}
|
|
163
|
+
if (data.name !== undefined) {
|
|
164
|
+
frontmatter.name = String(data.name);
|
|
165
|
+
}
|
|
166
|
+
if (data.link && typeof data.link === "object") {
|
|
167
|
+
const link = data.link;
|
|
168
|
+
frontmatter.link = {
|
|
169
|
+
from: String(link.from ?? ""),
|
|
170
|
+
to: String(link.to ?? ""),
|
|
171
|
+
type: normalizeLinkType(String(link.type)),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (data.rationale !== undefined) {
|
|
175
|
+
frontmatter.rationale = String(data.rationale);
|
|
176
|
+
}
|
|
177
|
+
return frontmatter;
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=proposals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proposals.js","sourceRoot":"","sources":["../src/proposals.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passive per-search recall log. Successor to the Phase-2 dogfood tally
|
|
3
|
+
* (removed per GH issue #3): the human-rating surface is gone, but the
|
|
4
|
+
* per-search event log stays — it is the instrument that caught the
|
|
5
|
+
* 2026-07-10 memoryRoot split-brain. Best-effort by design: a failed append
|
|
6
|
+
* must never fail the search that triggered it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RECALL_LOG_MAX_BYTES = 5000000;
|
|
9
|
+
/** One automatically-logged injection event (what search actually returned). */
|
|
10
|
+
export interface RecallEvent {
|
|
11
|
+
ts: string;
|
|
12
|
+
surface: string;
|
|
13
|
+
project?: string;
|
|
14
|
+
query: string;
|
|
15
|
+
abstained: boolean;
|
|
16
|
+
result_count: number;
|
|
17
|
+
top_score?: number;
|
|
18
|
+
paths: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function eventsPath(stateDir: string): string;
|
|
21
|
+
export declare function appendRecallEvent(stateDir: string, event: RecallEvent): Promise<void>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { appendFile, mkdir, rename, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Passive per-search recall log. Successor to the Phase-2 dogfood tally
|
|
5
|
+
* (removed per GH issue #3): the human-rating surface is gone, but the
|
|
6
|
+
* per-search event log stays — it is the instrument that caught the
|
|
7
|
+
* 2026-07-10 memoryRoot split-brain. Best-effort by design: a failed append
|
|
8
|
+
* must never fail the search that triggered it.
|
|
9
|
+
*/
|
|
10
|
+
export const RECALL_LOG_MAX_BYTES = 5_000_000;
|
|
11
|
+
export function eventsPath(stateDir) {
|
|
12
|
+
return path.join(stateDir, "recall", "events.jsonl");
|
|
13
|
+
}
|
|
14
|
+
export async function appendRecallEvent(stateDir, event) {
|
|
15
|
+
const file = eventsPath(stateDir);
|
|
16
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
17
|
+
// Single-slot rotation: the log is diagnostic, not a record of account.
|
|
18
|
+
const size = await stat(file).then((s) => s.size).catch(() => 0);
|
|
19
|
+
if (size > RECALL_LOG_MAX_BYTES) {
|
|
20
|
+
await rename(file, `${file}.1`).catch(() => { });
|
|
21
|
+
}
|
|
22
|
+
await appendFile(file, `${JSON.stringify(event)}\n`, "utf8");
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=recall-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recall-log.js","sourceRoot":"","sources":["../src/recall-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;GAMG;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,SAAS,CAAC;AAc9C,MAAM,UAAU,UAAU,CAAC,QAAgB;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAgB,EAAE,KAAkB;IAC1E,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAClC,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,wEAAwE;IACxE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IACjE,IAAI,IAAI,GAAG,oBAAoB,EAAE,CAAC;QAChC,MAAM,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAClD,CAAC;IACD,MAAM,UAAU,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC/D,CAAC"}
|
package/dist/recall.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface RecallOptions {
|
|
|
11
11
|
maxChars?: number;
|
|
12
12
|
/** Minimum relevance score (0..1); 0 disables the floor. */
|
|
13
13
|
minScore?: number;
|
|
14
|
-
/** Injection surface tag
|
|
14
|
+
/** Injection surface tag recorded in the per-search recall log (see src/recall-log.ts). */
|
|
15
15
|
surface?: string;
|
|
16
16
|
}
|
|
17
17
|
/** The slice of the daemon client `recall` depends on — kept narrow so tests can inject a fake. */
|
package/dist/service.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AugmentConfig } from "./config.js";
|
|
2
2
|
import { AugmentDatabase } from "./db.js";
|
|
3
3
|
import type { EmbeddingProvider } from "./embeddings.js";
|
|
4
|
+
import { type Proposal, type ProposalStatus } from "./proposals.js";
|
|
4
5
|
import { type SleepCluster } from "./sleep.js";
|
|
5
6
|
import type { LinkRecord, MemoryRecord, ProjectGraph, ProjectRecord, SearchInput, SearchResult, UpsertInput } from "./types.js";
|
|
6
7
|
export interface InitProjectInput {
|
|
@@ -28,6 +29,26 @@ export interface SleepCandidatesOutput {
|
|
|
28
29
|
clusters: SleepCluster[];
|
|
29
30
|
eligible: number;
|
|
30
31
|
}
|
|
32
|
+
/** Draft a sleep consolidation: >=2 WORK sources fold into one new SPEC/ARCH.
|
|
33
|
+
* The proposal is written OUTSIDE the corpus for review; nothing is consolidated
|
|
34
|
+
* until {@link AugmentService.sleepApply}. */
|
|
35
|
+
export interface SleepProposeInput {
|
|
36
|
+
project_name: string;
|
|
37
|
+
kind: "SPEC" | "ARCH";
|
|
38
|
+
name: string;
|
|
39
|
+
content: string;
|
|
40
|
+
derived_from: string[];
|
|
41
|
+
rationale?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface SleepApplyInput {
|
|
44
|
+
filename: string;
|
|
45
|
+
}
|
|
46
|
+
export interface SleepApplyOutput {
|
|
47
|
+
memory: MemoryRecord;
|
|
48
|
+
archived: string[];
|
|
49
|
+
proposal: Proposal;
|
|
50
|
+
text: string;
|
|
51
|
+
}
|
|
31
52
|
export declare class AugmentService {
|
|
32
53
|
private readonly config;
|
|
33
54
|
private readonly embeddingProvider;
|
|
@@ -45,6 +66,7 @@ export declare class AugmentService {
|
|
|
45
66
|
private corpusPromise?;
|
|
46
67
|
private readonly queryEmbeddingCache;
|
|
47
68
|
constructor(config: AugmentConfig, embeddingProvider?: EmbeddingProvider);
|
|
69
|
+
useStateDir(stateDir: string): void;
|
|
48
70
|
close(): Promise<void>;
|
|
49
71
|
init(): Promise<void>;
|
|
50
72
|
initProject(input?: InitProjectInput): Promise<ProjectRecord>;
|
|
@@ -111,6 +133,48 @@ export declare class AugmentService {
|
|
|
111
133
|
* behind the Phase-2 go/no-go and does not exist.
|
|
112
134
|
*/
|
|
113
135
|
sleepCandidates(input?: SleepCandidatesInput, now?: number): Promise<SleepCandidatesOutput>;
|
|
136
|
+
/**
|
|
137
|
+
* Draft a sleep consolidation as a review artifact OUTSIDE the corpus. Writes
|
|
138
|
+
* nothing to the memory store: the corpus is only mutated by {@link sleepApply}
|
|
139
|
+
* on an explicitly approved proposal. Validates the sources up front (same
|
|
140
|
+
* checks apply re-runs against the live corpus) so an impossible proposal fails
|
|
141
|
+
* loudly at draft time.
|
|
142
|
+
*/
|
|
143
|
+
sleepPropose(input: SleepProposeInput): Promise<{
|
|
144
|
+
proposal: Proposal;
|
|
145
|
+
text: string;
|
|
146
|
+
}>;
|
|
147
|
+
sleepProposals(status?: ProposalStatus): Promise<{
|
|
148
|
+
proposals: Proposal[];
|
|
149
|
+
text: string;
|
|
150
|
+
}>;
|
|
151
|
+
/**
|
|
152
|
+
* Apply an approved sleep proposal — the ONLY corpus mutation in the flow. The
|
|
153
|
+
* sequence is a contract: read the proposal fresh from disk (user edits count),
|
|
154
|
+
* require it still be `proposed`, re-validate against the LIVE corpus (drafts
|
|
155
|
+
* sit while the corpus moves underneath them), upsert the consolidated record
|
|
156
|
+
* (provenance threaded into frontmatter), archive each source in place (body
|
|
157
|
+
* untouched, verbatim), then flip the proposal to `applied`. Does NOT commit
|
|
158
|
+
* git: the route commits the whole pass as one revertible commit.
|
|
159
|
+
*/
|
|
160
|
+
sleepApply(input: SleepApplyInput): Promise<SleepApplyOutput>;
|
|
161
|
+
private sleepApplyCore;
|
|
162
|
+
sleepReject(filename: string): Promise<{
|
|
163
|
+
proposal: Proposal;
|
|
164
|
+
text: string;
|
|
165
|
+
}>;
|
|
166
|
+
/**
|
|
167
|
+
* Shared sleep validation, run at propose time and re-run against the live
|
|
168
|
+
* corpus at apply time. Every source must resolve, be a WORK memory in
|
|
169
|
+
* `projectName` (single-project constraint), and be unarchived; there must be
|
|
170
|
+
* >=2 of them; and the consolidation target must not already exist. Every
|
|
171
|
+
* violation names the offending path.
|
|
172
|
+
*/
|
|
173
|
+
private validateSleepSources;
|
|
174
|
+
/** Rewrite one source's frontmatter to archived + consolidated_into, leaving
|
|
175
|
+
* the body verbatim, and reindex it. Mirrors {@link scrubInboundLinks}. Returns
|
|
176
|
+
* the source's relative path. */
|
|
177
|
+
private archiveSource;
|
|
114
178
|
private findSimilarMemory;
|
|
115
179
|
private ensureEmbedding;
|
|
116
180
|
}
|
package/dist/service.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { DEFAULT_MEMORY_HARD_LIMIT_CHARS, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_MAX_CHARS, DEFAULT_SEARCH_MIN_SCORE, SEMANTIC_COSINE_BASELINE, SEMANTIC_COSINE_MATCH, SLEEP_MAX_CLUSTERS, SLEEP_MIN_AGE_DAYS, SLEEP_MIN_COSINE, } from "./constants.js";
|
|
3
|
+
import { ARCHIVED_SEARCH_WEIGHT, DEFAULT_MEMORY_HARD_LIMIT_CHARS, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_MAX_CHARS, DEFAULT_SEARCH_MIN_SCORE, SEMANTIC_COSINE_BASELINE, SEMANTIC_COSINE_MATCH, SLEEP_MAX_CLUSTERS, SLEEP_MIN_AGE_DAYS, SLEEP_MIN_COSINE, } from "./constants.js";
|
|
4
4
|
import { ensureConfigDirs } from "./config.js";
|
|
5
5
|
import { AugmentDatabase } from "./db.js";
|
|
6
6
|
import { TransformersEmbeddingProvider } from "./embeddings.js";
|
|
7
7
|
import { deleteMemoryFile, normalizeKind, normalizeLinkType, normalizeRelativePath, memoryRelativePath, readMemory, writeMemory } from "./memory/files.js";
|
|
8
8
|
import { inferProjectName } from "./memory/project.js";
|
|
9
9
|
import { findMemoryFiles } from "./memory/scan.js";
|
|
10
|
+
import { listProposals, readProposal, renderProposalsText, setProposalStatus, writeProposal, } from "./proposals.js";
|
|
10
11
|
import { clusterSleepCandidates, renderSleepCandidatesText } from "./sleep.js";
|
|
11
12
|
import { clamp01, cosineSimilarity, lexicalScoreWith, tokens } from "./util/text.js";
|
|
12
13
|
/** How long a self-write marker stays claimable. Long enough to cover the watcher's
|
|
@@ -37,6 +38,15 @@ export class AugmentService {
|
|
|
37
38
|
this.embeddingProvider = embeddingProvider;
|
|
38
39
|
this.db = new AugmentDatabase(config.databasePath);
|
|
39
40
|
}
|
|
41
|
+
useStateDir(stateDir) {
|
|
42
|
+
if (this.initialized) {
|
|
43
|
+
throw new Error("Cannot change AugmentService stateDir after initialization");
|
|
44
|
+
}
|
|
45
|
+
this.config.stateDir = stateDir;
|
|
46
|
+
this.config.databasePath = path.join(stateDir, "index.db");
|
|
47
|
+
this.config.daemonFile = path.join(stateDir, "daemon.json");
|
|
48
|
+
this.db.setDatabasePath(this.config.databasePath);
|
|
49
|
+
}
|
|
40
50
|
async close() {
|
|
41
51
|
this.db.close();
|
|
42
52
|
this.initialized = false;
|
|
@@ -195,7 +205,9 @@ export class AugmentService {
|
|
|
195
205
|
// memory at its deterministic path instead. Both versions stay
|
|
196
206
|
// recoverable from the store and the prior body never pollutes the new
|
|
197
207
|
// memory's embedding/lexical document.
|
|
198
|
-
if (
|
|
208
|
+
if (!input.skip_similarity_fold &&
|
|
209
|
+
similar &&
|
|
210
|
+
lexicalScoreWith(new Set(tokens(similar.body)), input.content) >= 1) {
|
|
199
211
|
existing = similar;
|
|
200
212
|
relativePath = similar.relative_path;
|
|
201
213
|
action = "similar-updated";
|
|
@@ -211,6 +223,9 @@ export class AugmentService {
|
|
|
211
223
|
links: existing.links,
|
|
212
224
|
created_at: existing.created_at,
|
|
213
225
|
updated_at: now,
|
|
226
|
+
archived: existing.archived,
|
|
227
|
+
consolidated_into: existing.consolidated_into,
|
|
228
|
+
derived_from: input.derived_from ?? existing.derived_from,
|
|
214
229
|
}
|
|
215
230
|
: {
|
|
216
231
|
project: project.name,
|
|
@@ -220,6 +235,7 @@ export class AugmentService {
|
|
|
220
235
|
links: [],
|
|
221
236
|
created_at: now,
|
|
222
237
|
updated_at: now,
|
|
238
|
+
derived_from: input.derived_from,
|
|
223
239
|
};
|
|
224
240
|
await writeMemory(this.config.memoryRoot, relativePath, normalizeMemoryFrontmatter(metadata), input.content);
|
|
225
241
|
this.recordSelfWrite(relativePath);
|
|
@@ -297,6 +313,9 @@ export class AugmentService {
|
|
|
297
313
|
links: memory.links.filter((link) => link.to !== targetPath),
|
|
298
314
|
created_at: memory.created_at,
|
|
299
315
|
updated_at: new Date().toISOString(),
|
|
316
|
+
archived: memory.archived,
|
|
317
|
+
consolidated_into: memory.consolidated_into,
|
|
318
|
+
derived_from: memory.derived_from,
|
|
300
319
|
});
|
|
301
320
|
await writeMemory(this.config.memoryRoot, memory.relative_path, frontmatter, memory.body);
|
|
302
321
|
this.recordSelfWrite(memory.relative_path);
|
|
@@ -344,6 +363,9 @@ export class AugmentService {
|
|
|
344
363
|
links: mergeFrontmatterLink(from.links, { to: to.relative_path, type }),
|
|
345
364
|
created_at: from.created_at,
|
|
346
365
|
updated_at: new Date().toISOString(),
|
|
366
|
+
archived: from.archived,
|
|
367
|
+
consolidated_into: from.consolidated_into,
|
|
368
|
+
derived_from: from.derived_from,
|
|
347
369
|
});
|
|
348
370
|
await writeMemory(this.config.memoryRoot, from.relative_path, frontmatter, from.body);
|
|
349
371
|
this.recordSelfWrite(from.relative_path);
|
|
@@ -387,6 +409,9 @@ export class AugmentService {
|
|
|
387
409
|
links: from.links.filter((candidate) => !(candidate.to === link.to_relative_path && candidate.type === link.type)),
|
|
388
410
|
created_at: from.created_at,
|
|
389
411
|
updated_at: new Date().toISOString(),
|
|
412
|
+
archived: from.archived,
|
|
413
|
+
consolidated_into: from.consolidated_into,
|
|
414
|
+
derived_from: from.derived_from,
|
|
390
415
|
});
|
|
391
416
|
await writeMemory(this.config.memoryRoot, from.relative_path, frontmatter, from.body);
|
|
392
417
|
this.recordSelfWrite(from.relative_path);
|
|
@@ -453,7 +478,15 @@ export class AugmentService {
|
|
|
453
478
|
const semanticScore = queryEmbedding ? normalizedCosine(queryEmbedding, embeddings.get(memory.id)) : 0;
|
|
454
479
|
const lexScore = lexicalScoreWith(queryTokens, searchDocument(memory));
|
|
455
480
|
const metadataScore = metadataScoreFor(input.project_name, memory);
|
|
456
|
-
|
|
481
|
+
let score = clamp01(semanticScore * 0.55 + lexScore * 0.25 + metadataScore * 0.1);
|
|
482
|
+
// Down-weight (never exclude) an archived memory's own relevance, after
|
|
483
|
+
// the blend and before the relevance floor below -- so a down-weighted
|
|
484
|
+
// score CAN fall under the floor and be abstained on, but read() still
|
|
485
|
+
// returns its full body verbatim (archival is a search-time concern
|
|
486
|
+
// only). See ARCHIVED_SEARCH_WEIGHT in constants.ts.
|
|
487
|
+
if (memory.archived) {
|
|
488
|
+
score = clamp01(score * ARCHIVED_SEARCH_WEIGHT);
|
|
489
|
+
}
|
|
457
490
|
direct.set(memory.id, {
|
|
458
491
|
memory,
|
|
459
492
|
score,
|
|
@@ -504,6 +537,7 @@ export class AugmentService {
|
|
|
504
537
|
const snapshot = await this.corpus();
|
|
505
538
|
const cutoff = now - minAgeDays * 86_400_000;
|
|
506
539
|
const eligible = snapshot.memories.filter((memory) => memory.kind === "WORK" &&
|
|
540
|
+
!memory.archived && // a consolidated source shouldn't be re-consolidated
|
|
507
541
|
(!input.project_name || memory.project_name === input.project_name) &&
|
|
508
542
|
Date.parse(memory.updated_at) <= cutoff);
|
|
509
543
|
const clusters = clusterSleepCandidates({
|
|
@@ -518,6 +552,169 @@ export class AugmentService {
|
|
|
518
552
|
text: renderSleepCandidatesText(clusters, eligible.length, minCosine, minAgeDays),
|
|
519
553
|
};
|
|
520
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Draft a sleep consolidation as a review artifact OUTSIDE the corpus. Writes
|
|
557
|
+
* nothing to the memory store: the corpus is only mutated by {@link sleepApply}
|
|
558
|
+
* on an explicitly approved proposal. Validates the sources up front (same
|
|
559
|
+
* checks apply re-runs against the live corpus) so an impossible proposal fails
|
|
560
|
+
* loudly at draft time.
|
|
561
|
+
*/
|
|
562
|
+
async sleepPropose(input) {
|
|
563
|
+
await this.init();
|
|
564
|
+
const kind = normalizeKind(input.kind);
|
|
565
|
+
await this.validateSleepSources(input.project_name, kind, input.name, input.derived_from);
|
|
566
|
+
const proposal = await writeProposal(this.config.stateDir, {
|
|
567
|
+
type: "sleep",
|
|
568
|
+
project: input.project_name,
|
|
569
|
+
kind,
|
|
570
|
+
name: input.name,
|
|
571
|
+
derived_from: input.derived_from,
|
|
572
|
+
...(input.rationale !== undefined ? { rationale: input.rationale } : {}),
|
|
573
|
+
}, input.content);
|
|
574
|
+
return { proposal, text: renderProposalsText([proposal], "sleep") };
|
|
575
|
+
}
|
|
576
|
+
async sleepProposals(status) {
|
|
577
|
+
await this.init();
|
|
578
|
+
const proposals = await listProposals(this.config.stateDir, "sleep", status);
|
|
579
|
+
return { proposals, text: renderProposalsText(proposals, "sleep") };
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Apply an approved sleep proposal — the ONLY corpus mutation in the flow. The
|
|
583
|
+
* sequence is a contract: read the proposal fresh from disk (user edits count),
|
|
584
|
+
* require it still be `proposed`, re-validate against the LIVE corpus (drafts
|
|
585
|
+
* sit while the corpus moves underneath them), upsert the consolidated record
|
|
586
|
+
* (provenance threaded into frontmatter), archive each source in place (body
|
|
587
|
+
* untouched, verbatim), then flip the proposal to `applied`. Does NOT commit
|
|
588
|
+
* git: the route commits the whole pass as one revertible commit.
|
|
589
|
+
*/
|
|
590
|
+
async sleepApply(input) {
|
|
591
|
+
try {
|
|
592
|
+
return await this.sleepApplyCore(input);
|
|
593
|
+
}
|
|
594
|
+
finally {
|
|
595
|
+
this.invalidateCorpusSnapshot();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async sleepApplyCore(input) {
|
|
599
|
+
await this.init();
|
|
600
|
+
// 1. Read the proposal fresh from disk so hand-edits (body, sources) count.
|
|
601
|
+
const proposal = await readProposal(this.config.stateDir, "sleep", input.filename);
|
|
602
|
+
const fm = proposal.frontmatter;
|
|
603
|
+
// 2. Only a still-proposed draft may apply; a second apply is rejected here.
|
|
604
|
+
if (fm.status !== "proposed") {
|
|
605
|
+
throw new Error(`Sleep proposal ${input.filename} is already ${fm.status}, not proposed (already applied?)`);
|
|
606
|
+
}
|
|
607
|
+
if (!fm.kind) {
|
|
608
|
+
throw new Error(`Sleep proposal ${input.filename} is missing a target kind`);
|
|
609
|
+
}
|
|
610
|
+
if (!fm.name) {
|
|
611
|
+
throw new Error(`Sleep proposal ${input.filename} is missing a target name`);
|
|
612
|
+
}
|
|
613
|
+
const kind = normalizeKind(fm.kind);
|
|
614
|
+
const name = fm.name;
|
|
615
|
+
const projectName = fm.project;
|
|
616
|
+
const derivedFrom = fm.derived_from;
|
|
617
|
+
// 3. Re-validate against the live corpus (sources may have moved since draft).
|
|
618
|
+
await this.validateSleepSources(projectName, kind, name, derivedFrom);
|
|
619
|
+
const project = await this.db.getProjectByName(projectName);
|
|
620
|
+
if (!project) {
|
|
621
|
+
throw new Error(`Sleep proposal project not found in corpus: ${projectName}`);
|
|
622
|
+
}
|
|
623
|
+
// 4. Write the consolidated record through the public upsert (snapshot
|
|
624
|
+
// invalidation + provenance handled there). skip_similarity_fold pins the
|
|
625
|
+
// write to the approved target's deterministic path: the user approved
|
|
626
|
+
// THIS identity (validateSleepSources above already guaranteed it's
|
|
627
|
+
// free), so upsertCore must not divert it into some other lexically-
|
|
628
|
+
// similar same-kind record it happens to subsume.
|
|
629
|
+
const upserted = await this.upsert({
|
|
630
|
+
project_id: project.id,
|
|
631
|
+
kind,
|
|
632
|
+
name,
|
|
633
|
+
content: proposal.body,
|
|
634
|
+
derived_from: derivedFrom,
|
|
635
|
+
skip_similarity_fold: true,
|
|
636
|
+
});
|
|
637
|
+
const expectedTargetPath = memoryRelativePath(projectName, kind, name);
|
|
638
|
+
if (upserted.memory.relative_path !== expectedTargetPath) {
|
|
639
|
+
throw new Error(`Sleep apply diverted from the approved target ${expectedTargetPath} to ${upserted.memory.relative_path}; aborting before archiving any source`);
|
|
640
|
+
}
|
|
641
|
+
const consolidatedInto = upserted.memory.relative_path;
|
|
642
|
+
// 5. Archive each source in place, pointing it at the consolidated record.
|
|
643
|
+
const archived = [];
|
|
644
|
+
for (const sourcePath of derivedFrom) {
|
|
645
|
+
archived.push(await this.archiveSource(sourcePath, consolidatedInto));
|
|
646
|
+
}
|
|
647
|
+
// 6. Flip the proposal to applied (outside the corpus).
|
|
648
|
+
const appliedProposal = await setProposalStatus(this.config.stateDir, "sleep", input.filename, "applied");
|
|
649
|
+
return {
|
|
650
|
+
memory: upserted.memory,
|
|
651
|
+
archived,
|
|
652
|
+
proposal: appliedProposal,
|
|
653
|
+
text: `applied sleep ${input.filename}: ${derivedFrom.length} WORK -> ${kind}/${name} (${projectName})`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
async sleepReject(filename) {
|
|
657
|
+
await this.init();
|
|
658
|
+
const proposal = await setProposalStatus(this.config.stateDir, "sleep", filename, "rejected");
|
|
659
|
+
return { proposal, text: renderProposalsText([proposal], "sleep") };
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Shared sleep validation, run at propose time and re-run against the live
|
|
663
|
+
* corpus at apply time. Every source must resolve, be a WORK memory in
|
|
664
|
+
* `projectName` (single-project constraint), and be unarchived; there must be
|
|
665
|
+
* >=2 of them; and the consolidation target must not already exist. Every
|
|
666
|
+
* violation names the offending path.
|
|
667
|
+
*/
|
|
668
|
+
async validateSleepSources(projectName, kind, name, derivedFrom) {
|
|
669
|
+
if (derivedFrom.length < 2) {
|
|
670
|
+
throw new Error(`A sleep consolidation needs at least 2 WORK sources; got ${derivedFrom.length}`);
|
|
671
|
+
}
|
|
672
|
+
for (const sourcePath of derivedFrom) {
|
|
673
|
+
const source = await this.db.getMemoryByPath(normalizeRelativePath(sourcePath));
|
|
674
|
+
if (!source) {
|
|
675
|
+
throw new Error(`Sleep source not found in corpus: ${sourcePath}`);
|
|
676
|
+
}
|
|
677
|
+
if (source.kind !== "WORK") {
|
|
678
|
+
throw new Error(`Sleep source must be a WORK memory: ${sourcePath} is ${source.kind}`);
|
|
679
|
+
}
|
|
680
|
+
if (source.project_name !== projectName) {
|
|
681
|
+
throw new Error(`Sleep source ${sourcePath} belongs to ${source.project_name}, not ${projectName}`);
|
|
682
|
+
}
|
|
683
|
+
if (source.archived) {
|
|
684
|
+
throw new Error(`Sleep source is already archived: ${sourcePath}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const targetPath = memoryRelativePath(projectName, kind, name);
|
|
688
|
+
if (await this.db.getMemoryByPath(targetPath)) {
|
|
689
|
+
throw new Error(`Sleep target already exists in corpus: ${targetPath}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/** Rewrite one source's frontmatter to archived + consolidated_into, leaving
|
|
693
|
+
* the body verbatim, and reindex it. Mirrors {@link scrubInboundLinks}. Returns
|
|
694
|
+
* the source's relative path. */
|
|
695
|
+
async archiveSource(sourcePath, consolidatedInto) {
|
|
696
|
+
const memory = await this.db.getMemoryByPath(normalizeRelativePath(sourcePath));
|
|
697
|
+
if (!memory) {
|
|
698
|
+
throw new Error(`Sleep source not found in corpus: ${sourcePath}`);
|
|
699
|
+
}
|
|
700
|
+
const frontmatter = normalizeMemoryFrontmatter({
|
|
701
|
+
project: memory.project_name,
|
|
702
|
+
kind: memory.kind,
|
|
703
|
+
name: memory.name,
|
|
704
|
+
tags: memory.tags,
|
|
705
|
+
links: memory.links,
|
|
706
|
+
created_at: memory.created_at,
|
|
707
|
+
updated_at: new Date().toISOString(),
|
|
708
|
+
archived: true,
|
|
709
|
+
consolidated_into: consolidatedInto,
|
|
710
|
+
derived_from: memory.derived_from,
|
|
711
|
+
});
|
|
712
|
+
await writeMemory(this.config.memoryRoot, memory.relative_path, frontmatter, memory.body);
|
|
713
|
+
this.recordSelfWrite(memory.relative_path);
|
|
714
|
+
const parsed = await readMemory(this.config.memoryRoot, memory.relative_path);
|
|
715
|
+
await this.db.upsertMemory(parsed);
|
|
716
|
+
return memory.relative_path;
|
|
717
|
+
}
|
|
521
718
|
async findSimilarMemory(projectId, kind, content) {
|
|
522
719
|
const candidates = await this.db.listMemoriesForProject(projectId, kind);
|
|
523
720
|
const contentTokens = new Set(tokens(content));
|
|
@@ -548,6 +745,19 @@ function normalizeMemoryFrontmatter(input) {
|
|
|
548
745
|
links: input.links ?? [],
|
|
549
746
|
created_at: input.created_at,
|
|
550
747
|
updated_at: input.updated_at,
|
|
748
|
+
// Every corpus-mutating write reconstructs frontmatter through this
|
|
749
|
+
// function. Keys are included ONLY when set so an ordinary (never
|
|
750
|
+
// archived) memory never gains "archived: false" churn -- but every
|
|
751
|
+
// caller MUST thread archived/consolidated_into/derived_from through from
|
|
752
|
+
// the existing record, or a write that has nothing to do with archival
|
|
753
|
+
// state (link/unlink/scrub) would silently strip it.
|
|
754
|
+
...(input.archived === true ? { archived: true } : {}),
|
|
755
|
+
...(typeof input.consolidated_into === "string"
|
|
756
|
+
? { consolidated_into: normalizeRelativePath(input.consolidated_into) }
|
|
757
|
+
: {}),
|
|
758
|
+
...(Array.isArray(input.derived_from)
|
|
759
|
+
? { derived_from: input.derived_from.map((entry) => normalizeRelativePath(entry)) }
|
|
760
|
+
: {}),
|
|
551
761
|
};
|
|
552
762
|
}
|
|
553
763
|
function mergeFrontmatterLink(links, link) {
|