contextgit 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.d.ts +10 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +43 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/commands/branch.d.ts +13 -0
- package/dist/commands/branch.d.ts.map +1 -0
- package/dist/commands/branch.js +52 -0
- package/dist/commands/branch.js.map +1 -0
- package/dist/commands/claim.d.ts +13 -0
- package/dist/commands/claim.d.ts.map +1 -0
- package/dist/commands/claim.js +50 -0
- package/dist/commands/claim.js.map +1 -0
- package/dist/commands/commit.d.ts +14 -0
- package/dist/commands/commit.d.ts.map +1 -0
- package/dist/commands/commit.js +71 -0
- package/dist/commands/commit.js.map +1 -0
- package/dist/commands/context.d.ts +9 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +38 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +84 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +126 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/keygen.d.ts +10 -0
- package/dist/commands/keygen.d.ts.map +1 -0
- package/dist/commands/keygen.js +57 -0
- package/dist/commands/keygen.js.map +1 -0
- package/dist/commands/log.d.ts +13 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +91 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/merge.d.ts +12 -0
- package/dist/commands/merge.d.ts.map +1 -0
- package/dist/commands/merge.js +29 -0
- package/dist/commands/merge.js.map +1 -0
- package/dist/commands/pull.d.ts +10 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +123 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/push.d.ts +10 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/push.js +141 -0
- package/dist/commands/push.js.map +1 -0
- package/dist/commands/remote-show.d.ts +6 -0
- package/dist/commands/remote-show.d.ts.map +1 -0
- package/dist/commands/remote-show.js +71 -0
- package/dist/commands/remote-show.js.map +1 -0
- package/dist/commands/search.d.ts +11 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +47 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/serve.d.ts +9 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +51 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/set-remote.d.ts +9 -0
- package/dist/commands/set-remote.d.ts.map +1 -0
- package/dist/commands/set-remote.js +26 -0
- package/dist/commands/set-remote.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +54 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/unclaim.d.ts +9 -0
- package/dist/commands/unclaim.d.ts.map +1 -0
- package/dist/commands/unclaim.js +22 -0
- package/dist/commands/unclaim.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -0
- package/dist/git-hooks.d.ts +6 -0
- package/dist/git-hooks.d.ts.map +1 -0
- package/dist/git-hooks.js +58 -0
- package/dist/git-hooks.js.map +1 -0
- package/package.json +22 -19
- package/.claude/settings.local.json +0 -41
- package/.contextgit/config.json +0 -10
- package/.contextgit/system-prompt.md +0 -4
- package/.github/workflows/contextgit-ci.yml +0 -40
- package/CLAUDE.md +0 -123
- package/CLAUDE.md.next +0 -65
- package/docs/ContextGit_ARCHITECTURE_v3.md +0 -1141
- package/docs/ContextGit_DELTA.md +0 -84
- package/docs/ContextGit_PHASE1_PLAN.md +0 -177
- package/docs/ContextGit_PHASE2_PLAN.md +0 -535
- package/docs/ContextGit_PRD_v4.md +0 -488
- package/docs/decisions.md +0 -370
- package/packages/api/package.json +0 -25
- package/packages/api/src/bootstrap.ts +0 -64
- package/packages/api/src/config.ts +0 -45
- package/packages/api/src/index.ts +0 -17
- package/packages/api/src/middleware/auth.test.ts +0 -83
- package/packages/api/src/middleware/auth.ts +0 -41
- package/packages/api/src/remote-store.test.ts +0 -301
- package/packages/api/src/router.ts +0 -121
- package/packages/api/src/server-config.ts +0 -34
- package/packages/api/src/server.ts +0 -38
- package/packages/api/src/store-router.ts +0 -241
- package/packages/api/tsconfig.json +0 -8
- package/packages/cli/package.json +0 -29
- package/packages/cli/src/bootstrap.ts +0 -68
- package/packages/cli/src/commands/branch.ts +0 -58
- package/packages/cli/src/commands/claim.ts +0 -58
- package/packages/cli/src/commands/commit.ts +0 -79
- package/packages/cli/src/commands/context.ts +0 -46
- package/packages/cli/src/commands/doctor.ts +0 -99
- package/packages/cli/src/commands/init.ts +0 -141
- package/packages/cli/src/commands/keygen.ts +0 -65
- package/packages/cli/src/commands/log.ts +0 -103
- package/packages/cli/src/commands/merge.ts +0 -36
- package/packages/cli/src/commands/pull.ts +0 -145
- package/packages/cli/src/commands/push.ts +0 -158
- package/packages/cli/src/commands/remote-show.ts +0 -87
- package/packages/cli/src/commands/search.ts +0 -54
- package/packages/cli/src/commands/serve.ts +0 -61
- package/packages/cli/src/commands/set-remote.ts +0 -30
- package/packages/cli/src/commands/status.ts +0 -62
- package/packages/cli/src/commands/unclaim.ts +0 -28
- package/packages/cli/src/config.ts +0 -64
- package/packages/cli/src/git-hooks.ts +0 -61
- package/packages/cli/tsconfig.json +0 -9
- package/packages/core/package.json +0 -28
- package/packages/core/src/embeddings.test.ts +0 -58
- package/packages/core/src/embeddings.ts +0 -75
- package/packages/core/src/engine.ts +0 -274
- package/packages/core/src/index.ts +0 -6
- package/packages/core/src/snapshot.ts +0 -82
- package/packages/core/src/summarizer.test.ts +0 -120
- package/packages/core/src/summarizer.ts +0 -113
- package/packages/core/src/threads.ts +0 -29
- package/packages/core/src/types.ts +0 -240
- package/packages/core/tsconfig.json +0 -9
- package/packages/mcp/package.json +0 -31
- package/packages/mcp/src/auto-snapshot.ts +0 -83
- package/packages/mcp/src/config.ts +0 -53
- package/packages/mcp/src/git-sync.ts +0 -94
- package/packages/mcp/src/index.ts +0 -19
- package/packages/mcp/src/server.ts +0 -377
- package/packages/mcp/tsconfig.json +0 -9
- package/packages/store/package.json +0 -30
- package/packages/store/src/branch-merge.test.ts +0 -127
- package/packages/store/src/engine-integration.test.ts +0 -93
- package/packages/store/src/index.ts +0 -3
- package/packages/store/src/interface.ts +0 -62
- package/packages/store/src/local/claims.test.ts +0 -190
- package/packages/store/src/local/index.ts +0 -380
- package/packages/store/src/local/local-store.test.ts +0 -164
- package/packages/store/src/local/migrations.ts +0 -99
- package/packages/store/src/local/queries.ts +0 -760
- package/packages/store/src/local/schema.ts +0 -157
- package/packages/store/src/remote/index.ts +0 -300
- package/packages/store/tsconfig.json +0 -9
- package/pnpm-workspace.yaml +0 -2
- package/scripts/build.sh +0 -28
- package/tsconfig.base.json +0 -14
- package/vitest.config.ts +0 -15
- /package/{packages/cli/bin → bin}/run.js +0 -0
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { EmbeddingService } from './embeddings.js'
|
|
3
|
-
|
|
4
|
-
// Fake pipeline that returns a predictable 384-dim vector
|
|
5
|
-
function makeFakePipeline() {
|
|
6
|
-
return async (_text: string, _opts: unknown) => ({
|
|
7
|
-
data: new Float32Array(384).fill(0.5),
|
|
8
|
-
})
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function makeBrokenPipeline() {
|
|
12
|
-
return Promise.reject(new Error('model load failed'))
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('EmbeddingService', () => {
|
|
16
|
-
it('returns a Float32Array of length 384 on success', async () => {
|
|
17
|
-
const svc = new EmbeddingService({
|
|
18
|
-
pipelineFactory: async () => makeFakePipeline(),
|
|
19
|
-
})
|
|
20
|
-
const vec = await svc.embed('hello world')
|
|
21
|
-
expect(vec).toBeInstanceOf(Float32Array)
|
|
22
|
-
expect(vec!.length).toBe(384)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('returns null when pipeline load fails', async () => {
|
|
26
|
-
const svc = new EmbeddingService({
|
|
27
|
-
pipelineFactory: async () => { throw new Error('load error') },
|
|
28
|
-
})
|
|
29
|
-
const vec = await svc.embed('hello')
|
|
30
|
-
expect(vec).toBeNull()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('returns null when pipeline call throws', async () => {
|
|
34
|
-
const svc = new EmbeddingService({
|
|
35
|
-
pipelineFactory: async () => {
|
|
36
|
-
return async () => { throw new Error('inference error') }
|
|
37
|
-
},
|
|
38
|
-
})
|
|
39
|
-
const vec = await svc.embed('hello')
|
|
40
|
-
expect(vec).toBeNull()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('never throws — always returns null on any error', async () => {
|
|
44
|
-
const svc = new EmbeddingService({
|
|
45
|
-
pipelineFactory: () => makeBrokenPipeline() as never,
|
|
46
|
-
})
|
|
47
|
-
await expect(svc.embed('anything')).resolves.toBeNull()
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('loads the pipeline only once across multiple embeds', async () => {
|
|
51
|
-
const factory = vi.fn(async () => makeFakePipeline())
|
|
52
|
-
const svc = new EmbeddingService({ pipelineFactory: factory })
|
|
53
|
-
await svc.embed('a')
|
|
54
|
-
await svc.embed('b')
|
|
55
|
-
await svc.embed('c')
|
|
56
|
-
expect(factory).toHaveBeenCalledTimes(1)
|
|
57
|
-
})
|
|
58
|
-
})
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// EmbeddingService — generates 384-dim sentence embeddings using
|
|
2
|
-
// @xenova/transformers (all-MiniLM-L6-v2, runs fully local, no API key).
|
|
3
|
-
//
|
|
4
|
-
// Usage:
|
|
5
|
-
// const svc = new EmbeddingService()
|
|
6
|
-
// const vector = await svc.embed('some text') // Float32Array | null
|
|
7
|
-
//
|
|
8
|
-
// Load failure is silently swallowed — callers receive null and should fall
|
|
9
|
-
// back to full-text search. Never let embedding errors propagate outward.
|
|
10
|
-
|
|
11
|
-
type PipelineFn = (text: string, options: { pooling: string; normalize: boolean }) => Promise<{ data: Float32Array }>
|
|
12
|
-
|
|
13
|
-
export interface EmbeddingServiceOptions {
|
|
14
|
-
/** Override the pipeline factory — for tests. */
|
|
15
|
-
pipelineFactory?: (task: string, model: string) => Promise<PipelineFn>
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class EmbeddingService {
|
|
19
|
-
private pipeline: PipelineFn | null = null
|
|
20
|
-
private loadPromise: Promise<void> | null = null
|
|
21
|
-
private readonly pipelineFactory: (task: string, model: string) => Promise<PipelineFn>
|
|
22
|
-
|
|
23
|
-
static readonly MODEL = 'Xenova/all-MiniLM-L6-v2'
|
|
24
|
-
static readonly DIMS = 384
|
|
25
|
-
|
|
26
|
-
constructor(options: EmbeddingServiceOptions = {}) {
|
|
27
|
-
this.pipelineFactory = options.pipelineFactory ?? EmbeddingService.defaultPipelineFactory
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Generate a 384-dim embedding for `text`.
|
|
32
|
-
* Returns null if the model is unavailable or any error occurs.
|
|
33
|
-
*/
|
|
34
|
-
async embed(text: string): Promise<Float32Array | null> {
|
|
35
|
-
try {
|
|
36
|
-
await this.ensureLoaded()
|
|
37
|
-
if (!this.pipeline) return null
|
|
38
|
-
const result = await this.pipeline(text, { pooling: 'mean', normalize: true })
|
|
39
|
-
return result.data
|
|
40
|
-
} catch {
|
|
41
|
-
return null
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** True once the model has been loaded at least once (even if it failed). */
|
|
46
|
-
get isReady(): boolean {
|
|
47
|
-
return this.loadPromise !== null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private async ensureLoaded(): Promise<void> {
|
|
51
|
-
if (this.pipeline) return
|
|
52
|
-
if (!this.loadPromise) {
|
|
53
|
-
this.loadPromise = this.load()
|
|
54
|
-
}
|
|
55
|
-
await this.loadPromise
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private async load(): Promise<void> {
|
|
59
|
-
try {
|
|
60
|
-
this.pipeline = await this.pipelineFactory('feature-extraction', EmbeddingService.MODEL)
|
|
61
|
-
} catch {
|
|
62
|
-
this.pipeline = null
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private static async defaultPipelineFactory(_task: string, model: string): Promise<PipelineFn> {
|
|
67
|
-
// Dynamic import so the heavy @xenova/transformers bundle is only loaded
|
|
68
|
-
// when embeddings are actually needed.
|
|
69
|
-
const { pipeline } = await import('@xenova/transformers')
|
|
70
|
-
// Cast task to any to avoid strict PipelineType enum mismatch — we always
|
|
71
|
-
// pass 'feature-extraction' which is valid at runtime.
|
|
72
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
-
return pipeline('feature-extraction' as any, model) as unknown as PipelineFn
|
|
74
|
-
}
|
|
75
|
-
}
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
// ContextEngine — pure business logic layer, no I/O.
|
|
2
|
-
// All persistence is delegated to the injected ContextStore.
|
|
3
|
-
//
|
|
4
|
-
// Usage:
|
|
5
|
-
// const engine = new ContextEngine(store, 'agent-1', 'dev', 'claude-code', 'interactive')
|
|
6
|
-
// await engine.init(projectId, branchId)
|
|
7
|
-
// await engine.commit({ message: '...', content: '...' })
|
|
8
|
-
// const snapshot = await engine.context('global')
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
AgentRole,
|
|
12
|
-
Branch,
|
|
13
|
-
Commit,
|
|
14
|
-
CommitType,
|
|
15
|
-
ContextScope,
|
|
16
|
-
SearchResult,
|
|
17
|
-
SessionSnapshot,
|
|
18
|
-
WorkflowType,
|
|
19
|
-
} from './types.js'
|
|
20
|
-
import { RollingSummarizer } from './summarizer.js'
|
|
21
|
-
import { EmbeddingService } from './embeddings.js'
|
|
22
|
-
|
|
23
|
-
// ─── Store subset the engine requires ─────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
interface EngineBranchInput {
|
|
26
|
-
projectId: string
|
|
27
|
-
name: string
|
|
28
|
-
gitBranch: string
|
|
29
|
-
parentBranchId?: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface EngineStore {
|
|
33
|
-
getBranch(id: string): Promise<{ headCommitId?: string; gitBranch?: string; name?: string } | null>
|
|
34
|
-
getCommit(id: string): Promise<{ summary: string } | null>
|
|
35
|
-
createBranch(input: EngineBranchInput): Promise<Branch>
|
|
36
|
-
createCommit(input: EngineCommitStoreInput): Promise<Commit>
|
|
37
|
-
mergeBranch(sourceBranchId: string, targetBranchId: string, summary: string): Promise<Commit>
|
|
38
|
-
getSessionSnapshot(projectId: string, branchId: string): Promise<SessionSnapshot>
|
|
39
|
-
upsertAgent(agent: EngineAgentInput): Promise<unknown>
|
|
40
|
-
indexEmbedding(commitId: string, vector: Float32Array): Promise<void>
|
|
41
|
-
semanticSearch(vector: Float32Array, projectId: string, limit: number): Promise<SearchResult[]>
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface EngineCommitStoreInput {
|
|
45
|
-
branchId: string
|
|
46
|
-
agentId: string
|
|
47
|
-
agentRole: AgentRole
|
|
48
|
-
tool: string
|
|
49
|
-
workflowType: WorkflowType
|
|
50
|
-
message: string
|
|
51
|
-
content: string
|
|
52
|
-
summary: string
|
|
53
|
-
commitType: CommitType
|
|
54
|
-
gitCommitSha?: string
|
|
55
|
-
ciRunId?: string
|
|
56
|
-
pipelineName?: string
|
|
57
|
-
threads?: {
|
|
58
|
-
open?: string[]
|
|
59
|
-
close?: Array<{ id: string; note: string }>
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface EngineAgentInput {
|
|
64
|
-
id: string
|
|
65
|
-
projectId: string
|
|
66
|
-
role: AgentRole
|
|
67
|
-
tool: string
|
|
68
|
-
workflowType: WorkflowType
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
export interface EngineCommitInput {
|
|
74
|
-
message: string
|
|
75
|
-
content: string
|
|
76
|
-
commitType?: CommitType
|
|
77
|
-
gitCommitSha?: string
|
|
78
|
-
ciRunId?: string
|
|
79
|
-
pipelineName?: string
|
|
80
|
-
threads?: {
|
|
81
|
-
open?: string[]
|
|
82
|
-
close?: Array<{ id: string; note: string }>
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface EngineOptions {
|
|
87
|
-
summarizer?: RollingSummarizer
|
|
88
|
-
embeddingService?: EmbeddingService
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export class ContextEngine {
|
|
92
|
-
private projectId = ''
|
|
93
|
-
private branchId = ''
|
|
94
|
-
private readonly summarizer: RollingSummarizer
|
|
95
|
-
private readonly embeddings: EmbeddingService | null
|
|
96
|
-
|
|
97
|
-
constructor(
|
|
98
|
-
private readonly store: EngineStore,
|
|
99
|
-
private readonly agentId: string,
|
|
100
|
-
private readonly agentRole: AgentRole,
|
|
101
|
-
private readonly tool: string,
|
|
102
|
-
private readonly workflowType: WorkflowType,
|
|
103
|
-
options: EngineOptions = {},
|
|
104
|
-
) {
|
|
105
|
-
this.summarizer = options.summarizer ?? new RollingSummarizer()
|
|
106
|
-
this.embeddings = options.embeddingService ?? null
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Bind the engine to a project + branch and register this agent.
|
|
111
|
-
* Must be called before commit() or context().
|
|
112
|
-
*/
|
|
113
|
-
async init(projectId: string, branchId: string): Promise<void> {
|
|
114
|
-
this.projectId = projectId
|
|
115
|
-
this.branchId = branchId
|
|
116
|
-
await this.store.upsertAgent({
|
|
117
|
-
id: this.agentId,
|
|
118
|
-
projectId,
|
|
119
|
-
role: this.agentRole,
|
|
120
|
-
tool: this.tool,
|
|
121
|
-
workflowType: this.workflowType,
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Persist a context commit. Summary is generated automatically via the
|
|
127
|
-
* RollingSummarizer (Week 1: string truncation; Week 2: Claude Haiku).
|
|
128
|
-
*/
|
|
129
|
-
async commit(input: EngineCommitInput): Promise<Commit> {
|
|
130
|
-
this.assertInitialized()
|
|
131
|
-
|
|
132
|
-
// Fetch previous summary from branch HEAD (if any)
|
|
133
|
-
let previousSummary = ''
|
|
134
|
-
const branch = await this.store.getBranch(this.branchId)
|
|
135
|
-
if (branch?.headCommitId) {
|
|
136
|
-
const head = await this.store.getCommit(branch.headCommitId)
|
|
137
|
-
previousSummary = head?.summary ?? ''
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const summary = await this.summarizer.summarize(input.content, previousSummary, 'branch')
|
|
141
|
-
|
|
142
|
-
const commit = await this.store.createCommit({
|
|
143
|
-
branchId: this.branchId,
|
|
144
|
-
agentId: this.agentId,
|
|
145
|
-
agentRole: this.agentRole,
|
|
146
|
-
tool: this.tool,
|
|
147
|
-
workflowType: this.workflowType,
|
|
148
|
-
message: input.message,
|
|
149
|
-
content: input.content,
|
|
150
|
-
summary,
|
|
151
|
-
commitType: input.commitType ?? 'manual',
|
|
152
|
-
gitCommitSha: input.gitCommitSha,
|
|
153
|
-
ciRunId: input.ciRunId,
|
|
154
|
-
pipelineName: input.pipelineName,
|
|
155
|
-
threads: input.threads,
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
// Generate and index embedding asynchronously — never block the commit.
|
|
159
|
-
if (this.embeddings) {
|
|
160
|
-
const text = `${input.message}\n${input.content}`
|
|
161
|
-
this.embeddings.embed(text).then(vector => {
|
|
162
|
-
if (vector) return this.store.indexEmbedding(commit.id, vector)
|
|
163
|
-
}).catch(() => { /* swallow — indexing is best-effort */ })
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return commit
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Create a new branch from the current branch.
|
|
171
|
-
* Writes a `branch-init` commit on the new branch carrying the parent HEAD
|
|
172
|
-
* summary forward so the branch starts with full context.
|
|
173
|
-
*/
|
|
174
|
-
async branch(gitBranch: string, name?: string): Promise<Branch> {
|
|
175
|
-
this.assertInitialized()
|
|
176
|
-
|
|
177
|
-
// Carry parent HEAD summary into the new branch
|
|
178
|
-
const parentBranch = await this.store.getBranch(this.branchId)
|
|
179
|
-
let parentSummary = ''
|
|
180
|
-
if (parentBranch?.headCommitId) {
|
|
181
|
-
const head = await this.store.getCommit(parentBranch.headCommitId)
|
|
182
|
-
parentSummary = head?.summary ?? ''
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const newBranch = await this.store.createBranch({
|
|
186
|
-
projectId: this.projectId,
|
|
187
|
-
name: name ?? gitBranch,
|
|
188
|
-
gitBranch,
|
|
189
|
-
parentBranchId: this.branchId,
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
// branch-init commit: carries parent summary, no rolling summarization needed
|
|
193
|
-
await this.store.createCommit({
|
|
194
|
-
branchId: newBranch.id,
|
|
195
|
-
agentId: this.agentId,
|
|
196
|
-
agentRole: this.agentRole,
|
|
197
|
-
tool: this.tool,
|
|
198
|
-
workflowType: this.workflowType,
|
|
199
|
-
message: `Branch ${gitBranch} created from ${parentBranch?.gitBranch ?? this.branchId}`,
|
|
200
|
-
content: parentSummary,
|
|
201
|
-
summary: parentSummary,
|
|
202
|
-
commitType: 'branch-init',
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
return newBranch
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Merge a source branch into the current branch.
|
|
210
|
-
* Generates a rolling summary for the merge commit, carries open threads
|
|
211
|
-
* from source to target, and marks the source branch as merged.
|
|
212
|
-
*/
|
|
213
|
-
async merge(sourceBranchId: string): Promise<Commit> {
|
|
214
|
-
this.assertInitialized()
|
|
215
|
-
|
|
216
|
-
// Source HEAD summary
|
|
217
|
-
const sourceBranch = await this.store.getBranch(sourceBranchId)
|
|
218
|
-
let sourceSummary = ''
|
|
219
|
-
if (sourceBranch?.headCommitId) {
|
|
220
|
-
const head = await this.store.getCommit(sourceBranch.headCommitId)
|
|
221
|
-
sourceSummary = head?.summary ?? ''
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Target (current branch) HEAD summary as the rolling base
|
|
225
|
-
const targetBranch = await this.store.getBranch(this.branchId)
|
|
226
|
-
let targetSummary = ''
|
|
227
|
-
if (targetBranch?.headCommitId) {
|
|
228
|
-
const head = await this.store.getCommit(targetBranch.headCommitId)
|
|
229
|
-
targetSummary = head?.summary ?? ''
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const mergeContent = `Merged ${sourceBranch?.name ?? sourceBranchId}: ${sourceSummary}`
|
|
233
|
-
const mergeSummary = await this.summarizer.summarize(mergeContent, targetSummary, 'branch')
|
|
234
|
-
|
|
235
|
-
return this.store.mergeBranch(sourceBranchId, this.branchId, mergeSummary)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Retrieve a SessionSnapshot for the current project+branch.
|
|
240
|
-
*
|
|
241
|
-
* scope='global' → full project snapshot (project summary + branch state)
|
|
242
|
-
* scope='branch' → same as global for now (branch-scoped view, Week 2)
|
|
243
|
-
* Other scopes → throws until implemented in later weeks
|
|
244
|
-
*/
|
|
245
|
-
async context(scope: ContextScope): Promise<SessionSnapshot> {
|
|
246
|
-
this.assertInitialized()
|
|
247
|
-
|
|
248
|
-
if (scope === 'global' || scope === 'branch') {
|
|
249
|
-
return this.store.getSessionSnapshot(this.projectId, this.branchId)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
throw new Error(`context scope '${scope}' is not yet implemented`)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Semantic search over commits in the current project.
|
|
257
|
-
* Requires an EmbeddingService to have been passed at construction time;
|
|
258
|
-
* returns an empty array if embeddings are unavailable.
|
|
259
|
-
*/
|
|
260
|
-
async semanticSearch(query: string, projectId: string, limit = 5): Promise<SearchResult[]> {
|
|
261
|
-
if (!this.embeddings) return []
|
|
262
|
-
const vector = await this.embeddings.embed(query)
|
|
263
|
-
if (!vector) return []
|
|
264
|
-
return this.store.semanticSearch(vector, projectId, limit)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private assertInitialized(): void {
|
|
268
|
-
if (!this.projectId || !this.branchId) {
|
|
269
|
-
throw new Error(
|
|
270
|
-
'ContextEngine not initialized — call engine.init(projectId, branchId) first.',
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
// SnapshotFormatter — converts a SessionSnapshot to one of the 3 output formats.
|
|
2
|
-
// Moved from store/local/index.ts (inline) to core so all consumers share the same output.
|
|
3
|
-
|
|
4
|
-
import type { SessionSnapshot, SnapshotFormat } from './types.js'
|
|
5
|
-
|
|
6
|
-
export class SnapshotFormatter {
|
|
7
|
-
format(snapshot: SessionSnapshot, fmt: SnapshotFormat): string {
|
|
8
|
-
const { projectSummary, branchName, branchSummary, recentCommits, openThreads, activeClaims } = snapshot
|
|
9
|
-
|
|
10
|
-
if (fmt === 'json') {
|
|
11
|
-
return JSON.stringify(snapshot, null, 2)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (fmt === 'agents-md') {
|
|
15
|
-
const commits = recentCommits
|
|
16
|
-
.map(
|
|
17
|
-
(c) =>
|
|
18
|
-
`- [${c.createdAt.toISOString()}] "${c.message}" by ${c.agentRole} via ${c.tool} (${c.workflowType})`,
|
|
19
|
-
)
|
|
20
|
-
.join('\n')
|
|
21
|
-
const threads = openThreads
|
|
22
|
-
.map(
|
|
23
|
-
(t) =>
|
|
24
|
-
`- [ ] ${t.description} (opened ${t.createdAt.toLocaleDateString()}, ${t.workflowType ?? 'interactive'})`,
|
|
25
|
-
)
|
|
26
|
-
.join('\n')
|
|
27
|
-
const claims = activeClaims
|
|
28
|
-
.map((cl) => `- [${cl.status}] ${cl.task} by ${cl.agentId} (${cl.role})`)
|
|
29
|
-
.join('\n')
|
|
30
|
-
return [
|
|
31
|
-
`## Project State`,
|
|
32
|
-
projectSummary || '(no summary yet)',
|
|
33
|
-
``,
|
|
34
|
-
`## Current Branch: ${branchName}`,
|
|
35
|
-
branchSummary || '(no branch summary yet)',
|
|
36
|
-
``,
|
|
37
|
-
`## Recent Activity`,
|
|
38
|
-
commits || '(no commits yet)',
|
|
39
|
-
``,
|
|
40
|
-
`## Open Threads`,
|
|
41
|
-
threads || '(none)',
|
|
42
|
-
``,
|
|
43
|
-
`## Active Claims`,
|
|
44
|
-
claims || '(none)',
|
|
45
|
-
].join('\n')
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// text (default)
|
|
49
|
-
const commits = recentCommits
|
|
50
|
-
.map(
|
|
51
|
-
(c) =>
|
|
52
|
-
`[${c.createdAt.toISOString()}] "${c.message}" by ${c.agentRole} via ${c.tool} (${c.workflowType})`,
|
|
53
|
-
)
|
|
54
|
-
.join('\n')
|
|
55
|
-
const threads = openThreads
|
|
56
|
-
.map(
|
|
57
|
-
(t) =>
|
|
58
|
-
`[ ] ${t.description} (opened ${t.createdAt.toLocaleDateString()}, ${t.workflowType ?? 'interactive'})`,
|
|
59
|
-
)
|
|
60
|
-
.join('\n')
|
|
61
|
-
const claims = activeClaims
|
|
62
|
-
.map((cl) => `[${cl.status}] ${cl.task} by ${cl.agentId} (${cl.role})`)
|
|
63
|
-
.join('\n')
|
|
64
|
-
|
|
65
|
-
return [
|
|
66
|
-
`=== PROJECT STATE ===`,
|
|
67
|
-
projectSummary || '(no summary yet)',
|
|
68
|
-
``,
|
|
69
|
-
`=== CURRENT BRANCH: ${branchName} ===`,
|
|
70
|
-
branchSummary || '(no branch summary yet)',
|
|
71
|
-
``,
|
|
72
|
-
`=== LAST 3 COMMITS ===`,
|
|
73
|
-
commits || '(none)',
|
|
74
|
-
``,
|
|
75
|
-
`=== OPEN THREADS ===`,
|
|
76
|
-
threads || '(none)',
|
|
77
|
-
``,
|
|
78
|
-
`=== ACTIVE CLAIMS ===`,
|
|
79
|
-
claims || '(none)',
|
|
80
|
-
].join('\n')
|
|
81
|
-
}
|
|
82
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
// Week 2 summarizer tests.
|
|
2
|
-
// All API calls are mocked — no network required.
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
5
|
-
import Anthropic from '@anthropic-ai/sdk'
|
|
6
|
-
import { RollingSummarizer } from './summarizer.js'
|
|
7
|
-
|
|
8
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
function makeClient(responseText: string): Anthropic {
|
|
11
|
-
const client = new Anthropic({ apiKey: 'test-key' })
|
|
12
|
-
vi.spyOn(client.messages, 'create').mockResolvedValue({
|
|
13
|
-
id: 'msg_test',
|
|
14
|
-
type: 'message',
|
|
15
|
-
role: 'assistant',
|
|
16
|
-
content: [{ type: 'text', text: responseText }],
|
|
17
|
-
model: 'claude-haiku-4-5',
|
|
18
|
-
stop_reason: 'end_turn',
|
|
19
|
-
stop_sequence: null,
|
|
20
|
-
usage: { input_tokens: 10, output_tokens: 20 },
|
|
21
|
-
} as Awaited<ReturnType<typeof client.messages.create>>)
|
|
22
|
-
return client
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function makeFailingClient(): Anthropic {
|
|
26
|
-
const client = new Anthropic({ apiKey: 'test-key' })
|
|
27
|
-
vi.spyOn(client.messages, 'create').mockRejectedValue(
|
|
28
|
-
new Error('Simulated API failure'),
|
|
29
|
-
)
|
|
30
|
-
return client
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
describe('RollingSummarizer', () => {
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
vi.restoreAllMocks()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('returns Claude summary when API succeeds', async () => {
|
|
41
|
-
const claudeReply = 'Concise summary from Claude Haiku.'
|
|
42
|
-
const summarizer = new RollingSummarizer({ client: makeClient(claudeReply) })
|
|
43
|
-
|
|
44
|
-
const result = await summarizer.summarize('some content', '', 'branch')
|
|
45
|
-
|
|
46
|
-
expect(result).toBe(claudeReply)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('passes previous summary and new content to Claude', async () => {
|
|
50
|
-
const client = makeClient('merged summary')
|
|
51
|
-
const summarizer = new RollingSummarizer({ client })
|
|
52
|
-
|
|
53
|
-
await summarizer.summarize('new work', 'old summary', 'branch')
|
|
54
|
-
|
|
55
|
-
const callArg = (client.messages.create as ReturnType<typeof vi.fn>).mock
|
|
56
|
-
.calls[0][0] as Anthropic.MessageCreateParamsNonStreaming
|
|
57
|
-
const userContent = callArg.messages[0].content as string
|
|
58
|
-
expect(userContent).toContain('old summary')
|
|
59
|
-
expect(userContent).toContain('new work')
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('falls back to string truncation when API fails', async () => {
|
|
63
|
-
const summarizer = new RollingSummarizer({ client: makeFailingClient() })
|
|
64
|
-
|
|
65
|
-
const result = await summarizer.summarize('beta', 'alpha', 'branch')
|
|
66
|
-
|
|
67
|
-
// Fallback: concatenate and keep; both fit in 2000 chars
|
|
68
|
-
expect(result).toContain('alpha')
|
|
69
|
-
expect(result).toContain('beta')
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('never throws when API fails', async () => {
|
|
73
|
-
const summarizer = new RollingSummarizer({ client: makeFailingClient() })
|
|
74
|
-
|
|
75
|
-
await expect(
|
|
76
|
-
summarizer.summarize('content', '', 'branch'),
|
|
77
|
-
).resolves.toBeDefined()
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('falls back to string truncation when no client is injected and ANTHROPIC_API_KEY is absent', async () => {
|
|
81
|
-
const savedKey = process.env['ANTHROPIC_API_KEY']
|
|
82
|
-
delete process.env['ANTHROPIC_API_KEY']
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const summarizer = new RollingSummarizer()
|
|
86
|
-
const result = await summarizer.summarize('beta content', 'alpha content', 'branch')
|
|
87
|
-
expect(result).toContain('alpha content')
|
|
88
|
-
expect(result).toContain('beta content')
|
|
89
|
-
} finally {
|
|
90
|
-
if (savedKey !== undefined) process.env['ANTHROPIC_API_KEY'] = savedKey
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('truncates from the start (keeps newest) when fallback overflows budget', async () => {
|
|
95
|
-
const savedKey = process.env['ANTHROPIC_API_KEY']
|
|
96
|
-
delete process.env['ANTHROPIC_API_KEY']
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const summarizer = new RollingSummarizer({ maxBranchChars: 20 })
|
|
100
|
-
const result = await summarizer.summarize('NEWEST', 'A'.repeat(30), 'branch')
|
|
101
|
-
// Budget is 20 chars; tail of the combined string should end with NEWEST
|
|
102
|
-
expect(result).toHaveLength(20)
|
|
103
|
-
expect(result.endsWith('NEWEST')).toBe(true)
|
|
104
|
-
} finally {
|
|
105
|
-
if (savedKey !== undefined) process.env['ANTHROPIC_API_KEY'] = savedKey
|
|
106
|
-
}
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('respects maxChars for Claude response (truncates to budget)', async () => {
|
|
110
|
-
const longReply = 'X'.repeat(5000)
|
|
111
|
-
const summarizer = new RollingSummarizer({
|
|
112
|
-
client: makeClient(longReply),
|
|
113
|
-
maxBranchChars: 100,
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
const result = await summarizer.summarize('content', '', 'branch')
|
|
117
|
-
|
|
118
|
-
expect(result).toHaveLength(100)
|
|
119
|
-
})
|
|
120
|
-
})
|