Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
42e0a6a
docs: add MCP-first pivot design spec and implementation plan
devlikebear Aug 22, 2026
3dcd45c
refactor(engine): extract storycontext from internal/ai
devlikebear Aug 22, 2026
bbf249e
refactor(engine): extract storyops applier from internal/companion
devlikebear Aug 22, 2026
89fa1e0
feat(engine): merge facts, memories, and references into the story brief
devlikebear Aug 22, 2026
4fb67a1
refactor(engine): isolate the summarizer's LLM surface in one file
devlikebear Aug 22, 2026
505e5f2
docs: mark MCP pivot Phase 1 complete in the implementation plan
devlikebear Aug 22, 2026
b4ee65f
chore: gate the story core against LLM dependencies in make test-go
devlikebear Aug 22, 2026
7e32a97
docs: lock in Phase 0 decisions for the MCP-first pivot
devlikebear Aug 23, 2026
1e0a9c7
feat(engine): add MCP settings keys and secret-stored bearer token
devlikebear Aug 23, 2026
c604964
feat(engine): add the loopback MCP host with bearer auth and discovery
devlikebear Aug 23, 2026
e2d3d4d
feat(engine): wire the MCP host into engineapp behind a build-tag pair
devlikebear Aug 23, 2026
ecaf07e
feat(engine): serve the nine MCP read tools with an audit trail
devlikebear Aug 23, 2026
8fe8eb6
docs: mark MCP pivot Phase 2 implementation complete
devlikebear Aug 23, 2026
f700dbf
fix(settings): persist and reload MCP settings
devlikebear Aug 23, 2026
a8ba48e
fix(mcp): return an empty body for an untouched scene
devlikebear Aug 23, 2026
8cd4e3b
fix(mcp): only the owning host retracts the discovery file
devlikebear Aug 23, 2026
2100cb8
fix(settings): keep MCP usable on platforms without a secure secret s…
devlikebear Aug 23, 2026
793a031
refactor(engine): share the plaintext helpers instead of duplicating …
devlikebear Aug 23, 2026
903c186
Merge Phase 1 helper dedup into Phase 2
devlikebear Aug 23, 2026
e6c91bd
test(mcp): wait for the OS to release the port instead of asserting i…
devlikebear Aug 23, 2026
a50be1c
Merge main into Phase 2 after the Phase 1 squash merge
devlikebear Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 28 additions & 24 deletions docs/superpowers/plans/2026-08-22-mcp-first-pivot.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,60 +92,64 @@

**파일:** `engine/go.mod`, `engine/go.sum`

- [ ] `cd engine && go get github.com/modelcontextprotocol/go-sdk@v1.7.0`
- [ ] `go build -tags mas ./...`가 SDK를 **링크하는지** 확인한다(MAS도 MCP를 쓴다).
- [ ] `go test -tags mobile ./...`는 SDK를 링크하지 않아야 한다. `go list -deps -tags mobile ./... | grep modelcontextprotocol`이 비어야 한다.
- [x] `cd engine && go get github.com/modelcontextprotocol/go-sdk@v1.7.0`
- [x] `go build -tags mas ./...`가 SDK를 **링크하는지** 확인한다(MAS도 MCP를 쓴다).
- [x] `go test -tags mobile ./...`는 SDK를 링크하지 않아야 한다. `go list -deps -tags mobile ./... | grep modelcontextprotocol`이 비어야 한다. **확인: 0건.**

### Task 2.2 — 설정 키와 시크릿 토큰

**파일:** `engine/internal/settings/settings.go`, `secrets.go`, `+ 테스트`

- [ ] `MCPMode`(`off`|`read_only`|`full`, 기본 `off`), `MCPPort`(기본 `7391`), `MCPProjectID`, `MCPConsentVersion`, `MCPConsentedAt`를 `Settings`와 `SettingsPatch`에 추가한다.
- [ ] `MCPTokenSet bool`(읽기용 존재 플래그)과 시크릿 저장소를 통해 쓰는 `RegenerateMCPToken()`을 추가한다. 토큰 값 자체는 `settings.get`이 절대 반환하지 않는다 — `api_key` 처리 방식과 동일하다.
- [ ] 테스트: `settings.get`이 토큰을 가리고 존재 플래그만 노출한다. 모드가 왕복한다. 알 수 없는 모드는 `off`로 떨어진다.
- [x] `MCPMode`(`off`|`read_only`|`full`, 기본 `off`), `MCPPort`(기본 `7391`), `MCPProjectID`, `MCPConsentVersion`, `MCPConsentedAt`를 `Settings`와 `SettingsPatch`에 추가한다.
- [x] `MCPTokenSet bool`(읽기용 존재 플래그)과 시크릿 저장소를 통해 쓰는 `RegenerateMCPToken()`을 추가한다. 토큰 값 자체는 `settings.get`이 절대 반환하지 않는다 — `api_key` 처리 방식과 동일하다.
- [x] 테스트: `settings.get`이 토큰을 가리고 존재 플래그만 노출한다. 모드가 왕복한다. 알 수 없는 모드는 `off`로 떨어진다.

### Task 2.3 — `mcphost` 골격, 인증, 수명 주기

**파일:** `engine/internal/mcphost/host.go`, `auth.go`, `discovery.go`, `+ 테스트`

- [ ] `mcphost.New(deps)`가 `*mcp.Server`와 `http.Server`를 만들고 설정된 포트로 `net.Listen("tcp", "127.0.0.1:"+port)` 한다. 저장된 클라이언트 설정이 재시작을 견디도록 포트는 고정이다.
- [ ] 포트가 이미 사용 중이면 설정 화면이 "7391 포트가 사용 중입니다 — 다른 포트를 선택하세요"로 렌더링할 수 있는 타입 에러를 반환한다. **다른 포트로 조용히 넘어가지 않는다.**
- [ ] 인증 미들웨어: 상수 시간 베어러 비교, `Origin`이 있는데 루프백이 아니면 거부, `Host`가 루프백이 아니면 거부.
- [ ] `Start()`가 `$LINETTA_HOME/mcp.json`(권한 0600, `{port, token, pid, started_at}`)을 쓰고, `Stop()`이 삭제하며 리스너를 내린다. 설정 파일 `settings.json`과는 별개 파일이다.
- [ ] 테스트: 토큰 없음 → 401, 토큰 틀림 → 401, `Origin: https://evil.test` → 403, 포트 점유 → 타입 에러, POSIX에서 디스커버리 파일 권한 0600, `Stop` 후 파일 삭제.
- [x] `mcphost.New(deps)`가 `*mcp.Server`와 `http.Server`를 만들고 설정된 포트로 `net.Listen("tcp", "127.0.0.1:"+port)` 한다. 저장된 클라이언트 설정이 재시작을 견디도록 포트는 고정이다.
- [x] 포트가 이미 사용 중이면 설정 화면이 "7391 포트가 사용 중입니다 — 다른 포트를 선택하세요"로 렌더링할 수 있는 타입 에러를 반환한다. **다른 포트로 조용히 넘어가지 않는다.**
- [x] 인증 미들웨어: 상수 시간 베어러 비교, `Origin`이 있는데 루프백이 아니면 거부, `Host`가 루프백이 아니면 거부.
- [x] `Start()`가 `$LINETTA_HOME/mcp.json`(권한 0600, `{port, token, pid, started_at}`)을 쓰고, `Stop()`이 삭제하며 리스너를 내린다. 설정 파일 `settings.json`과는 별개 파일이다.
- [x] 테스트: 토큰 없음 → 401, 토큰 틀림 → 401, `Origin: https://evil.test` → 403, 포트 점유 → 타입 에러, POSIX에서 디스커버리 파일 권한 0600, `Stop` 후 파일 삭제.

### Task 2.4 — `engineapp` 연결

**파일:** `engine/internal/engineapp/mcp_enabled.go`(`//go:build !mobile`), `mcp_disabled.go`(`//go:build mobile`), `engineapp.go`, `+ 테스트`

- [ ] `gitsync_enabled.go` / `gitsync_disabled.go` 패턴을 그대로 따른다: `const mcpAvailable`, `setupMCP(deps) mcpController`.
- [ ] RPC `mcp.status`, `mcp.enable`, `mcp.disable`, `mcp.regenerate_token`, `mcp.activity`를 등록한다. 비활성 쌍둥이는 `CodeMethodNotFound`를 반환한다.
- [ ] 호스트의 `Stop`을 `a.closers`에 넣어 앱과 함께 리스너가 죽게 한다.
- [ ] `handlers.Capabilities`에 `MCPAvailable`을 추가하고 `diagnostics.version` / `diagnostics.get`으로 노출한다.
- [ ] 테스트: 모드 `off`면 아무것도 바인딩하지 않음, `mcp.enable` 후 `mcp.status`가 포트를 보고함, `Close()`가 포트를 반납함.
- [x] `gitsync_enabled.go` / `gitsync_disabled.go` 패턴을 그대로 따른다: `const mcpAvailable`, `setupMCP(deps) mcpController`.
- [x] RPC `mcp.status`, `mcp.enable`, `mcp.disable`, `mcp.regenerate_token`, `mcp.activity`를 등록한다. 비활성 쌍둥이는 `CodeMethodNotFound`를 반환한다.
- [x] 호스트의 `Stop`을 `a.closers`에 넣어 앱과 함께 리스너가 죽게 한다.
- [x] `handlers.Capabilities`에 `MCPAvailable`을 추가하고 `diagnostics.version` / `diagnostics.get`으로 노출한다.
- [x] 테스트: 모드 `off`면 아무것도 바인딩하지 않음, `mcp.enable` 후 `mcp.status`가 포트를 보고함, `Close()`가 포트를 반납함.

### Task 2.5 — 읽기 툴 9개

**파일:** `engine/internal/mcphost/tools_read.go`, `+ 테스트`

- [ ] 설계 문서 5절의 읽기 툴 9개를 `mcp.AddTool`로 등록한다. 입출력을 타입 구조체로 선언해 스키마가 생성되게 한다.
- [ ] `linetta_get_story_context`는 병합된 `storycontext` 빌더(Task 1.3 완료가 전제)로 브리프를 조립하고, 평문 렌더러로 마크다운을 만들어 "무엇이 포함됐는지" 요약과 함께 반환한다.
- [ ] `linetta_read_scene`은 `content_version`을 반환하고, 설명에 쓰기에는 이 값이 필요하다고 명시한다.
- [ ] `MCPProjectID` 범위 제한은 툴마다가 아니라 공용 헬퍼 한 곳에서 강제한다.
- [ ] 테스트: 씨드된 임시 스토어로 각 툴 검증, 범위 밖 `project_id` 차단, `read_only` 모드에서 정확히 이 9개만 등록됨, **LLM 프로바이더가 설정되지 않은 상태에서 `linetta_get_story_context`가 요약만 빈 채 팩트·메모리를 포함한 완전한 브리프를 에러 없이 반환함**(전환의 전제가 이 테스트에 달려 있다).
- [x] 설계 문서 5절의 읽기 툴 9개를 `mcp.AddTool`로 등록한다. 입출력을 타입 구조체로 선언해 스키마가 생성되게 한다.
- [x] `linetta_get_story_context`는 병합된 `storycontext` 빌더(Task 1.3 완료가 전제)로 브리프를 조립하고, 평문 렌더러로 마크다운을 만들어 "무엇이 포함됐는지" 요약과 함께 반환한다.
- [x] `linetta_read_scene`은 `content_version`을 반환하고, 설명에 쓰기에는 이 값이 필요하다고 명시한다.
- [x] `MCPProjectID` 범위 제한은 툴마다가 아니라 공용 헬퍼 한 곳에서 강제한다.
- [x] 테스트: 씨드된 임시 스토어로 각 툴 검증, 범위 밖 `project_id` 차단, `read_only` 모드에서 정확히 이 9개만 등록됨, **LLM 프로바이더가 설정되지 않은 상태에서 `linetta_get_story_context`가 요약만 빈 채 팩트·메모리를 포함한 완전한 브리프를 에러 없이 반환함**(전환의 전제가 이 테스트에 달려 있다).

### Task 2.6 — 활동 로그

**파일:** `engine/internal/store/migrations/*`, `engine/internal/mcphost/activity.go`, `+ 테스트`

- [ ] `mcp_activity` 테이블 마이그레이션(`id, at, tool, project_id, target_id, ok, detail`).
- [ ] 성공·실패 관계없이 모든 툴 호출을 기록하고, 기존 스냅샷 정리 잡에 보존 한도를 얹는다.
- [ ] `mcp.activity` RPC가 최근 기록을 반환한다.
- [x] `mcp_activity` 테이블 마이그레이션(`id, at, tool, project_id, target_id, ok, detail`).
- [x] 성공·실패 관계없이 모든 툴 호출을 기록한다. **계획에서 이탈:** 보존 한도를 스냅샷 정리 잡에 얹지 않고 삽입 직후 자체 트리밍(500행)으로 처리했다 — 움직이는 부품이 하나 줄고, 앱이 유휴 상태가 되지 않아도 상한이 지켜진다.
- [x] `mcp.activity` RPC가 최근 기록을 반환한다.

**2단계 종료 조건:** `claude mcp add --transport http linetta http://127.0.0.1:7391/mcp --header "Authorization: Bearer <token>"`로 연결되고, 실제 Claude Code 세션이 작품 구조를 설명할 수 있다.

---

> **완료 (2026-08-22):** 엔진 42개 패키지 통과, `mas`/`mobile` 빌드 통과, mobile의 SDK 의존 0건. 테스트가 실제 HTTP 엔드포인트를 외부 클라이언트처럼 구동한다(initialize → tools/list → tools/call, SSE 파싱). 툴 등록은 활동 로그 데코레이터로 감싸 Task 2.6을 같이 끝냈고, 범위 제한은 씬 id 우회까지 막는 것을 확인했다.
>
> **남은 종료 조건:** 실제 Claude Code에서 `claude mcp add`로 붙여 작품 구조를 읽는 왕복 — 사용자 기기에서 직접 해야 하는 단계다.

## Phase 3 — 쓰기 툴과 안전장치

### Task 3.1 — `linetta_write_scene`
Expand Down
8 changes: 8 additions & 0 deletions engine/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@ go 1.26.6
require (
github.com/devlikebear/tars v0.34.3
github.com/google/uuid v1.6.0
github.com/modelcontextprotocol/go-sdk v1.7.0
golang.org/x/sys v0.44.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.50.1
)

require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/jsonschema-go v0.4.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/time v0.15.0 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
Expand Down
18 changes: 18 additions & 0 deletions engine/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ github.com/devlikebear/tars v0.34.3/go.mod h1:VO3aJQ+y1ou9pWuhU76AbAOOR8MEb4XWYV
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand All @@ -16,6 +22,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/modelcontextprotocol/go-sdk v1.7.0 h1:yqjY2dsbKAC0LSuWZVBMrHgiG8ukXv6NRo0JiALay44=
github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand All @@ -26,15 +34,25 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
104 changes: 104 additions & 0 deletions engine/internal/companion/context_sources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package companion

import (
"context"
"strings"

"github.com/devlikebear/linetta/engine/internal/fact"
"github.com/devlikebear/linetta/engine/internal/storycontext"
)

// referenceContextLimit matches gatherContext's own cap on injected references.
const referenceContextLimit = 40

// The companion is currently the only place that gathers Fact Book cards,
// remembered facts, and writer-attached references. storycontext grew optional
// source interfaces for those sections in Phase 1 of the MCP-first pivot (#47);
// these adapters connect the two so the MCP story brief carries everything the
// companion's own prompt does.
//
// When this package is removed (pivot Phase 6), these three methods move to
// whatever owns the underlying repos — the fact and reference repos are
// already independent, and memory recall follows the remember op into
// storyops.

var (
_ storycontext.FactSource = (*Service)(nil)
_ storycontext.MemorySource = (*Service)(nil)
_ storycontext.ReferenceSource = (*Service)(nil)
)

// ContextFacts returns Fact Book cards for the brief, preferring cards
// attached to the current scene, exactly as gatherContext does.
func (s *Service) ContextFacts(ctx context.Context, projectID, nodeID string) ([]storycontext.FactBrief, error) {
if s.facts == nil {
return nil, nil
}
filter := fact.ListFilter{ProjectID: projectID, Limit: factContextLimit}
if strings.TrimSpace(nodeID) != "" {
filter.NodeID = &nodeID
}
cards, err := s.facts.List(ctx, filter)
if err != nil {
return nil, err
}
out := make([]storycontext.FactBrief, 0, len(cards))
for _, c := range cards {
brief := storycontext.FactBrief{
ID: c.ID,
Status: c.Status,
Claim: c.Claim,
Category: c.Category,
Result: c.Result,
}
for _, src := range c.Sources {
if strings.TrimSpace(src.URL) == "" {
continue
}
brief.Sources = append(brief.Sources, storycontext.FactSourceBrief{
Title: src.Title,
URL: src.URL,
})
}
out = append(out, brief)
}
return out, nil
}

// ContextMemories returns recent remembered facts for the brief.
func (s *Service) ContextMemories(projectID string) []string {
return s.Recall(projectID, "", recallLimit)
}

// ContextReferences returns the writer-attached material for the brief,
// skipping disabled entries and using the same prompt text (summary vs full
// content) the companion sends.
func (s *Service) ContextReferences(ctx context.Context, projectID, nodeID string) ([]storycontext.ReferenceBrief, error) {
if s.references == nil {
return nil, nil
}
refs, err := s.ListReferences(ctx, ReferenceQuery{
ProjectID: projectID,
NodeID: nodeID,
Limit: referenceContextLimit,
})
if err != nil {
return nil, err
}
out := make([]storycontext.ReferenceBrief, 0, len(refs))
for _, r := range refs {
if r.Status == ReferenceStatusDisabled {
continue
}
text := strings.TrimSpace(referencePromptText(r))
if text == "" {
continue
}
out = append(out, storycontext.ReferenceBrief{
Title: strings.TrimSpace(r.Title),
Purpose: strings.TrimSpace(r.Purpose),
Body: text,
})
}
return out, nil
}
Loading
Loading