@etiquekit/etq 1.0.12 → 1.0.14

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 (149) hide show
  1. package/AGENTS.md +19 -26
  2. package/LICENSE +2 -3
  3. package/NOTICE +6 -4
  4. package/QuickStart.md +35 -47
  5. package/README.md +54 -54
  6. package/bin/etiquette +1 -5
  7. package/bin/etiquette-core +1 -5
  8. package/docs/ARCHITECTURE.md +5 -5
  9. package/docs/CODEX_CLIENT_COMPATIBILITY.md +1 -1
  10. package/docs/CONCEPTS.md +7 -23
  11. package/docs/CONCEPT_STATUS.md +66 -0
  12. package/docs/CORE_PROFILE.md +3 -5
  13. package/docs/LANE_PROVISIONING.md +7 -2
  14. package/docs/README.md +4 -4
  15. package/docs/RELEASE_SURFACE_AUDIT.md +8 -7
  16. package/docs/SEAT_DISCIPLINE.md +55 -91
  17. package/docs/SEAT_PROVISIONING.md +19 -22
  18. package/docs/TEAM_HANDOFF.md +21 -21
  19. package/docs/WORKTREE_QOL.md +7 -6
  20. package/docs/contracts/ledger-entry/README.md +11 -11
  21. package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +3 -3
  22. package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
  23. package/package.json +9 -9
  24. package/packages/control/src/authority/lease.ts +261 -0
  25. package/packages/control/src/authority/node-delegation.ts +257 -0
  26. package/packages/control/src/authority/rig-conductor.ts +632 -0
  27. package/packages/control/src/cli/argv.ts +155 -0
  28. package/packages/control/src/cli/commands/console.ts +200 -0
  29. package/packages/control/src/cli/commands/dispatch-core.ts +89 -0
  30. package/packages/control/src/cli/commands/dispatch.ts +279 -0
  31. package/packages/control/src/cli/commands/harness.ts +89 -0
  32. package/packages/control/src/cli/commands/hook.ts +50 -0
  33. package/packages/control/src/cli/commands/ledger.ts +91 -0
  34. package/packages/control/src/cli/commands/local-workflow.ts +4690 -0
  35. package/packages/control/src/cli/commands/memory.ts +445 -0
  36. package/packages/control/src/cli/commands/release.ts +108 -0
  37. package/packages/control/src/cli/commands/rubric.ts +103 -0
  38. package/packages/control/src/cli/commands/seat.ts +179 -0
  39. package/packages/control/src/cli/commands/session.ts +127 -0
  40. package/packages/control/src/cli/commands/supervision.ts +246 -0
  41. package/packages/control/src/cli/commands/sync.ts +119 -0
  42. package/packages/control/src/cli/commands/workflow.ts +86 -0
  43. package/packages/control/src/cli/commands/workspace.ts +50 -0
  44. package/packages/control/src/cli/core-usage.ts +34 -0
  45. package/packages/control/src/cli/prompt.ts +67 -0
  46. package/packages/control/src/cli/supervision-deps.ts +44 -0
  47. package/packages/control/src/cli/usage.ts +241 -0
  48. package/packages/control/src/cli.ts +207 -0
  49. package/packages/control/src/core-cli.ts +50 -0
  50. package/packages/control/src/dispatch/decision.ts +202 -0
  51. package/packages/control/src/dispatch/projection.ts +293 -0
  52. package/packages/control/src/dispatch/record.ts +153 -0
  53. package/packages/control/src/engagement/project.ts +170 -0
  54. package/packages/control/src/fs.ts +19 -0
  55. package/packages/control/src/harness/pruning.ts +406 -0
  56. package/packages/control/src/hooks/dispatcher.ts +117 -0
  57. package/packages/control/src/hooks/outbox.ts +86 -0
  58. package/packages/control/src/hooks/sanitize.ts +6 -0
  59. package/packages/control/src/hooks/types.ts +34 -0
  60. package/packages/control/src/index.ts +384 -0
  61. package/packages/control/src/ledger/entry.ts +303 -0
  62. package/packages/control/src/ledger/indexer.ts +542 -0
  63. package/packages/control/src/memory/context.ts +149 -0
  64. package/packages/control/src/memory/drain-import.ts +207 -0
  65. package/packages/control/src/memory/indexer.ts +284 -0
  66. package/packages/control/src/memory/query.ts +75 -0
  67. package/packages/control/src/memory/sanitize.ts +50 -0
  68. package/packages/control/src/memory/sharded-drain-import.ts +212 -0
  69. package/packages/control/src/memory/status.ts +211 -0
  70. package/packages/control/src/memory/store-lifecycle.ts +509 -0
  71. package/packages/control/src/memory/store.ts +284 -0
  72. package/packages/control/src/memory/types.ts +146 -0
  73. package/packages/control/src/parity/surfaces.ts +748 -0
  74. package/packages/control/src/project.ts +141 -0
  75. package/packages/control/src/projection/local-ledger-view.ts +373 -0
  76. package/packages/control/src/projection/return-enforcement.ts +48 -0
  77. package/packages/control/src/projection/timeline-preview.ts +539 -0
  78. package/packages/control/src/projection/timeline.ts +708 -0
  79. package/packages/control/src/release/readiness.ts +842 -0
  80. package/packages/control/src/rubric/loader.ts +326 -0
  81. package/packages/control/src/rubric/promotion.ts +54 -0
  82. package/packages/control/src/rubric/runner.ts +159 -0
  83. package/packages/control/src/rubric/types.ts +158 -0
  84. package/packages/control/src/seat/owner-card.ts +388 -0
  85. package/packages/control/src/seat/readiness.ts +834 -0
  86. package/packages/control/src/session/runbook.ts +431 -0
  87. package/packages/control/src/shared/sanitize.ts +49 -0
  88. package/packages/control/src/supervision/action-classes.ts +192 -0
  89. package/packages/control/src/supervision/command-apply.ts +378 -0
  90. package/packages/control/src/supervision/errors.ts +14 -0
  91. package/packages/control/src/supervision/event-replay.ts +155 -0
  92. package/packages/control/src/supervision/events.ts +109 -0
  93. package/packages/control/src/supervision/index.ts +16 -0
  94. package/packages/control/src/supervision/manifest.ts +127 -0
  95. package/packages/control/src/supervision/paths.ts +49 -0
  96. package/packages/control/src/supervision/projection-adapter.ts +274 -0
  97. package/packages/control/src/supervision/projection.ts +75 -0
  98. package/packages/control/src/supervision/rebuild.ts +99 -0
  99. package/packages/control/src/supervision/session-open.ts +131 -0
  100. package/packages/control/src/supervision/session-read.ts +99 -0
  101. package/packages/control/src/supervision/sqlite-impl.ts +71 -0
  102. package/packages/control/src/supervision/sqlite.ts +121 -0
  103. package/packages/control/src/supervision/store-rows.ts +371 -0
  104. package/packages/control/src/supervision/turn-close.ts +154 -0
  105. package/packages/control/src/supervision/turn-open.ts +284 -0
  106. package/packages/control/src/sync/event-log-merge-driver.ts +263 -0
  107. package/packages/control/src/sync/join-plan.ts +375 -0
  108. package/packages/control/src/sync/outbox.ts +492 -0
  109. package/packages/control/src/workflow/evaluator.ts +140 -0
  110. package/packages/control/src/workflow/loader.ts +200 -0
  111. package/packages/control/src/workflow/types.ts +90 -0
  112. package/packages/control/src/workspace/authority.ts +499 -0
  113. package/packages/protocol/src/guards.ts +119 -0
  114. package/packages/protocol/src/huddle-board.ts +198 -0
  115. package/packages/protocol/src/huddle.ts +295 -0
  116. package/packages/protocol/src/incident.ts +251 -0
  117. package/packages/protocol/src/index.ts +8 -0
  118. package/packages/protocol/src/interfaces.ts +107 -0
  119. package/packages/protocol/src/packet-profile.ts +195 -0
  120. package/packages/protocol/src/state.ts +81 -0
  121. package/packages/protocol/src/types.ts +434 -0
  122. package/release/lineage.v0.json +15 -0
  123. package/scripts/release-candidate-verify.sh +175 -0
  124. package/scripts/release-checksum.sh +25 -0
  125. package/scripts/release-pack-canary.sh +97 -0
  126. package/scripts/release-scan.sh +249 -0
  127. package/scripts/release-sign.sh +34 -0
  128. package/templates/etiquette-vanilla-v0/README.md +2 -3
  129. package/templates/etiquette-vanilla-v0/source/control-seat/README.md +0 -6
  130. package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +0 -5
  131. package/templates/etiquette-vanilla-v0/validate-vanilla.sh +0 -5
  132. package/templates/seat-packs-v0/README.md +3 -2
  133. package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -29
  134. package/templates/seat-packs-v0/source/codex-seat/README.md +3 -17
  135. package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -23
  136. package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
  137. package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
  138. package/docs/AGENT_SPAWNING.md +0 -78
  139. package/docs/INTEGRATION_UI.md +0 -215
  140. package/docs/OPERATOR_PLAYBOOK.md +0 -77
  141. package/docs/TAG_ROUTE.md +0 -95
  142. package/lib/etiquette-core.js +0 -315
  143. package/lib/etiquette.js +0 -1160
  144. package/templates/DISPATCH_PREMISES.md +0 -39
  145. package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +0 -55
  146. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +0 -39
  147. package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +0 -53
  148. package/templates/hosted-receiver/README.md +0 -41
  149. package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +0 -129
@@ -0,0 +1,207 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, writeFile } from 'node:fs/promises';
3
+ import { dirname, posix, resolve } from 'node:path';
4
+ import { stringify as stringifyYaml } from 'yaml';
5
+ import { assertSharedMemoryScopeId, assertSharedMemoryText } from './sanitize';
6
+ import {
7
+ addMemoryStoreCandidate,
8
+ listMemoryStoreCandidateEntries,
9
+ loadMemoryStoreManifest,
10
+ readMemoryStoreCandidateContent,
11
+ type MemoryStoreEntry,
12
+ } from './store-lifecycle';
13
+ import type { EvidenceRef } from './types';
14
+
15
+ export interface MemoryDrainImportReceipt {
16
+ schema: 'memory-drain-import-receipt.v0';
17
+ receipt_id: string;
18
+ created_at: string;
19
+ source: {
20
+ workspace_ref: string;
21
+ store_id: string;
22
+ candidate_count: number;
23
+ };
24
+ target: {
25
+ workspace_ref: string;
26
+ store_id: string;
27
+ imported_count: number;
28
+ };
29
+ imported_entries: Array<{
30
+ source_entry_id: string;
31
+ source_content_ref: string;
32
+ target_entry_id: string;
33
+ target_content_ref: string;
34
+ target_path: string;
35
+ evidence_refs: EvidenceRef[];
36
+ }>;
37
+ authority_boundary: {
38
+ drain_can_authorize_work: false;
39
+ drain_can_promote_memory: false;
40
+ drain_can_write_parent_ledger: false;
41
+ runtime_pod_can_write_parent_ledger: false;
42
+ imported_entries_are_candidates: true;
43
+ source_store_mutated: false;
44
+ };
45
+ }
46
+
47
+ export interface DrainImportMemoryCandidatesOptions {
48
+ sourceProjectRoot: string;
49
+ targetProjectRoot: string;
50
+ sourceStoresRoot?: string;
51
+ targetStoresRoot?: string;
52
+ sourceStoreId: string;
53
+ targetStoreId: string;
54
+ sourceWorkspaceRef?: string;
55
+ targetWorkspaceRef?: string;
56
+ pathPrefix?: string;
57
+ receiptPath?: string;
58
+ at?: string;
59
+ }
60
+
61
+ function isoNow(at?: string): string {
62
+ const value = at ?? new Date().toISOString();
63
+ if (!Number.isFinite(Date.parse(value))) throw new Error('--at must be an ISO-8601 timestamp');
64
+ return new Date(value).toISOString();
65
+ }
66
+
67
+ function stableHash(value: unknown): string {
68
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16);
69
+ }
70
+
71
+ function assertOpaque(value: string, field: string): string {
72
+ assertSharedMemoryText(value, field);
73
+ if (value.startsWith('/') || value.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(value)) {
74
+ throw new Error(`${field} must be opaque or repo-relative, not a local absolute path`);
75
+ }
76
+ return value;
77
+ }
78
+
79
+ function assertStoreId(value: string, field: string): string {
80
+ assertSharedMemoryScopeId(value, field);
81
+ return value;
82
+ }
83
+
84
+ function assertPathPrefix(raw: string | undefined, sourceStoreId: string): string {
85
+ const value = raw ?? `imports/${sourceStoreId}`;
86
+ if (value.startsWith('/') || value.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(value)) {
87
+ throw new Error('--path-prefix must be repo-relative');
88
+ }
89
+ if (value.includes('\\')) throw new Error('--path-prefix must use POSIX / separators');
90
+ const normalized = posix.normalize(value);
91
+ if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
92
+ throw new Error('--path-prefix must stay inside the target store');
93
+ }
94
+ assertSharedMemoryText(normalized, 'path_prefix');
95
+ return normalized.replace(/\/+$/g, '');
96
+ }
97
+
98
+ function evidenceFor(entry: MemoryStoreEntry): EvidenceRef[] {
99
+ const refs: EvidenceRef[] = [...entry.evidence_refs, { type: 'doc', ref: `memory-entry:${entry.entry_id}` }];
100
+ const seen = new Set<string>();
101
+ return refs.filter((ref) => {
102
+ const key = `${ref.type}:${ref.ref}`;
103
+ if (seen.has(key)) return false;
104
+ seen.add(key);
105
+ return true;
106
+ });
107
+ }
108
+
109
+ export async function drainImportMemoryCandidates(
110
+ options: DrainImportMemoryCandidatesOptions,
111
+ ): Promise<MemoryDrainImportReceipt> {
112
+ const createdAt = isoNow(options.at);
113
+ const sourceProjectRoot = resolve(options.sourceProjectRoot);
114
+ const targetProjectRoot = resolve(options.targetProjectRoot);
115
+ const sourceStoreId = assertStoreId(options.sourceStoreId, 'source_store');
116
+ const targetStoreId = assertStoreId(options.targetStoreId, 'target_store');
117
+ const sourceWorkspaceRef = assertOpaque(options.sourceWorkspaceRef ?? 'child-workspace', 'source_workspace_ref');
118
+ const targetWorkspaceRef = assertOpaque(options.targetWorkspaceRef ?? 'parent-workspace', 'target_workspace_ref');
119
+ const pathPrefix = assertPathPrefix(options.pathPrefix, sourceStoreId);
120
+
121
+ await loadMemoryStoreManifest({
122
+ projectRoot: sourceProjectRoot,
123
+ storesRoot: options.sourceStoresRoot,
124
+ storeId: sourceStoreId,
125
+ });
126
+ const targetStore = await loadMemoryStoreManifest({
127
+ projectRoot: targetProjectRoot,
128
+ storesRoot: options.targetStoresRoot,
129
+ storeId: targetStoreId,
130
+ });
131
+ if (targetStore.access !== 'read_write' || targetStore.write_policy === 'none') {
132
+ throw new Error(`target memory store ${targetStoreId} must be read_write`);
133
+ }
134
+
135
+ const sourceEntries = await listMemoryStoreCandidateEntries({
136
+ projectRoot: sourceProjectRoot,
137
+ storesRoot: options.sourceStoresRoot,
138
+ storeId: sourceStoreId,
139
+ });
140
+ const imported: MemoryDrainImportReceipt['imported_entries'] = [];
141
+
142
+ for (const entry of sourceEntries) {
143
+ const content = await readMemoryStoreCandidateContent({
144
+ projectRoot: sourceProjectRoot,
145
+ storesRoot: options.sourceStoresRoot,
146
+ storeId: sourceStoreId,
147
+ entry,
148
+ });
149
+ const evidenceRefs = evidenceFor(entry);
150
+ const targetEntry = await addMemoryStoreCandidate({
151
+ projectRoot: targetProjectRoot,
152
+ storesRoot: options.targetStoresRoot,
153
+ storeId: targetStoreId,
154
+ path: `${pathPrefix}/${entry.path}`,
155
+ content,
156
+ evidenceRefs,
157
+ at: createdAt,
158
+ });
159
+ imported.push({
160
+ source_entry_id: entry.entry_id,
161
+ source_content_ref: entry.content_ref,
162
+ target_entry_id: targetEntry.entry_id,
163
+ target_content_ref: targetEntry.content_ref,
164
+ target_path: targetEntry.path,
165
+ evidence_refs: evidenceRefs,
166
+ });
167
+ }
168
+
169
+ const receipt: MemoryDrainImportReceipt = {
170
+ schema: 'memory-drain-import-receipt.v0',
171
+ receipt_id: `memory-drain-import:${stableHash({
172
+ sourceWorkspaceRef,
173
+ targetWorkspaceRef,
174
+ sourceStoreId,
175
+ targetStoreId,
176
+ imported: imported.map((entry) => `${entry.source_entry_id}->${entry.target_entry_id}`),
177
+ })}`,
178
+ created_at: createdAt,
179
+ source: {
180
+ workspace_ref: sourceWorkspaceRef,
181
+ store_id: sourceStoreId,
182
+ candidate_count: sourceEntries.length,
183
+ },
184
+ target: {
185
+ workspace_ref: targetWorkspaceRef,
186
+ store_id: targetStoreId,
187
+ imported_count: imported.length,
188
+ },
189
+ imported_entries: imported,
190
+ authority_boundary: {
191
+ drain_can_authorize_work: false,
192
+ drain_can_promote_memory: false,
193
+ drain_can_write_parent_ledger: false,
194
+ runtime_pod_can_write_parent_ledger: false,
195
+ imported_entries_are_candidates: true,
196
+ source_store_mutated: false,
197
+ },
198
+ };
199
+
200
+ if (options.receiptPath) {
201
+ const receiptPath = resolve(options.receiptPath);
202
+ await mkdir(dirname(receiptPath), { recursive: true });
203
+ await writeFile(receiptPath, stringifyYaml(receipt));
204
+ }
205
+
206
+ return receipt;
207
+ }
@@ -0,0 +1,284 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { readdir, readFile, writeFile, mkdir } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { basename, dirname, extname, join, relative, resolve } from 'node:path';
6
+ import {
7
+ assertCapsuleIsSharedSafe,
8
+ assertSharedMemoryScopeId,
9
+ detectMemoryRiskFlags,
10
+ validateCandidateForPromotion,
11
+ } from './sanitize';
12
+ import { openMemoryStore } from './store';
13
+ import type {
14
+ EvidenceRef,
15
+ MemoryCandidate,
16
+ MemoryCategory,
17
+ MemoryIndexResult,
18
+ MemoryRiskFlag,
19
+ RecallCapsule,
20
+ } from './types';
21
+
22
+ export const APPROVED_SCAN_DIRS = [
23
+ 'docs/work/ledger',
24
+ 'docs/work/tasks',
25
+ 'docs/work/playbacks',
26
+ 'internal/docs/playbacks',
27
+ 'internal/docs/contracts',
28
+ '.etiquette/events',
29
+ ];
30
+
31
+ const TEXT_EXTENSIONS = new Set(['.md', '.yaml', '.yml', '.json', '.jsonl', '.ndjson', '.txt']);
32
+ const LOCAL_SEARCH_TEXT_MAX = 8000;
33
+
34
+ interface IndexedMemoryCandidate {
35
+ candidate: MemoryCandidate;
36
+ searchText: string;
37
+ }
38
+
39
+ export function defaultMemoryDbPath(projectRoot: string): string {
40
+ return join(projectRoot, '.etiquette', 'state', 'memory', 'index.db');
41
+ }
42
+
43
+ function safePathSegment(value: string): string {
44
+ const cleaned = value.replace(/[^A-Za-z0-9_.:-]+/g, '-').replace(/^-+|-+$/g, '');
45
+ return cleaned || 'workspace';
46
+ }
47
+
48
+ export function defaultMemoryExportDir(projectRoot: string, workspaceId?: string, now = new Date()): string {
49
+ const workspace = safePathSegment(workspaceId ?? basename(resolve(projectRoot)));
50
+ const day = now.toISOString().slice(0, 10);
51
+ return join(homedir(), '.etiquette', 'exports', workspace, day);
52
+ }
53
+
54
+ function sha(input: string): string {
55
+ return createHash('sha256').update(input).digest('hex').slice(0, 16);
56
+ }
57
+
58
+ function compactWhitespace(value: string): string {
59
+ return value.replace(/\s+/g, ' ').trim();
60
+ }
61
+
62
+ function cleanSummary(value: string): string {
63
+ const compact = compactWhitespace(value.replace(/[`*_>#-]/g, ' '));
64
+ return compact.length > 260 ? `${compact.slice(0, 257)}...` : compact;
65
+ }
66
+
67
+ function firstHeading(content: string, fallback: string): string {
68
+ const heading = content.split('\n').find((line) => line.startsWith('# '));
69
+ if (!heading) return fallback;
70
+ return compactWhitespace(heading.replace(/^#+\s*/, ''));
71
+ }
72
+
73
+ function firstParagraph(content: string, fallback: string): string {
74
+ const paragraph = content
75
+ .split(/\n\s*\n/)
76
+ .map((part) => part.trim())
77
+ .find((part) => part.length > 0 && !part.startsWith('#') && !part.startsWith('```'));
78
+ return cleanSummary(paragraph ?? fallback);
79
+ }
80
+
81
+ function sanitizeLocalSearchText(content: string): string {
82
+ const kept: string[] = [];
83
+ for (const line of content.split('\n')) {
84
+ const withoutFenceMarker = line.replace(/```[A-Za-z0-9_-]*/g, ' ');
85
+ const restricted = detectMemoryRiskFlags(withoutFenceMarker).filter((flag) => flag !== 'contains_source_snippet');
86
+ if (restricted.length > 0) continue;
87
+ const normalized = withoutFenceMarker.replace(/[`*_>#|\[\]{}()]/g, ' ');
88
+ const compact = compactWhitespace(normalized);
89
+ if (compact.length > 0) kept.push(compact);
90
+ }
91
+ const searchText = compactWhitespace(kept.join(' '));
92
+ return searchText.length > LOCAL_SEARCH_TEXT_MAX ? searchText.slice(0, LOCAL_SEARCH_TEXT_MAX) : searchText;
93
+ }
94
+
95
+ function categoryForPath(relPath: string): MemoryCategory {
96
+ if (relPath.includes('/playbacks/')) return 'playback_candidate';
97
+ if (relPath.includes('/ledger/') || relPath.includes('/tasks/')) return 'pattern';
98
+ if (relPath.includes('hosted-pass-plane') || relPath.includes('event-spine')) return 'decision';
99
+ if (relPath.includes('workflow-memory')) return 'adapter_note';
100
+ return 'onboarding';
101
+ }
102
+
103
+ function evidenceForPath(relPath: string): EvidenceRef {
104
+ if (relPath.includes('/ledger/')) return { type: 'ledger', ref: relPath };
105
+ if (relPath.includes('/playbacks/')) return { type: 'playback', ref: relPath };
106
+ if (relPath.includes('.etiquette/events/')) return { type: 'event', ref: relPath };
107
+ return { type: 'doc', ref: relPath };
108
+ }
109
+
110
+ async function collectFiles(root: string, relDir: string): Promise<string[]> {
111
+ const absDir = join(root, relDir);
112
+ if (!existsSync(absDir)) return [];
113
+ const out: string[] = [];
114
+ async function walk(dir: string): Promise<void> {
115
+ const entries = await readdir(dir, { withFileTypes: true });
116
+ for (const entry of entries) {
117
+ const abs = join(dir, entry.name);
118
+ if (entry.isDirectory()) {
119
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '_worktrees') continue;
120
+ await walk(abs);
121
+ continue;
122
+ }
123
+ if (!entry.isFile()) continue;
124
+ if (!TEXT_EXTENSIONS.has(extname(entry.name))) continue;
125
+ out.push(relative(root, abs));
126
+ }
127
+ }
128
+ await walk(absDir);
129
+ return out.sort();
130
+ }
131
+
132
+ export async function collectApprovedMemoryFiles(projectRoot: string): Promise<string[]> {
133
+ const batches = await Promise.all(APPROVED_SCAN_DIRS.map((dir) => collectFiles(projectRoot, dir)));
134
+ return batches.flat().sort();
135
+ }
136
+
137
+ export async function candidateFromFile(projectRoot: string, relPath: string): Promise<MemoryCandidate> {
138
+ return (await indexedCandidateFromFile(projectRoot, relPath)).candidate;
139
+ }
140
+
141
+ async function indexedCandidateFromFile(projectRoot: string, relPath: string): Promise<IndexedMemoryCandidate> {
142
+ const abs = join(projectRoot, relPath);
143
+ const content = await readFile(abs, 'utf-8');
144
+ const fallbackTitle = basename(relPath, extname(relPath)).replace(/[-_]+/g, ' ');
145
+ const title = firstHeading(content, fallbackTitle);
146
+ const summary = firstParagraph(content, title);
147
+ const riskFlags = new Set<MemoryRiskFlag>();
148
+ for (const flag of detectMemoryRiskFlags(`${title}\n${summary}`)) {
149
+ riskFlags.add(flag);
150
+ }
151
+ const stat = statSync(abs);
152
+ return {
153
+ candidate: {
154
+ candidate_id: `cand_${sha(`${relPath}:${stat.mtimeMs}:${stat.size}`)}`,
155
+ adapter: 'local_importer',
156
+ query: relPath,
157
+ category: categoryForPath(relPath),
158
+ title,
159
+ summary,
160
+ source_refs: [evidenceForPath(relPath)],
161
+ confidence: 'medium',
162
+ risk_flags: [...riskFlags],
163
+ },
164
+ searchText: sanitizeLocalSearchText(content),
165
+ };
166
+ }
167
+
168
+ export function promoteCandidate(
169
+ candidate: MemoryCandidate,
170
+ opts: { localPrivate?: boolean; now?: string; scope?: RecallCapsule['scope'] } = {},
171
+ ): RecallCapsule | null {
172
+ const flags = validateCandidateForPromotion(candidate, opts);
173
+ const blocking = flags.filter((flag) => flag !== 'stale' && flag !== 'contradicts_current_ledger');
174
+ if (blocking.length > 0) return null;
175
+ const now = opts.now ?? new Date().toISOString();
176
+ const stale = flags.includes('stale') || flags.includes('contradicts_current_ledger');
177
+ const capsule: RecallCapsule = {
178
+ schema: 'recall-capsule.v0',
179
+ capsule_id: `mem_${sha(`${candidate.title}:${candidate.source_refs.map((ref) => ref.ref).join('|')}`)}`,
180
+ created_at: now,
181
+ updated_at: now,
182
+ scope: opts.scope ?? {},
183
+ category: candidate.category,
184
+ title: candidate.title,
185
+ summary: candidate.summary,
186
+ why_it_matters: `Imported from ${candidate.source_refs[0]?.ref}; use as cited context, not authority.`,
187
+ evidence_refs: candidate.source_refs,
188
+ confidence: candidate.confidence,
189
+ staleness: {
190
+ status: stale ? 'watch' : 'current',
191
+ stale_if: stale ? 'current ledger or receipt contradicts this capsule' : undefined,
192
+ },
193
+ recommended_action: {
194
+ type: candidate.category === 'playback_candidate' ? 'run_playback' : 'read_before_work',
195
+ },
196
+ authority_boundary: {
197
+ can_authorize_work: false,
198
+ can_close_task: false,
199
+ can_replace_receipt: false,
200
+ },
201
+ };
202
+ assertCapsuleIsSharedSafe(capsule);
203
+ return capsule;
204
+ }
205
+
206
+ export async function indexMemory(options: {
207
+ projectRoot: string;
208
+ dbPath?: string;
209
+ exportDir?: string;
210
+ localPrivate?: boolean;
211
+ orgId?: string;
212
+ workspaceId?: string;
213
+ confirmSharedExport?: boolean;
214
+ }): Promise<MemoryIndexResult> {
215
+ const projectRoot = resolve(options.projectRoot);
216
+ const dbPath = options.dbPath ?? defaultMemoryDbPath(projectRoot);
217
+ const sharedExport = options.exportDir !== undefined;
218
+ if (sharedExport) {
219
+ if (options.localPrivate) {
220
+ throw new Error('--local-private cannot be combined with shared capsule export');
221
+ }
222
+ if (!options.confirmSharedExport) {
223
+ throw new Error('--confirm-shared-export is required when writing shared capsule exports');
224
+ }
225
+ if (!options.orgId) {
226
+ throw new Error('--org-id is required when writing shared capsule exports');
227
+ }
228
+ assertSharedMemoryScopeId(options.orgId, '--org-id');
229
+ if (options.workspaceId) assertSharedMemoryScopeId(options.workspaceId, '--workspace-id');
230
+ }
231
+ const store = openMemoryStore(dbPath);
232
+ try {
233
+ store.reset();
234
+ const files = await collectApprovedMemoryFiles(projectRoot);
235
+ const capsuleIds: string[] = [];
236
+ let rejected = 0;
237
+ for (const relPath of files) {
238
+ const { candidate, searchText } = await indexedCandidateFromFile(projectRoot, relPath);
239
+ store.insertCandidate(candidate);
240
+ const flags = validateCandidateForPromotion(candidate, {
241
+ localPrivate: options.localPrivate,
242
+ });
243
+ const blocking = flags.filter((flag) => flag !== 'stale' && flag !== 'contradicts_current_ledger');
244
+ if (blocking.length > 0) {
245
+ rejected += 1;
246
+ store.markCandidate(candidate.candidate_id, 'rejected', blocking);
247
+ continue;
248
+ }
249
+ if (sharedExport && candidate.confidence === 'low') {
250
+ rejected += 1;
251
+ store.markCandidate(candidate.candidate_id, 'rejected', ['confidence_low_for_shared_export']);
252
+ continue;
253
+ }
254
+ const capsule = promoteCandidate(candidate, {
255
+ localPrivate: options.localPrivate,
256
+ scope: sharedExport ? {
257
+ org_id: options.orgId,
258
+ workspace_id: options.workspaceId,
259
+ } : {},
260
+ });
261
+ if (!capsule) {
262
+ rejected += 1;
263
+ store.markCandidate(candidate.candidate_id, 'rejected', ['no_durable_evidence']);
264
+ continue;
265
+ }
266
+ store.markCandidate(candidate.candidate_id, 'promoted', []);
267
+ store.insertCapsule(capsule, searchText);
268
+ capsuleIds.push(capsule.capsule_id);
269
+ if (options.exportDir) {
270
+ const target = join(options.exportDir, `${capsule.capsule_id}.json`);
271
+ await mkdir(dirname(target), { recursive: true });
272
+ await writeFile(target, `${JSON.stringify(capsule, null, 2)}\n`, 'utf-8');
273
+ }
274
+ }
275
+ return {
276
+ candidates: files.length,
277
+ promoted: capsuleIds.length,
278
+ rejected,
279
+ capsuleIds,
280
+ };
281
+ } finally {
282
+ store.close();
283
+ }
284
+ }
@@ -0,0 +1,75 @@
1
+ import { resolve } from 'node:path';
2
+ import { defaultMemoryDbPath } from './indexer';
3
+ import { openMemoryStore } from './store';
4
+ import type { CompactMemoryResult, RecallCapsule, RejectedMemoryCandidate } from './types';
5
+
6
+ export function searchMemory(options: {
7
+ projectRoot: string;
8
+ dbPath?: string;
9
+ query: string;
10
+ limit: number;
11
+ }): CompactMemoryResult[] {
12
+ const projectRoot = resolve(options.projectRoot);
13
+ const store = openMemoryStore(options.dbPath ?? defaultMemoryDbPath(projectRoot));
14
+ try {
15
+ return store.search(options.query, options.limit);
16
+ } finally {
17
+ store.close();
18
+ }
19
+ }
20
+
21
+ export function showMemory(options: {
22
+ projectRoot: string;
23
+ dbPath?: string;
24
+ capsuleId: string;
25
+ }): RecallCapsule | null {
26
+ const projectRoot = resolve(options.projectRoot);
27
+ const store = openMemoryStore(options.dbPath ?? defaultMemoryDbPath(projectRoot));
28
+ try {
29
+ return store.show(options.capsuleId);
30
+ } finally {
31
+ store.close();
32
+ }
33
+ }
34
+
35
+ export function staleMemory(options: {
36
+ projectRoot: string;
37
+ dbPath?: string;
38
+ limit: number;
39
+ }): CompactMemoryResult[] {
40
+ const projectRoot = resolve(options.projectRoot);
41
+ const store = openMemoryStore(options.dbPath ?? defaultMemoryDbPath(projectRoot));
42
+ try {
43
+ return store.stale(options.limit);
44
+ } finally {
45
+ store.close();
46
+ }
47
+ }
48
+
49
+ export function rejectedMemory(options: {
50
+ projectRoot: string;
51
+ dbPath?: string;
52
+ limit: number;
53
+ }): RejectedMemoryCandidate[] {
54
+ const projectRoot = resolve(options.projectRoot);
55
+ const store = openMemoryStore(options.dbPath ?? defaultMemoryDbPath(projectRoot));
56
+ try {
57
+ return store.rejected(options.limit);
58
+ } finally {
59
+ store.close();
60
+ }
61
+ }
62
+
63
+ export function preflightMemory(options: {
64
+ projectRoot: string;
65
+ dbPath?: string;
66
+ topic: string;
67
+ limit: number;
68
+ }): CompactMemoryResult[] {
69
+ return searchMemory({
70
+ projectRoot: options.projectRoot,
71
+ dbPath: options.dbPath,
72
+ query: options.topic,
73
+ limit: options.limit,
74
+ });
75
+ }
@@ -0,0 +1,50 @@
1
+ import {
2
+ assertSharedMemoryScopeId,
3
+ assertSharedMemoryText,
4
+ detectMemoryRiskFlags,
5
+ } from '../shared/sanitize';
6
+ import type { MemoryCandidate, MemoryRiskFlag, RecallCapsule } from './types';
7
+
8
+ export {
9
+ assertSharedMemoryScopeId,
10
+ assertSharedMemoryText,
11
+ detectMemoryRiskFlags,
12
+ } from '../shared/sanitize';
13
+
14
+ export function validateEvidenceRefs(candidate: MemoryCandidate): MemoryRiskFlag[] {
15
+ if (candidate.source_refs.length === 0) return ['no_durable_evidence'];
16
+ const badRefs = candidate.source_refs.flatMap((ref) => detectMemoryRiskFlags(ref.ref));
17
+ return badRefs;
18
+ }
19
+
20
+ export function validateCandidateForPromotion(
21
+ candidate: MemoryCandidate,
22
+ opts: { localPrivate?: boolean } = {},
23
+ ): MemoryRiskFlag[] {
24
+ const flags = new Set<MemoryRiskFlag>(candidate.risk_flags);
25
+ for (const flag of validateEvidenceRefs(candidate)) {
26
+ flags.add(flag);
27
+ }
28
+ for (const value of [candidate.title, candidate.summary, candidate.query]) {
29
+ for (const flag of detectMemoryRiskFlags(value)) {
30
+ flags.add(flag);
31
+ }
32
+ }
33
+ if (opts.localPrivate) {
34
+ flags.delete('contains_private_context');
35
+ }
36
+ return [...flags];
37
+ }
38
+
39
+ export function assertCapsuleIsSharedSafe(capsule: RecallCapsule): void {
40
+ if (capsule.scope.org_id) assertSharedMemoryScopeId(capsule.scope.org_id, 'scope.org_id');
41
+ if (capsule.scope.workspace_id) assertSharedMemoryScopeId(capsule.scope.workspace_id, 'scope.workspace_id');
42
+ if (capsule.scope.repo_ref) assertSharedMemoryText(capsule.scope.repo_ref, 'scope.repo_ref');
43
+ if (capsule.scope.lane_ref) assertSharedMemoryText(capsule.scope.lane_ref, 'scope.lane_ref');
44
+ assertSharedMemoryText(capsule.title, 'title');
45
+ assertSharedMemoryText(capsule.summary, 'summary');
46
+ assertSharedMemoryText(capsule.why_it_matters, 'why_it_matters');
47
+ for (const evidence of capsule.evidence_refs) {
48
+ assertSharedMemoryText(evidence.ref, `evidence_ref:${evidence.type}`);
49
+ }
50
+ }