@fingerskier/augment 0.3.1 → 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.
Files changed (50) hide show
  1. package/dist/cli.d.ts +9 -6
  2. package/dist/cli.js +9 -43
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +2 -0
  5. package/dist/config.js +2 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.d.ts +1 -0
  8. package/dist/constants.js +7 -0
  9. package/dist/constants.js.map +1 -1
  10. package/dist/daemon/client.d.ts +4 -2
  11. package/dist/daemon/client.js +12 -5
  12. package/dist/daemon/client.js.map +1 -1
  13. package/dist/daemon/http.js +93 -28
  14. package/dist/daemon/http.js.map +1 -1
  15. package/dist/db.d.ts +1 -0
  16. package/dist/db.js +35 -4
  17. package/dist/db.js.map +1 -1
  18. package/dist/install.d.ts +2 -0
  19. package/dist/install.js +68 -5
  20. package/dist/install.js.map +1 -1
  21. package/dist/mcp/apps.js +0 -16
  22. package/dist/mcp/apps.js.map +1 -1
  23. package/dist/mcp/insights.d.ts +4 -6
  24. package/dist/mcp/insights.js +4 -11
  25. package/dist/mcp/insights.js.map +1 -1
  26. package/dist/mcp/server.js +43 -23
  27. package/dist/mcp/server.js.map +1 -1
  28. package/dist/memory/files.js +11 -0
  29. package/dist/memory/files.js.map +1 -1
  30. package/dist/memory/git.d.ts +5 -1
  31. package/dist/memory/git.js +7 -3
  32. package/dist/memory/git.js.map +1 -1
  33. package/dist/pi/extension.d.ts +4 -2
  34. package/dist/pi/extension.js +29 -47
  35. package/dist/pi/extension.js.map +1 -1
  36. package/dist/proposals.d.ts +42 -0
  37. package/dist/proposals.js +179 -0
  38. package/dist/proposals.js.map +1 -0
  39. package/dist/recall-log.d.ts +21 -0
  40. package/dist/recall-log.js +24 -0
  41. package/dist/recall-log.js.map +1 -0
  42. package/dist/recall.d.ts +1 -1
  43. package/dist/service.d.ts +63 -0
  44. package/dist/service.js +204 -3
  45. package/dist/service.js.map +1 -1
  46. package/dist/types.d.ts +26 -2
  47. package/docs/FEATURES.md +98 -27
  48. package/integrations/claude/skills/augment-context/SKILL.md +74 -0
  49. package/integrations/claude/skills/augment-sleep/SKILL.md +26 -0
  50. package/package.json +14 -14
@@ -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 for the dogfood tally (see src/tally.ts). */
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;
@@ -112,6 +133,48 @@ export declare class AugmentService {
112
133
  * behind the Phase-2 go/no-go and does not exist.
113
134
  */
114
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;
115
178
  private findSimilarMemory;
116
179
  private ensureEmbedding;
117
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
@@ -204,7 +205,9 @@ export class AugmentService {
204
205
  // memory at its deterministic path instead. Both versions stay
205
206
  // recoverable from the store and the prior body never pollutes the new
206
207
  // memory's embedding/lexical document.
207
- if (similar && lexicalScoreWith(new Set(tokens(similar.body)), input.content) >= 1) {
208
+ if (!input.skip_similarity_fold &&
209
+ similar &&
210
+ lexicalScoreWith(new Set(tokens(similar.body)), input.content) >= 1) {
208
211
  existing = similar;
209
212
  relativePath = similar.relative_path;
210
213
  action = "similar-updated";
@@ -220,6 +223,9 @@ export class AugmentService {
220
223
  links: existing.links,
221
224
  created_at: existing.created_at,
222
225
  updated_at: now,
226
+ archived: existing.archived,
227
+ consolidated_into: existing.consolidated_into,
228
+ derived_from: input.derived_from ?? existing.derived_from,
223
229
  }
224
230
  : {
225
231
  project: project.name,
@@ -229,6 +235,7 @@ export class AugmentService {
229
235
  links: [],
230
236
  created_at: now,
231
237
  updated_at: now,
238
+ derived_from: input.derived_from,
232
239
  };
233
240
  await writeMemory(this.config.memoryRoot, relativePath, normalizeMemoryFrontmatter(metadata), input.content);
234
241
  this.recordSelfWrite(relativePath);
@@ -306,6 +313,9 @@ export class AugmentService {
306
313
  links: memory.links.filter((link) => link.to !== targetPath),
307
314
  created_at: memory.created_at,
308
315
  updated_at: new Date().toISOString(),
316
+ archived: memory.archived,
317
+ consolidated_into: memory.consolidated_into,
318
+ derived_from: memory.derived_from,
309
319
  });
310
320
  await writeMemory(this.config.memoryRoot, memory.relative_path, frontmatter, memory.body);
311
321
  this.recordSelfWrite(memory.relative_path);
@@ -353,6 +363,9 @@ export class AugmentService {
353
363
  links: mergeFrontmatterLink(from.links, { to: to.relative_path, type }),
354
364
  created_at: from.created_at,
355
365
  updated_at: new Date().toISOString(),
366
+ archived: from.archived,
367
+ consolidated_into: from.consolidated_into,
368
+ derived_from: from.derived_from,
356
369
  });
357
370
  await writeMemory(this.config.memoryRoot, from.relative_path, frontmatter, from.body);
358
371
  this.recordSelfWrite(from.relative_path);
@@ -396,6 +409,9 @@ export class AugmentService {
396
409
  links: from.links.filter((candidate) => !(candidate.to === link.to_relative_path && candidate.type === link.type)),
397
410
  created_at: from.created_at,
398
411
  updated_at: new Date().toISOString(),
412
+ archived: from.archived,
413
+ consolidated_into: from.consolidated_into,
414
+ derived_from: from.derived_from,
399
415
  });
400
416
  await writeMemory(this.config.memoryRoot, from.relative_path, frontmatter, from.body);
401
417
  this.recordSelfWrite(from.relative_path);
@@ -462,7 +478,15 @@ export class AugmentService {
462
478
  const semanticScore = queryEmbedding ? normalizedCosine(queryEmbedding, embeddings.get(memory.id)) : 0;
463
479
  const lexScore = lexicalScoreWith(queryTokens, searchDocument(memory));
464
480
  const metadataScore = metadataScoreFor(input.project_name, memory);
465
- const score = clamp01(semanticScore * 0.55 + lexScore * 0.25 + metadataScore * 0.1);
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
+ }
466
490
  direct.set(memory.id, {
467
491
  memory,
468
492
  score,
@@ -513,6 +537,7 @@ export class AugmentService {
513
537
  const snapshot = await this.corpus();
514
538
  const cutoff = now - minAgeDays * 86_400_000;
515
539
  const eligible = snapshot.memories.filter((memory) => memory.kind === "WORK" &&
540
+ !memory.archived && // a consolidated source shouldn't be re-consolidated
516
541
  (!input.project_name || memory.project_name === input.project_name) &&
517
542
  Date.parse(memory.updated_at) <= cutoff);
518
543
  const clusters = clusterSleepCandidates({
@@ -527,6 +552,169 @@ export class AugmentService {
527
552
  text: renderSleepCandidatesText(clusters, eligible.length, minCosine, minAgeDays),
528
553
  };
529
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
+ }
530
718
  async findSimilarMemory(projectId, kind, content) {
531
719
  const candidates = await this.db.listMemoriesForProject(projectId, kind);
532
720
  const contentTokens = new Set(tokens(content));
@@ -557,6 +745,19 @@ function normalizeMemoryFrontmatter(input) {
557
745
  links: input.links ?? [],
558
746
  created_at: input.created_at,
559
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
+ : {}),
560
761
  };
561
762
  }
562
763
  function mergeFrontmatterLink(links, link) {