project-librarian 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -63,7 +63,8 @@ npx project-librarian@latest install --scope user --agents all
63
63
  - **구조적인 위키 작성.** 새 프로젝트 내용은 작성하거나 취합하기 전에 `wiki/meta/document-taxonomy.md`로 분류하므로 PRD, 정책, UX, 데이터, API, QA, 릴리즈, 운영 메모가 하나의 잡다한 페이지로 합쳐지지 않습니다.
64
64
  - **검토 가능한 위키 그래프.** `--wiki-visualize`는 `.project-wiki/` 아래에 독립 실행형 HTML 그래프를 작성해, 시작 컨텍스트를 늘리지 않고 페이지 유형·라우터 깊이·역링크·결정 참조를 보여줍니다.
65
65
  - **막연한 표현이 아니라 측정값.** 모든 성능 주장은 격리된 Codex 벤치마크에서 나오며, 손해를 본 경우도 이긴 경우 바로 옆에 보여 줍니다.
66
- - **선택적 코드 근거.** 재생성 가능한 SQLite 인덱스와 답변 형태 MCP 도구가 영향·소유권·워크스페이스 그래프 질문에 답하며, 추가 런타임 의존성이 전혀 없습니다.
66
+ - **로컬 세션 핸드오프.** `--handoff-save`는 `.project-wiki/session/` 아래에 생성형 재개 메모를 저장합니다. 시작 훅은 존재만 알려주므로 실행 로그가 canonical 위키 정본으로 섞이지 않습니다.
67
+ - **선택적 코드 근거.** 재생성 가능한 SQLite 인덱스와 답변 형태 MCP 도구가 영향·소유권·워크스페이스 그래프 질문에 답하며, MCP SDK 의존성은 추가하지 않습니다.
67
68
  - **다시 실행해도 안전.** 부트스트랩은 멱등하고 기존 내용 보존을 우선하며, 진단은 깨진 경로·도달 불가 페이지·오래된 사실을 에이전트가 오해하기 전에 잡아냅니다.
68
69
 
69
70
  ## 존재 이유
@@ -80,6 +81,7 @@ Project Librarian은 에이전트에게 두 가지 로컬 정본을 제공합니
80
81
  | `.codex/`, `.claude/`, `.cursor/`, `.gemini/` 훅 | 전체 위키를 불러오지 않는 Codex/Claude Code/Cursor/Gemini CLI 시작 컨텍스트. |
81
82
  | `GEMINI.md` 및 `.cursor/rules/` | Gemini CLI와 Cursor를 같은 간결한 위키 우선 계약으로 안내하는 지침 파일. |
82
83
  | `.project-wiki/code-evidence.sqlite` | 파일, 심볼, import, route, 소유권, 워크스페이스 그래프, 보고서, 영향 확인을 위한 재생성 가능한 코드 근거. |
84
+ | `.project-wiki/session/last-handoff.md` | 마지막 세션의 목표, 상태, 막힘, 다음 액션, 결정, 명령, 검증을 담는 선택적 생성형 로컬 핸드오프입니다. 프로젝트 정본이 아니라 참고 데이터입니다. |
83
85
  | `.project-wiki/wiki-graph.html` | 파생 concept type, 라우터 도달성, 링크, 역링크, 결정 참조를 보여주는 선택적 정적 위키 그래프 시각화. |
84
86
  | 진단 및 마이그레이션 모드 | 링크 확인, 품질 확인, 마이그레이션 수신함, 오래된 신호 보고, 작업 흐름에서 문제가 드러날 때의 이슈 초안. |
85
87
 
@@ -207,6 +209,10 @@ npx project-librarian@latest install --scope project --agents all
207
209
  | 페이지의 역링크/결정 인용 확인 | "decisions/release-policy의 Project Librarian 위키 영향도를 보여줘." | `--wiki-impact "decisions/release-policy"` |
208
210
  | 위키 그래프 시각화 생성 | "Project Librarian 위키 그래프 시각화를 생성해줘." | `--wiki-visualize` |
209
211
  | 후보 메모 저장 | "이 내용을 Project Librarian 후보 메모로 저장해줘: <내용>." | `--capture-inbox --title "Candidate" --content "Details"` |
212
+ | 세션 핸드오프 저장 | "현재 작업을 Project Librarian 세션 핸드오프로 저장해줘." | `--handoff-save --goal "..." --state "..." --next "..."` |
213
+ | 핸드오프에서 재개 | "마지막 Project Librarian 세션 핸드오프를 보여줘." | `--handoff-show` |
214
+ | 핸드오프 후보 승격 | "마지막 Project Librarian 핸드오프를 위키 수신함 후보로 승격해줘." | `--handoff-promote-inbox` |
215
+ | 전체 핸드오프 주입 실험 켜기 | "Project Librarian 전체 핸드오프 주입 실험을 켜줘." | `--handoff-injection-enable` |
210
216
  | 오래되었거나 미해결인 위키 페이지 보고 | "Project Librarian에서 오래되었거나 미해결인 페이지를 확인해줘." | `--prune-check` |
211
217
  | 신호가 높은 오래되었거나 미해결인 위키 페이지만 보고 | "Project Librarian에서 엄격한 기준으로 오래되었거나 미해결인 페이지를 확인해줘." | `--prune-check --prune-check-strict` |
212
218
  | git 설정 변경 없이 훅 파일 설치 | "git 설정은 바꾸지 말고 Project Librarian 훅 파일만 설정해줘." | `--no-git-config` |
@@ -291,7 +297,9 @@ MCP 서버 등록 (`mcpServers`에 기존 항목 보존하며 병합):
291
297
 
292
298
  ## 코드 근거 MCP 서버
293
299
 
294
- `project-librarian mcp`는 직접 구현한 stdio MCP 서버(줄바꿈 구분 JSON 위의 JSON-RPC 2.0, 추가 런타임 의존성 없음)를 실행해 기존 `.project-wiki` 코드 근거 인덱스를 읽기 전용으로 제공합니다. 답변 형태 도구 — `code_context_pack`, `code_impact`, `code_ownership`(CODEOWNERS 마지막 일치 우선순위), `code_workspace_graph`, `code_search`, `code_status` — 를 노출하며, 각 응답은 한 줄 답변으로 시작해 간결한 경로/심볼/시그니처 근거가 뒤따르고, 응답마다 길이를 제한하며, `code_status`가 인덱스가 오래되었다고 보고하면 경고를 앞에 붙입니다.
300
+ `project-librarian mcp`는 직접 구현한 stdio MCP 서버(줄바꿈 구분 JSON 위의 JSON-RPC 2.0, MCP SDK 의존성 없음)를 실행해 기존 `.project-wiki` 코드 근거 인덱스를 읽기 전용으로 제공합니다. 패키지의 필수 런타임 의존성은 `typescript`이며, 코드 근거 기능은 Node의 `node:sqlite`도 사용하고 Tree-sitter 문법 패키지는 선택 사항입니다. 답변 형태 도구 — `code_context_pack`, `code_impact`, `code_ownership`(CODEOWNERS 마지막 일치 우선순위), `code_workspace_graph`, `code_search`, `code_status` — 를 노출하며, 각 응답은 한 줄 답변으로 시작해 간결한 경로/심볼/시그니처 근거가 뒤따르고, 응답마다 길이를 제한하며, `code_status`가 인덱스가 오래되었다고 보고하면 경고를 앞에 붙입니다.
301
+
302
+ `--code-report`, `--code-impact`, `--code-context-pack`, MCP 도구 출력을 현재 코드 구조 근거로 인용하기 전에는 `project-librarian --code-status` 또는 MCP `code_status`를 실행해 `stale_files: 0`인지 확인하세요. 오래된 보고서는 재빌드가 필요하다는 신호이지 권위 있는 프로젝트 진실이 아닙니다.
295
303
 
296
304
  서버는 고정 리소스 — `project-librarian://wiki/startup`, `project-librarian://wiki/index`, `project-librarian://code/status` — 와 위키 분류 갱신, 코드 영향 추적, 유지보수 개선 검토, 검색 품질 검토용 프롬프트 템플릿도 제공합니다. 리소스 읽기는 임의 파일 경로가 아니라 고정 URI 레지스트리에서만 처리합니다.
297
305
 
@@ -383,6 +391,10 @@ node .codex/skills/project-librarian/dist/init-project-wiki.js install [--scope
383
391
  | `--wiki-visualize-out <path>` | `--wiki-visualize`와 함께 사용해 `.project-wiki/` 아래의 사용자 지정 저장소 상대 경로에 작성합니다. |
384
392
  | `--refresh-index` | 생성된 자동 발견 위키 라우팅을 갱신합니다. |
385
393
  | `--capture-inbox --title <title> --content <content>` | 위키 수신함에 후보 메모를 추가합니다. |
394
+ | `--handoff-save --goal <goal> --state <state> --next <action>` | `.project-wiki/session/` 아래에 생성형 로컬 세션 핸드오프를 저장합니다. 필요하면 `--next`, `--decision`, `--blocked`, `--open-question`, `--verification`을 반복합니다. |
395
+ | `--handoff-show`, `--handoff-status`, `--handoff-clear` | 생성된 세션 핸드오프를 출력, 점검, 삭제합니다. 시작 훅은 핸드오프 존재만 알리고 기본적으로 전체 파일을 주입하지 않습니다. |
396
+ | `--handoff-promote-inbox` | 생성된 핸드오프의 선별된 사실만 `wiki/inbox/project-candidates.md`에 pending 후보로 추가합니다. canonical, plan, decision 문서는 쓰지 않습니다. |
397
+ | `--handoff-injection-enable`, `--handoff-injection-disable`, `--handoff-injection-status` | 2,500자 상한의 전체 핸드오프 주입 실험을 켜고, 끄고, 상태를 확인합니다. 기본 시작 동작은 계속 pointer-only입니다. |
386
398
  | `--issue-draft --issue-title <title>` | 문제 또는 부작용에 대한 읽기 전용 GitHub 이슈 본문 초안을 출력합니다. |
387
399
  | `--issue-create --issue-title <title>` | 명시적 사용자 승인 후 `gh`로 GitHub 이슈를 생성합니다. |
388
400
  | `--glossary-init` | 선택적 용어집 페이지를 만들고 라우팅합니다. |
@@ -421,6 +433,8 @@ npm pack --dry-run
421
433
 
422
434
  `npm run test:coverage`는 Node 내장 test coverage에 보수적인 line, branch, function threshold를 적용하므로 coverage를 단순 보고서가 아니라 회귀 게이트로 사용합니다.
423
435
 
436
+ 지원 런타임 하한은 Node.js 22.13+입니다. TypeScript가 지원 사용자에게 없는 API를 허용하지 않도록 개발용 타입 정의는 Node 22 지원 계약에 맞추거나 Node 22 호환성 검사를 함께 둬야 합니다.
437
+
424
438
  `npm run release:check`는 로컬 전용 관리자 게이트입니다. 테스트, Node 내장 coverage, 벤치마크 파서 smoke, real-corpus 오프라인 데모, 벤치마크 release preview, 벤치마크 claim ledger 분류, raw 보관 상태 감사, package dry-run 검사, dist 실행 가능 여부와 소스 동기화, README 벤치마크 claim 경계 문구를 확인합니다. publish하지 않고 raw 벤치마크 산출물을 삭제하지 않으며 measured Codex 벤치마크도 실행하지 않습니다.
425
439
 
426
440
  `release:check` 통과는 런타임 보증이 아니라 재현 가능한 릴리스 준비 근거로 봐야 합니다. 현재 checkout에서 위 로컬 게이트를 통과했음을 증명하며, package dry run이 예상 publish 경계(`agents/`, `dist/`, `LICENSE`, `README.md`, `README.ko.md`, `SKILL.md`) 안에 머물고 소스 파일, 테스트, 저장소 로컬 위키/워크플로 상태, raw 벤치마크 출력, 로컬 캐시를 제외하는지도 확인합니다.
package/README.md CHANGED
@@ -63,7 +63,8 @@ npx project-librarian@latest install --scope user --agents all
63
63
  - **Structured wiki writing.** New project content is classified through `wiki/meta/document-taxonomy.md` before it is written or consolidated, so PRDs, policies, UX, data, APIs, QA, release, and operations notes do not collapse into one catch-all page.
64
64
  - **Inspectable wiki graph.** `--wiki-visualize` writes a self-contained HTML graph under `.project-wiki/`, showing page types, router depth, backlinks, and decision references without adding to startup context.
65
65
  - **Measured, not hand-wavy.** Every performance claim comes from hermetic Codex benchmarks — and the cases where it costs *more* are shown right next to the wins.
66
- - **Optional code evidence.** A regenerable SQLite index plus answer-shaped MCP tools answer impact, ownership, and workspace-graph questions, with zero extra runtime dependencies.
66
+ - **Local session handoff.** `--handoff-save` writes generated resume notes under `.project-wiki/session/`; startup hooks only point to it, so rolling execution memory does not become canonical wiki truth.
67
+ - **Optional code evidence.** A regenerable SQLite index plus answer-shaped MCP tools answer impact, ownership, and workspace-graph questions without adding an MCP SDK dependency.
67
68
  - **Safe to re-run.** Bootstrap is idempotent and preservation-first; diagnostics flag broken routes, unreachable pages, and stale truth before they mislead an agent.
68
69
 
69
70
  ## Why It Exists
@@ -80,6 +81,7 @@ Project Librarian gives agents two local sources of truth:
80
81
  | `.codex/`, `.claude/`, `.cursor/`, and `.gemini/` hooks | Automatic startup context for Codex, Claude Code, Cursor, and Gemini CLI without loading the full wiki. |
81
82
  | `GEMINI.md` and `.cursor/rules/` | Gemini CLI and Cursor instruction files that route agents to the same compact wiki-first contract. |
82
83
  | `.project-wiki/code-evidence.sqlite` | Regenerable code evidence for files, symbols, imports, routes, ownership, workspace graph, reports, and impact checks. |
84
+ | `.project-wiki/session/last-handoff.md` | Optional generated local handoff for the last session's goal, state, blockers, next actions, decisions, commands, and verification. It is reference data, not project truth. |
83
85
  | `.project-wiki/wiki-graph.html` | Optional static wiki graph visualizer with derived concept types, router reachability, links, backlinks, and decision references. |
84
86
  | Diagnostics and migration modes | Link checks, quality checks, migration inboxes, stale-signal reports, and issue drafts when the workflow exposes a problem. |
85
87
 
@@ -207,6 +209,10 @@ Wiki setup and maintenance:
207
209
  | Show backlinks and decision citations for a page | "Show Project Librarian wiki impact for decisions/release-policy." | `--wiki-impact "decisions/release-policy"` |
208
210
  | Generate a wiki graph visualizer | "Generate the Project Librarian wiki graph visualizer." | `--wiki-visualize` |
209
211
  | Capture a candidate note | "Capture this as a Project Librarian candidate note: <details>." | `--capture-inbox --title "Candidate" --content "Details"` |
212
+ | Save a session handoff | "Save a Project Librarian session handoff for the current work." | `--handoff-save --goal "..." --state "..." --next "..."` |
213
+ | Resume from a handoff | "Show the last Project Librarian session handoff." | `--handoff-show` |
214
+ | Promote handoff candidates | "Promote the last Project Librarian handoff to the wiki inbox." | `--handoff-promote-inbox` |
215
+ | Opt in to full handoff injection | "Enable the Project Librarian full handoff injection experiment." | `--handoff-injection-enable` |
210
216
  | Report stale or unresolved wiki pages | "Check Project Librarian for stale or unresolved pages." | `--prune-check` |
211
217
  | Report only higher-signal stale or unresolved wiki pages | "Check Project Librarian for strict stale or unresolved pages." | `--prune-check --prune-check-strict` |
212
218
  | Install hook files without changing git config | "Set up Project Librarian hook files without changing git config." | `--no-git-config` |
@@ -291,7 +297,9 @@ Disposable code evidence cache:
291
297
 
292
298
  ## Code Evidence MCP Server
293
299
 
294
- `project-librarian mcp` runs a hand-rolled stdio MCP server (JSON-RPC 2.0 over newline-delimited JSON, no extra runtime dependencies) that serves the existing `.project-wiki` code-evidence index read-only. It exposes answer-shaped tools — `code_context_pack`, `code_impact`, `code_ownership` (CODEOWNERS last-match precedence), `code_workspace_graph`, `code_search`, and `code_status` — whose responses lead with a one-line answer, follow with compact path/symbol/signature evidence, cap each reply, and prepend a warning when `code_status` reports the index is stale.
300
+ `project-librarian mcp` runs a hand-rolled stdio MCP server (JSON-RPC 2.0 over newline-delimited JSON, no MCP SDK dependency) that serves the existing `.project-wiki` code-evidence index read-only. The package's hard runtime dependency is `typescript`; code evidence also uses Node's `node:sqlite`, with Tree-sitter grammars remaining optional. The server exposes answer-shaped tools — `code_context_pack`, `code_impact`, `code_ownership` (CODEOWNERS last-match precedence), `code_workspace_graph`, `code_search`, and `code_status` — whose responses lead with a one-line answer, follow with compact path/symbol/signature evidence, cap each reply, and prepend a warning when `code_status` reports the index is stale.
301
+
302
+ Before citing `--code-report`, `--code-impact`, `--code-context-pack`, or MCP tool output as current code-structure evidence, run `project-librarian --code-status` or MCP `code_status` and require `stale_files: 0`. Stale reports are pointers for rebuild, not authoritative project truth.
295
303
 
296
304
  The server also exposes fixed resources — `project-librarian://wiki/startup`, `project-librarian://wiki/index`, and `project-librarian://code/status` — plus prompt templates for wiki taxonomy updates, code impact traces, maintenance improvement reviews, and retrieval quality reviews. Resource reads come from a fixed URI registry rather than arbitrary filesystem paths.
297
305
 
@@ -383,6 +391,10 @@ Important options:
383
391
  | `--wiki-visualize-out <path>` | With `--wiki-visualize`, write to a custom repository-relative path under `.project-wiki/`. |
384
392
  | `--refresh-index` | Update generated auto-discovered wiki routing. |
385
393
  | `--capture-inbox --title <title> --content <content>` | Append a candidate note to the wiki inbox. |
394
+ | `--handoff-save --goal <goal> --state <state> --next <action>` | Save generated local session handoff state under `.project-wiki/session/`. Repeat `--next`, `--decision`, `--blocked`, `--open-question`, and `--verification` as needed. |
395
+ | `--handoff-show`, `--handoff-status`, `--handoff-clear` | Print, inspect, or remove generated session handoff state. Startup hooks mention the handoff when it exists but do not inject the full file by default. |
396
+ | `--handoff-promote-inbox` | Append selected generated handoff facts to `wiki/inbox/project-candidates.md` as a pending candidate. It does not write canonical, plan, or decision pages. |
397
+ | `--handoff-injection-enable`, `--handoff-injection-disable`, `--handoff-injection-status` | Opt in, opt out, or inspect the capped full handoff injection experiment. Default startup behavior remains pointer-only. |
386
398
  | `--issue-draft --issue-title <title>` | Print a read-only GitHub issue body draft for problems or side effects. |
387
399
  | `--issue-create --issue-title <title>` | Create a GitHub issue through `gh` after explicit user approval. |
388
400
  | `--glossary-init` | Create and route the optional glossary page. |
@@ -421,6 +433,8 @@ When editing TypeScript under `src/`, rebuild before committing so `dist/` stays
421
433
 
422
434
  `npm run test:coverage` uses Node's native test coverage with conservative line, branch, and function thresholds so coverage is a regression gate, not only a report.
423
435
 
436
+ The supported runtime floor is Node.js 22.13+. Development type definitions should stay aligned with the Node 22 support contract or be backed by a Node 22 compatibility check so TypeScript does not admit APIs unavailable to supported users.
437
+
424
438
  `npm run release:check` is a local-only maintainer gate: it runs tests, native Node coverage, benchmark parser smoke, the real-corpus offline demo, benchmark release preview, benchmark claim-ledger classification, raw hygiene audit, package dry-run inspection, dist executable/parity checks, and README benchmark-claim boundary checks. It never publishes, never deletes raw benchmark artifacts, and never launches a measured Codex benchmark.
425
439
 
426
440
  Treat a green `release:check` as a reproducible release-readiness bundle, not a runtime guarantee: it proves those local gates on the current checkout, including that the package dry run stays inside the expected publish boundary (`agents/`, `dist/`, `LICENSE`, `README.md`, `README.ko.md`, and `SKILL.md`) and excludes source files, tests, repo-local wiki/workflow state, raw benchmark output, and local caches.
package/SKILL.md CHANGED
@@ -19,6 +19,7 @@ Supported actions:
19
19
  - Search project wiki content.
20
20
  - Refresh the wiki index.
21
21
  - Capture a candidate note into the wiki inbox.
22
+ - Save, show, inspect, promote, inject, or clear generated local session handoff state under `.project-wiki/session/`.
22
23
  - Check for pending, stale, proposed, or undecided wiki pages.
23
24
  - Draft a GitHub issue body for problems or side effects found while using the skill.
24
25
  - Initialize a project glossary.
@@ -90,6 +91,12 @@ Map lifecycle requests to these internal operations:
90
91
  - Search the wiki: `$PROJECT_LIBRARIAN --query "search terms"`.
91
92
  - Refresh wiki routing/index: `$PROJECT_LIBRARIAN --refresh-index`.
92
93
  - Capture a project candidate: `$PROJECT_LIBRARIAN --capture-inbox --title "Candidate title" --content "Candidate content"`.
94
+ - Save generated session handoff state: `$PROJECT_LIBRARIAN --handoff-save --goal "Goal" --state "Current state" --next "Next action"`.
95
+ - Show the last generated session handoff: `$PROJECT_LIBRARIAN --handoff-show`.
96
+ - Inspect generated session handoff status: `$PROJECT_LIBRARIAN --handoff-status`.
97
+ - Clear generated session handoff state: `$PROJECT_LIBRARIAN --handoff-clear`.
98
+ - Promote selected generated handoff facts to the wiki inbox: `$PROJECT_LIBRARIAN --handoff-promote-inbox`.
99
+ - Enable, disable, or inspect the capped full handoff injection experiment: `$PROJECT_LIBRARIAN --handoff-injection-enable`, `$PROJECT_LIBRARIAN --handoff-injection-disable`, or `$PROJECT_LIBRARIAN --handoff-injection-status`.
93
100
  - Check stale/pending pages: `$PROJECT_LIBRARIAN --prune-check`.
94
101
  - Draft a GitHub issue body for a skill problem or side effect: `$PROJECT_LIBRARIAN --issue-draft --issue-title "Issue title"`.
95
102
  - After explicit user approval in a GitHub-backed repository, create the issue through GitHub CLI: `$PROJECT_LIBRARIAN --issue-create --issue-title "Issue title"`.
@@ -132,7 +139,9 @@ Use `--lint` for read-only validation:
132
139
  $PROJECT_LIBRARIAN --lint
133
140
  ```
134
141
 
135
- Use `--query`, `--prune-check`, `--issue-draft`, `--link-check`, `--quality-check`, `--doctor`, `--migration-lint`, `--migration-quality-check`, and `--migration-doctor` for read-only inspection/output through the resolved runner. Use `--doctor --fix` when safe generated routing refresh is intended. Use `--refresh-index`, `--capture-inbox`, `--glossary-init`, and `--migrate` only when updating wiki files is intended.
142
+ Use `--query`, `--handoff-show`, `--handoff-status`, `--prune-check`, `--issue-draft`, `--link-check`, `--quality-check`, `--doctor`, `--migration-lint`, `--migration-quality-check`, and `--migration-doctor` for read-only inspection/output through the resolved runner. Use `--doctor --fix` when safe generated routing refresh is intended. Use `--refresh-index`, `--capture-inbox`, `--handoff-save`, `--handoff-clear`, `--glossary-init`, and `--migrate` only when updating local project-librarian files is intended.
143
+
144
+ Session handoff files are generated local reference data, not canonical wiki truth. Do not promote handoff content into `wiki/canonical/`, `wiki/plans/`, or `wiki/decisions/` automatically; capture stable project-planning facts through the inbox/taxonomy workflow first.
136
145
 
137
146
  Use `--review-migration` or `--semantic-migrate` after migration coverage or compatible inbox rows are processed. It syncs unit coverage and safe file-level inbox statuses into `wiki/migration/review.md` and `wiki/migration/verification.md`.
138
147
 
@@ -320,6 +329,10 @@ These modes preserve this project's stricter source-of-truth boundaries:
320
329
  - `--issue-draft --issue-title "..."`: read-only Markdown problem-report draft for skill failures, side effects, confusing behavior, or generated-file surprises. It does not create a GitHub issue or require network access.
321
330
  - `--refresh-index`: updates a managed auto-discovered block in `wiki/index.md` for wiki files not routed by the hand-written index.
322
331
  - `--capture-inbox --title "..." --content "..."`: appends a project-candidate row to `wiki/inbox/project-candidates.md` and routes the inbox from `wiki/index.md`.
332
+ - `--handoff-save --goal "..." --state "..." --next "..."`: writes generated local resume context under `.project-wiki/session/`; repeat `--next`, `--decision`, `--blocked`, `--open-question`, and `--verification` as needed.
333
+ - `--handoff-show`, `--handoff-status`, `--handoff-clear`: print, inspect, or remove generated handoff state. Startup hooks only mention that a handoff exists; they do not inject the full handoff by default.
334
+ - `--handoff-promote-inbox`: appends selected generated handoff facts to `wiki/inbox/project-candidates.md` as pending candidate material. It does not write canonical, plan, or decision pages.
335
+ - `--handoff-injection-enable`, `--handoff-injection-disable`, `--handoff-injection-status`: opt in, opt out, or inspect the capped full handoff injection experiment. Keep it off by default unless measured resume behavior justifies the added startup context.
323
336
  - `--prune-check`: read-only report of active wiki pages with pending/proposed/stale review signals.
324
337
 
325
338
  Captured inbox entries are not canonical. Fold them into `wiki/canonical/`, `wiki/decisions/`, `wiki/sources/`, or `wiki/meta/` only after review.
package/dist/args.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.codeImpactTarget = exports.codeContextPackTarget = exports.wikiVisualizeOutput = exports.wikiVisualizeMode = exports.wikiImpactTarget = exports.wikiImpactMode = exports.queryTerm = exports.codeSearchSymbolMode = exports.codeQueryMode = exports.codeImpactMode = exports.codeParserMode = exports.codeContextPackMode = exports.codeFilesMode = exports.codeStatusMode = exports.codeReportMode = exports.codeIndexHealthMode = exports.codeIndexFullMode = exports.codeIndexIncrementalMode = exports.codeIndexMode = exports.acknowledgeSmallRepoMode = exports.noGitConfigMode = exports.reviewMigrationMode = exports.pruneCheckStrictMode = exports.pruneCheckMode = exports.captureInboxMode = exports.refreshIndexMode = exports.issueDraftMode = exports.issueCreateMode = exports.glossaryMode = exports.fixMode = exports.doctorMode = exports.qualityCheckMode = exports.linkCheckMode = exports.migrationQualityCheckMode = exports.migrationLintMode = exports.migrationDoctorMode = exports.lintMode = exports.migrateMode = exports.invalidAgentTargets = exports.missingValueOptions = exports.unexpectedValueOptions = exports.unknownOptions = exports.args = exports.commandArgs = exports.command = exports.unknownCommand = exports.helpMode = exports.agentTargets = exports.parsedArgs = exports.rawArgs = void 0;
4
- exports.issueDraftTitle = exports.issueBodyFile = exports.captureCategory = exports.captureContent = exports.captureTitle = exports.codeIndexScopes = exports.codeParser = exports.codeIndexOutput = exports.codeSearchSymbol = exports.codeReportSection = exports.codeQuerySql = void 0;
3
+ exports.codeIndexMode = exports.acknowledgeSmallRepoMode = exports.noGitConfigMode = exports.reviewMigrationMode = exports.pruneCheckStrictMode = exports.pruneCheckMode = exports.captureInboxMode = exports.refreshIndexMode = exports.issueDraftMode = exports.issueCreateMode = exports.handoffVerification = exports.handoffStatusMode = exports.handoffState = exports.handoffShowMode = exports.handoffSaveMode = exports.handoffPromoteInboxMode = exports.handoffInjectionStatusMode = exports.handoffInjectionEnableMode = exports.handoffInjectionDisableMode = exports.handoffOpenQuestions = exports.handoffNextActions = exports.handoffLastSuccessCommand = exports.handoffLastFailureCommand = exports.handoffInputMode = exports.handoffGoal = exports.handoffDecisions = exports.handoffClearMode = exports.handoffBlocked = exports.glossaryMode = exports.fixMode = exports.doctorMode = exports.qualityCheckMode = exports.linkCheckMode = exports.migrationQualityCheckMode = exports.migrationLintMode = exports.migrationDoctorMode = exports.lintMode = exports.migrateMode = exports.invalidAgentTargets = exports.missingValueOptions = exports.unexpectedValueOptions = exports.unknownOptions = exports.args = exports.commandArgs = exports.command = exports.unknownCommand = exports.helpMode = exports.agentTargets = exports.parsedArgs = exports.rawArgs = void 0;
4
+ exports.issueDraftTitle = exports.issueBodyFile = exports.captureCategory = exports.captureContent = exports.captureTitle = exports.codeIndexScopes = exports.codeParser = exports.codeIndexOutput = exports.codeSearchSymbol = exports.codeReportSection = exports.codeQuerySql = exports.codeImpactTarget = exports.codeContextPackTarget = exports.wikiVisualizeOutput = exports.wikiVisualizeMode = exports.wikiImpactTarget = exports.wikiImpactMode = exports.queryTerm = exports.codeSearchSymbolMode = exports.codeQueryMode = exports.codeImpactMode = exports.codeParserMode = exports.codeContextPackMode = exports.codeFilesMode = exports.codeStatusMode = exports.codeReportMode = exports.codeIndexHealthMode = exports.codeIndexFullMode = exports.codeIndexIncrementalMode = void 0;
5
5
  exports.parseArgs = parseArgs;
6
6
  exports.argValue = argValue;
7
7
  exports.argValues = argValues;
@@ -34,6 +34,23 @@ const flagDefinitions = [
34
34
  { name: "--dry-run", value: "none" },
35
35
  { name: "--fix", value: "none" },
36
36
  { name: "--glossary-init", value: "none" },
37
+ { name: "--blocked", value: "value" },
38
+ { name: "--decision", value: "value" },
39
+ { name: "--goal", value: "value" },
40
+ { name: "--handoff-clear", value: "none" },
41
+ { name: "--handoff-injection-disable", value: "none" },
42
+ { name: "--handoff-injection-enable", value: "none" },
43
+ { name: "--handoff-injection-status", value: "none" },
44
+ { name: "--handoff-promote-inbox", value: "none" },
45
+ { name: "--handoff-save", value: "none" },
46
+ { name: "--handoff-show", value: "none" },
47
+ { name: "--handoff-status", value: "none" },
48
+ { name: "--last-failure-command", value: "value" },
49
+ { name: "--last-success-command", value: "value" },
50
+ { name: "--next", value: "value" },
51
+ { name: "--open-question", value: "value" },
52
+ { name: "--state", value: "value" },
53
+ { name: "--verification", value: "value" },
37
54
  { name: "--issue-body-file", value: "value" },
38
55
  { name: "--issue-create", value: "none" },
39
56
  { name: "--issue-draft", value: "none" },
@@ -138,6 +155,17 @@ function parseArgs(argv) {
138
155
  const codeQuerySql = argValueFromAny("--code-query");
139
156
  const codeSearchSymbol = argValueFromAny("--code-search-symbol");
140
157
  const parsedAgentTargets = (0, agent_surfaces_1.parseAgentSurfaceValues)(argValues("--agents"));
158
+ const handoffInputMode = [
159
+ "--blocked",
160
+ "--decision",
161
+ "--goal",
162
+ "--last-failure-command",
163
+ "--last-success-command",
164
+ "--next",
165
+ "--open-question",
166
+ "--state",
167
+ "--verification",
168
+ ].some(hasFlag);
141
169
  return {
142
170
  acknowledgeSmallRepoMode: args.has("--acknowledge-small-repo"),
143
171
  agentTargets: parsedAgentTargets.surfaces,
@@ -172,6 +200,24 @@ function parseArgs(argv) {
172
200
  fixMode: args.has("--fix"),
173
201
  glossaryMode: args.has("--glossary-init"),
174
202
  helpMode: argv.includes("--help") || argv.includes("-h"),
203
+ handoffBlocked: argValues("--blocked"),
204
+ handoffClearMode: args.has("--handoff-clear"),
205
+ handoffDecisions: argValues("--decision"),
206
+ handoffGoal: argValue("--goal"),
207
+ handoffInputMode,
208
+ handoffLastFailureCommand: argValue("--last-failure-command"),
209
+ handoffLastSuccessCommand: argValue("--last-success-command"),
210
+ handoffNextActions: argValues("--next"),
211
+ handoffOpenQuestions: argValues("--open-question"),
212
+ handoffInjectionDisableMode: args.has("--handoff-injection-disable"),
213
+ handoffInjectionEnableMode: args.has("--handoff-injection-enable"),
214
+ handoffInjectionStatusMode: args.has("--handoff-injection-status"),
215
+ handoffPromoteInboxMode: args.has("--handoff-promote-inbox"),
216
+ handoffSaveMode: args.has("--handoff-save"),
217
+ handoffShowMode: args.has("--handoff-show"),
218
+ handoffState: argValue("--state"),
219
+ handoffStatusMode: args.has("--handoff-status"),
220
+ handoffVerification: argValues("--verification"),
175
221
  issueBodyFile: argValue("--issue-body-file"),
176
222
  issueCreateMode: args.has("--issue-create"),
177
223
  issueDraftMode: args.has("--issue-draft"),
@@ -228,6 +274,24 @@ exports.qualityCheckMode = exports.parsedArgs.qualityCheckMode;
228
274
  exports.doctorMode = exports.parsedArgs.doctorMode;
229
275
  exports.fixMode = exports.parsedArgs.fixMode;
230
276
  exports.glossaryMode = exports.parsedArgs.glossaryMode;
277
+ exports.handoffBlocked = exports.parsedArgs.handoffBlocked;
278
+ exports.handoffClearMode = exports.parsedArgs.handoffClearMode;
279
+ exports.handoffDecisions = exports.parsedArgs.handoffDecisions;
280
+ exports.handoffGoal = exports.parsedArgs.handoffGoal;
281
+ exports.handoffInputMode = exports.parsedArgs.handoffInputMode;
282
+ exports.handoffLastFailureCommand = exports.parsedArgs.handoffLastFailureCommand;
283
+ exports.handoffLastSuccessCommand = exports.parsedArgs.handoffLastSuccessCommand;
284
+ exports.handoffNextActions = exports.parsedArgs.handoffNextActions;
285
+ exports.handoffOpenQuestions = exports.parsedArgs.handoffOpenQuestions;
286
+ exports.handoffInjectionDisableMode = exports.parsedArgs.handoffInjectionDisableMode;
287
+ exports.handoffInjectionEnableMode = exports.parsedArgs.handoffInjectionEnableMode;
288
+ exports.handoffInjectionStatusMode = exports.parsedArgs.handoffInjectionStatusMode;
289
+ exports.handoffPromoteInboxMode = exports.parsedArgs.handoffPromoteInboxMode;
290
+ exports.handoffSaveMode = exports.parsedArgs.handoffSaveMode;
291
+ exports.handoffShowMode = exports.parsedArgs.handoffShowMode;
292
+ exports.handoffState = exports.parsedArgs.handoffState;
293
+ exports.handoffStatusMode = exports.parsedArgs.handoffStatusMode;
294
+ exports.handoffVerification = exports.parsedArgs.handoffVerification;
231
295
  exports.issueCreateMode = exports.parsedArgs.issueCreateMode;
232
296
  exports.issueDraftMode = exports.parsedArgs.issueDraftMode;
233
297
  exports.refreshIndexMode = exports.parsedArgs.refreshIndexMode;
@@ -52,8 +52,10 @@ function pathOwnerKey(filePath) {
52
52
  return parts[0] ?? ".";
53
53
  }
54
54
  function readJsonObject(relativePath) {
55
+ if (!(0, workspace_1.containedProjectFileStat)(relativePath))
56
+ return null;
55
57
  try {
56
- const parsed = JSON.parse(fs.readFileSync((0, workspace_1.abs)(relativePath), "utf8"));
58
+ const parsed = JSON.parse((0, workspace_1.read)(relativePath));
57
59
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
58
60
  }
59
61
  catch {
@@ -83,19 +85,19 @@ function workspacePatternCandidates(pattern) {
83
85
  const suffix = normalized.slice(starIndex + 1).replace(/^\/+/, "");
84
86
  const base = prefix || ".";
85
87
  const basePath = (0, workspace_1.abs)(base);
86
- if (!fs.existsSync(basePath) || !fs.statSync(basePath).isDirectory())
88
+ if (!(0, workspace_1.containedProjectDirectoryStat)(base))
87
89
  return [];
88
90
  return fs.readdirSync(basePath, { withFileTypes: true })
89
91
  .filter((entry) => entry.isDirectory())
90
92
  .map((entry) => (0, workspace_1.normalizePath)(path.join(base, entry.name, suffix)))
91
- .filter((candidate) => fs.existsSync((0, workspace_1.abs)(candidate)) && fs.statSync((0, workspace_1.abs)(candidate)).isDirectory());
93
+ .filter((candidate) => (0, workspace_1.containedProjectDirectoryStat)(candidate));
92
94
  }
93
95
  function workspacePackages() {
94
96
  const packages = new Map();
95
97
  for (const pattern of workspacePatternsFromRootPackage()) {
96
98
  for (const candidate of workspacePatternCandidates(pattern)) {
97
99
  const packageJsonPath = (0, workspace_1.normalizePath)(path.join(candidate, "package.json"));
98
- if (!fs.existsSync((0, workspace_1.abs)(packageJsonPath)))
100
+ if (!(0, workspace_1.containedProjectFileStat)(packageJsonPath))
99
101
  continue;
100
102
  const packageJson = readJsonObject(packageJsonPath);
101
103
  const packageName = typeof packageJson?.name === "string" ? packageJson.name : candidate;
@@ -119,9 +121,9 @@ function codeownerRules() {
119
121
  const files = [".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"];
120
122
  const rules = [];
121
123
  for (const filePath of files) {
122
- if (!fs.existsSync((0, workspace_1.abs)(filePath)))
124
+ if (!(0, workspace_1.containedProjectFileStat)(filePath))
123
125
  continue;
124
- const lines = fs.readFileSync((0, workspace_1.abs)(filePath), "utf8").split(/\r?\n/);
126
+ const lines = (0, workspace_1.read)(filePath).split(/\r?\n/);
125
127
  lines.forEach((lineText, index) => {
126
128
  const trimmed = lineText.trim();
127
129
  if (!trimmed || trimmed.startsWith("#"))
@@ -4,17 +4,27 @@ exports.codeEvidenceNodeRuntimeRequirement = void 0;
4
4
  exports.loadDatabaseSync = loadDatabaseSync;
5
5
  exports.openDatabase = openDatabase;
6
6
  exports.codeEvidenceNodeRuntimeRequirement = "Node.js 22.13+ or 24+; node:sqlite was added in Node.js 22.5.0 and became available without --experimental-sqlite in Node.js 22.13.0";
7
+ function warningType(option) {
8
+ if (typeof option === "string")
9
+ return option;
10
+ if (typeof option !== "object" || option === null || !("type" in option))
11
+ return "";
12
+ const value = option.type;
13
+ return typeof value === "string" ? value : "";
14
+ }
15
+ function isSqliteExperimentalWarning(warning, options) {
16
+ const message = warning instanceof Error ? warning.message : typeof warning === "string" ? warning : "";
17
+ const type = warning instanceof Error ? warning.name : warningType(options[0]);
18
+ return type === "ExperimentalWarning" && message.includes("SQLite");
19
+ }
7
20
  function loadDatabaseSync(fail) {
8
- const previousListeners = process.listeners("warning");
9
- const suppressExperimentalSqliteWarning = (warning) => {
10
- if (warning.name !== "ExperimentalWarning" || !warning.message.includes("SQLite")) {
11
- for (const listener of previousListeners)
12
- listener.call(process, warning);
13
- }
14
- };
21
+ const previousEmitWarning = process.emitWarning;
15
22
  try {
16
- process.removeAllListeners("warning");
17
- process.on("warning", suppressExperimentalSqliteWarning);
23
+ process.emitWarning = ((warning, ...options) => {
24
+ if (isSqliteExperimentalWarning(warning, options))
25
+ return;
26
+ previousEmitWarning.call(process, warning, ...options);
27
+ });
18
28
  const sqlite = require("node:sqlite");
19
29
  return sqlite.DatabaseSync;
20
30
  }
@@ -23,9 +33,7 @@ function loadDatabaseSync(fail) {
23
33
  return fail(`code evidence index requires Node.js 22.13+ because it uses node:sqlite without experimental flags; current Node is ${process.version}. Runtime policy: ${exports.codeEvidenceNodeRuntimeRequirement}. Error: ${message}`);
24
34
  }
25
35
  finally {
26
- process.removeAllListeners("warning");
27
- for (const listener of previousListeners)
28
- process.on("warning", listener);
36
+ process.emitWarning = previousEmitWarning;
29
37
  }
30
38
  }
31
39
  function openDatabase(databasePath, fail) {
@@ -92,6 +92,8 @@ function isBlockedSensitiveConfigFile(relativePath) {
92
92
  const base = path.basename(relativePath).toLowerCase();
93
93
  if (base === ".env.example")
94
94
  return false;
95
+ if (base.startsWith("."))
96
+ return true;
95
97
  return /(^|[._-])(secret|secrets|credential|credentials|token|tokens|private|key|keys)([._-]|$)/i.test(base);
96
98
  }
97
99
  function isJavaScriptLike(relativePath) {
@@ -130,7 +132,9 @@ function walkCodeFiles(relativePath, files = []) {
130
132
  const target = (0, workspace_1.abs)(relativePath);
131
133
  if (!fs.existsSync(target))
132
134
  return files;
133
- const stat = fs.statSync(target);
135
+ const stat = fs.lstatSync(target);
136
+ if (stat.isSymbolicLink())
137
+ return files.sort();
134
138
  if (stat.isFile()) {
135
139
  if (stat.size <= exports.maxIndexedBytes && shouldIndexFile(relativePath))
136
140
  files.push(relativePath);
@@ -143,8 +147,8 @@ function walkCodeFiles(relativePath, files = []) {
143
147
  walkCodeFiles(child, files);
144
148
  }
145
149
  else if (entry.isFile() && shouldIndexFile(child)) {
146
- const childStat = fs.statSync((0, workspace_1.abs)(child));
147
- if (childStat.size <= exports.maxIndexedBytes)
150
+ const childStat = (0, workspace_1.containedProjectFileStat)(child);
151
+ if (childStat && childStat.size <= exports.maxIndexedBytes)
148
152
  files.push(child);
149
153
  }
150
154
  }
@@ -164,13 +168,7 @@ function gitTrackedAndUnignoredFiles(scopes) {
164
168
  }
165
169
  }
166
170
  function indexableFileStat(file) {
167
- try {
168
- const stat = fs.statSync((0, workspace_1.abs)(file));
169
- return stat.isFile() ? stat : null;
170
- }
171
- catch {
172
- return null;
173
- }
171
+ return (0, workspace_1.containedProjectFileStat)(file);
174
172
  }
175
173
  function discoverCodeFiles(scopes) {
176
174
  const gitFiles = gitTrackedAndUnignoredFiles(scopes);
@@ -114,7 +114,7 @@ function normalizedMtimeMs(stat) {
114
114
  return Number(stat.mtimeMs.toFixed(3));
115
115
  }
116
116
  function readCodeFileFingerprint(relativePath) {
117
- const stat = fs.statSync((0, workspace_1.abs)(relativePath));
117
+ const { stat } = (0, workspace_1.requireContainedProjectFile)(relativePath, "code-index file");
118
118
  return {
119
119
  mtimeMs: normalizedMtimeMs(stat),
120
120
  path: relativePath,
@@ -122,7 +122,8 @@ function readCodeFileFingerprint(relativePath) {
122
122
  };
123
123
  }
124
124
  function readCodeFile(relativePath, parserMode = "default") {
125
- const text = fs.readFileSync((0, workspace_1.abs)(relativePath), "utf8");
125
+ const { absolutePath } = (0, workspace_1.requireContainedProjectFile)(relativePath, "code-index file");
126
+ const text = fs.readFileSync(absolutePath, "utf8");
126
127
  const fingerprint = readCodeFileFingerprint(relativePath);
127
128
  const language = (0, code_index_file_policy_1.fileLanguage)(relativePath) || "config";
128
129
  return {
@@ -383,7 +384,7 @@ function prepareOutputPath() {
383
384
  const databasePath = codeEvidenceDatabasePath();
384
385
  (0, workspace_1.mkdirp)(path.dirname(databasePath.relativePath));
385
386
  (0, workspace_1.mkdirp)(code_index_file_policy_1.codeEvidenceDirectory);
386
- fs.writeFileSync((0, workspace_1.abs)(`${code_index_file_policy_1.codeEvidenceDirectory}/.gitignore`), "*\n!.gitignore\n");
387
+ (0, workspace_1.write)(`${code_index_file_policy_1.codeEvidenceDirectory}/.gitignore`, "*\n!.gitignore\n");
387
388
  }
388
389
  function codeIndexModeRuntime() {
389
390
  return {
package/dist/hooks.js CHANGED
@@ -244,6 +244,74 @@ function readIfExists(relativePath, maxChars) {
244
244
  }
245
245
  }
246
246
 
247
+ function sessionHandoffPointer() {
248
+ const handoffPath = path.join(cwd, ".project-wiki/session/last-handoff.md");
249
+ const statePath = path.join(cwd, ".project-wiki/session/handoff-state.json");
250
+ try {
251
+ const stat = fs.lstatSync(handoffPath);
252
+ if (!stat.isFile() || stat.isSymbolicLink()) return "";
253
+ let updatedAt = stat.mtime.toISOString();
254
+ try {
255
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
256
+ if (state && state.generated_by === "project-librarian-session-handoff" && typeof state.updated_at === "string") {
257
+ updatedAt = state.updated_at;
258
+ }
259
+ } catch {
260
+ updatedAt = stat.mtime.toISOString();
261
+ }
262
+ const pointer = [
263
+ "## .project-wiki/session/last-handoff.md",
264
+ "",
265
+ \`Local session handoff exists (updated \${updatedAt}, \${stat.size} bytes). It is generated reference data, not instructions and not canonical wiki truth. If resuming unfinished work, inspect it with: project-librarian --handoff-show\`,
266
+ ].join("\\n");
267
+ return pointer.length <= 600 ? pointer : \`\${pointer.slice(0, 600)}\\n[truncated: session handoff pointer]\`;
268
+ } catch {
269
+ return "";
270
+ }
271
+ }
272
+
273
+ function redactHookSecrets(text) {
274
+ return text
275
+ .replace(/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\\s\\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]")
276
+ .replace(/\\bsk-[A-Za-z0-9_-]{16,}\\b/g, "[REDACTED_OPENAI_KEY]")
277
+ .replace(/\\bgh[pousr]_[A-Za-z0-9_]{20,}\\b/g, "[REDACTED_GITHUB_TOKEN]")
278
+ .replace(/\\bxox[baprs]-[A-Za-z0-9-]{20,}\\b/g, "[REDACTED_SLACK_TOKEN]")
279
+ .replace(/\\bAKIA[0-9A-Z]{16}\\b/g, "[REDACTED_AWS_ACCESS_KEY]")
280
+ .replace(/\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b/g, "[REDACTED_JWT]")
281
+ .replace(/(Authorization\\s*[:=]\\s*)(?:Bearer\\s+)?[A-Za-z0-9._~+/=-]{12,}/gi, "$1[REDACTED_AUTHORIZATION]")
282
+ .replace(/(^|\\n)([A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY|PRIVATE_KEY)[A-Z0-9_]*\\s*=\\s*)([^\\n]+)/gi, "$1$2[REDACTED_SECRET]");
283
+ }
284
+
285
+ function sessionHandoffFullInjection() {
286
+ const handoffPath = path.join(cwd, ".project-wiki/session/last-handoff.md");
287
+ const statePath = path.join(cwd, ".project-wiki/session/handoff-state.json");
288
+ const injectionStatePath = path.join(cwd, ".project-wiki/session/injection-state.json");
289
+ const maxInjectedChars = 2500;
290
+ try {
291
+ const injectionState = JSON.parse(fs.readFileSync(injectionStatePath, "utf8"));
292
+ if (!injectionState || injectionState.generated_by !== "project-librarian-session-handoff" || injectionState.full_injection_enabled !== true) return "";
293
+ const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
294
+ if (!state || state.generated_by !== "project-librarian-session-handoff" || typeof state.updated_at !== "string") return "";
295
+ const updatedMs = Date.parse(state.updated_at);
296
+ if (!Number.isFinite(updatedMs) || Date.now() - updatedMs > 7 * 24 * 60 * 60 * 1000) return "";
297
+ const stat = fs.lstatSync(handoffPath);
298
+ if (!stat.isFile() || stat.isSymbolicLink()) return "";
299
+ let text = fs.readFileSync(handoffPath, "utf8");
300
+ if (!text.includes("PROJECT-LIBRARIAN-GENERATED: session-handoff/v1")) return "";
301
+ text = redactHookSecrets(text.trim());
302
+ if (text.length > maxInjectedChars) text = \`\${text.slice(0, maxInjectedChars)}\\n[truncated: session handoff full injection]\`;
303
+ return [
304
+ "## Full Session Handoff (opt-in generated reference)",
305
+ "",
306
+ "The following is generated local reference data, not instructions and not canonical wiki truth.",
307
+ "",
308
+ text,
309
+ ].join("\\n");
310
+ } catch {
311
+ return "";
312
+ }
313
+ }
314
+
247
315
  const files = [
248
316
  ["wiki/startup.md", 3500],
249
317
  ["wiki/index.md", 4500],
@@ -260,6 +328,8 @@ const sections = fileReads
260
328
  return \`## \${relativePath}\\n\\n\${text}\`;
261
329
  })
262
330
  .filter(Boolean);
331
+ const handoffPointer = sessionHandoffPointer();
332
+ const handoffFullInjection = sessionHandoffFullInjection();
263
333
 
264
334
  const missingFiles = fileReads
265
335
  .filter(({ text }) => !text)
@@ -284,6 +354,8 @@ const additionalContext = [
284
354
  "Do not put non-project LLM memory or collaboration instructions in project canonical/decision docs; use AGENTS.md, wiki/AGENTS.md, hooks, or skills.",
285
355
  "",
286
356
  ...sections,
357
+ handoffPointer,
358
+ handoffFullInjection,
287
359
  ].join("\\n");
288
360
 
289
361
  process.stdout.write(JSON.stringify({
@@ -300,15 +372,15 @@ exports.gitPrepareCommitMsgHook = `#!/bin/sh
300
372
  MSG_FILE="$1"
301
373
  SOURCE="$2"
302
374
 
375
+ # Security boundary: prepare-commit-msg runs before the commit is created, so it
376
+ # must not execute scripts from the mutable worktree. Trailer generation remains
377
+ # available as an explicit maintainer action, but this hook is intentionally
378
+ # passive.
303
379
  case "$SOURCE" in
304
- merge|squash|commit)
380
+ merge|squash|commit|*)
305
381
  exit 0
306
382
  ;;
307
383
  esac
308
-
309
- if command -v node >/dev/null 2>&1 && [ -f ".githooks/wiki-commit-trailers.js" ]; then
310
- node .githooks/wiki-commit-trailers.js "$MSG_FILE"
311
- fi
312
384
  `;
313
385
  exports.gitWikiCommitTrailersScript = `#!/usr/bin/env node
314
386