project-librarian 0.5.2 → 0.5.4
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 +14 -3
- package/README.md +14 -3
- package/SKILL.md +25 -1
- package/dist/args.js +66 -2
- package/dist/hooks.js +94 -5
- package/dist/init-project-wiki.js +56 -0
- package/dist/mcp-server.js +23 -0
- package/dist/modes.js +12 -5
- package/dist/session-handoff.js +627 -0
- package/dist/templates.js +2 -0
- package/package.json +3 -1
package/README.ko.md
CHANGED
|
@@ -63,6 +63,7 @@ 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
|
+
- **로컬 세션 핸드오프.** `--handoff-save`는 `.project-wiki/session/` 아래에 생성형 재개 메모를 저장합니다. 시작 훅은 존재만 알려주므로 실행 로그가 canonical 위키 정본으로 섞이지 않습니다.
|
|
66
67
|
- **선택적 코드 근거.** 재생성 가능한 SQLite 인덱스와 답변 형태 MCP 도구가 영향·소유권·워크스페이스 그래프 질문에 답하며, 추가 런타임 의존성이 전혀 없습니다.
|
|
67
68
|
- **다시 실행해도 안전.** 부트스트랩은 멱등하고 기존 내용 보존을 우선하며, 진단은 깨진 경로·도달 불가 페이지·오래된 사실을 에이전트가 오해하기 전에 잡아냅니다.
|
|
68
69
|
|
|
@@ -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` |
|
|
@@ -293,7 +299,7 @@ MCP 서버 등록 (`mcpServers`에 기존 항목 보존하며 병합):
|
|
|
293
299
|
|
|
294
300
|
`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`가 인덱스가 오래되었다고 보고하면 경고를 앞에 붙입니다.
|
|
295
301
|
|
|
296
|
-
서버는 고정 리소스 — `project-librarian://wiki/startup`, `project-librarian://wiki/index`, `project-librarian://code/status` — 와 위키 분류 갱신, 코드 영향 추적, 검색 품질 검토용 프롬프트 템플릿도 제공합니다. 리소스 읽기는 임의 파일 경로가 아니라 고정 URI 레지스트리에서만 처리합니다.
|
|
302
|
+
서버는 고정 리소스 — `project-librarian://wiki/startup`, `project-librarian://wiki/index`, `project-librarian://code/status` — 와 위키 분류 갱신, 코드 영향 추적, 유지보수 개선 검토, 검색 품질 검토용 프롬프트 템플릿도 제공합니다. 리소스 읽기는 임의 파일 경로가 아니라 고정 URI 레지스트리에서만 처리합니다.
|
|
297
303
|
|
|
298
304
|
부트스트랩은 Claude Code(`.mcp.json`), Cursor(`.cursor/mcp.json`), Gemini CLI(`.gemini/settings.json`의 `mcpServers`)에 서버를 등록하며, 기존 서버와 키를 보존하고 다시 실행하면 `exists`를 보고합니다. 저장소에 로컬 실행 경로가 있으면 `node <runner> mcp`로, 없으면 설치된 `project-librarian mcp` 바이너리로 등록합니다.
|
|
299
305
|
|
|
@@ -383,6 +389,10 @@ node .codex/skills/project-librarian/dist/init-project-wiki.js install [--scope
|
|
|
383
389
|
| `--wiki-visualize-out <path>` | `--wiki-visualize`와 함께 사용해 `.project-wiki/` 아래의 사용자 지정 저장소 상대 경로에 작성합니다. |
|
|
384
390
|
| `--refresh-index` | 생성된 자동 발견 위키 라우팅을 갱신합니다. |
|
|
385
391
|
| `--capture-inbox --title <title> --content <content>` | 위키 수신함에 후보 메모를 추가합니다. |
|
|
392
|
+
| `--handoff-save --goal <goal> --state <state> --next <action>` | `.project-wiki/session/` 아래에 생성형 로컬 세션 핸드오프를 저장합니다. 필요하면 `--next`, `--decision`, `--blocked`, `--open-question`, `--verification`을 반복합니다. |
|
|
393
|
+
| `--handoff-show`, `--handoff-status`, `--handoff-clear` | 생성된 세션 핸드오프를 출력, 점검, 삭제합니다. 시작 훅은 핸드오프 존재만 알리고 기본적으로 전체 파일을 주입하지 않습니다. |
|
|
394
|
+
| `--handoff-promote-inbox` | 생성된 핸드오프의 선별된 사실만 `wiki/inbox/project-candidates.md`에 pending 후보로 추가합니다. canonical, plan, decision 문서는 쓰지 않습니다. |
|
|
395
|
+
| `--handoff-injection-enable`, `--handoff-injection-disable`, `--handoff-injection-status` | 2,500자 상한의 전체 핸드오프 주입 실험을 켜고, 끄고, 상태를 확인합니다. 기본 시작 동작은 계속 pointer-only입니다. |
|
|
386
396
|
| `--issue-draft --issue-title <title>` | 문제 또는 부작용에 대한 읽기 전용 GitHub 이슈 본문 초안을 출력합니다. |
|
|
387
397
|
| `--issue-create --issue-title <title>` | 명시적 사용자 승인 후 `gh`로 GitHub 이슈를 생성합니다. |
|
|
388
398
|
| `--glossary-init` | 선택적 용어집 페이지를 만들고 라우팅합니다. |
|
|
@@ -407,6 +417,7 @@ node .codex/skills/project-librarian/dist/init-project-wiki.js install [--scope
|
|
|
407
417
|
npm install
|
|
408
418
|
npm run typecheck
|
|
409
419
|
npm run build
|
|
420
|
+
npm run check:dist
|
|
410
421
|
npm test
|
|
411
422
|
npm run test:coverage
|
|
412
423
|
npm run benchmark:llm:raw-audit
|
|
@@ -416,11 +427,11 @@ npm run release:check
|
|
|
416
427
|
npm pack --dry-run
|
|
417
428
|
```
|
|
418
429
|
|
|
419
|
-
`src/` 아래 TypeScript를 수정할 때는 커밋 전에 빌드해 `dist/`를 최신 상태로 유지하세요.
|
|
430
|
+
`src/` 아래 TypeScript를 수정할 때는 커밋 전에 빌드해 `dist/`를 최신 상태로 유지하세요. `npm run check:dist`는 체크인된 생성 파일이 최신 빌드 출력과 일치하는지 확인합니다.
|
|
420
431
|
|
|
421
432
|
`npm run test:coverage`는 Node 내장 test coverage에 보수적인 line, branch, function threshold를 적용하므로 coverage를 단순 보고서가 아니라 회귀 게이트로 사용합니다.
|
|
422
433
|
|
|
423
|
-
`npm run release:check`는 로컬 전용 관리자 게이트입니다. 테스트, Node 내장 coverage, 벤치마크 파서 smoke, real-corpus 오프라인 데모, 벤치마크 release preview, 벤치마크 claim ledger 분류, raw 보관 상태 감사, package dry-run 검사, dist 실행 가능
|
|
434
|
+
`npm run release:check`는 로컬 전용 관리자 게이트입니다. 테스트, Node 내장 coverage, 벤치마크 파서 smoke, real-corpus 오프라인 데모, 벤치마크 release preview, 벤치마크 claim ledger 분류, raw 보관 상태 감사, package dry-run 검사, dist 실행 가능 여부와 소스 동기화, README 벤치마크 claim 경계 문구를 확인합니다. publish하지 않고 raw 벤치마크 산출물을 삭제하지 않으며 measured Codex 벤치마크도 실행하지 않습니다.
|
|
424
435
|
|
|
425
436
|
`release:check` 통과는 런타임 보증이 아니라 재현 가능한 릴리스 준비 근거로 봐야 합니다. 현재 checkout에서 위 로컬 게이트를 통과했음을 증명하며, package dry run이 예상 publish 경계(`agents/`, `dist/`, `LICENSE`, `README.md`, `README.ko.md`, `SKILL.md`) 안에 머물고 소스 파일, 테스트, 저장소 로컬 위키/워크플로 상태, raw 벤치마크 출력, 로컬 캐시를 제외하는지도 확인합니다.
|
|
426
437
|
|
package/README.md
CHANGED
|
@@ -63,6 +63,7 @@ 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
|
+
- **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.
|
|
66
67
|
- **Optional code evidence.** A regenerable SQLite index plus answer-shaped MCP tools answer impact, ownership, and workspace-graph questions, with zero extra runtime dependencies.
|
|
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
|
|
|
@@ -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` |
|
|
@@ -293,7 +299,7 @@ Disposable code evidence cache:
|
|
|
293
299
|
|
|
294
300
|
`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.
|
|
295
301
|
|
|
296
|
-
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, and retrieval quality reviews. Resource reads come from a fixed URI registry rather than arbitrary filesystem paths.
|
|
302
|
+
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
303
|
|
|
298
304
|
Bootstrap registers the server for Claude Code (`.mcp.json`), Cursor (`.cursor/mcp.json`), and Gemini CLI (`mcpServers` in `.gemini/settings.json`), preserving any existing servers and keys and reporting `exists` on a re-run. When the repository contains a local runner the registration uses `node <runner> mcp`; otherwise it uses the installed `project-librarian mcp` binary.
|
|
299
305
|
|
|
@@ -383,6 +389,10 @@ Important options:
|
|
|
383
389
|
| `--wiki-visualize-out <path>` | With `--wiki-visualize`, write to a custom repository-relative path under `.project-wiki/`. |
|
|
384
390
|
| `--refresh-index` | Update generated auto-discovered wiki routing. |
|
|
385
391
|
| `--capture-inbox --title <title> --content <content>` | Append a candidate note to the wiki inbox. |
|
|
392
|
+
| `--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. |
|
|
393
|
+
| `--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. |
|
|
394
|
+
| `--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. |
|
|
395
|
+
| `--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
396
|
| `--issue-draft --issue-title <title>` | Print a read-only GitHub issue body draft for problems or side effects. |
|
|
387
397
|
| `--issue-create --issue-title <title>` | Create a GitHub issue through `gh` after explicit user approval. |
|
|
388
398
|
| `--glossary-init` | Create and route the optional glossary page. |
|
|
@@ -407,6 +417,7 @@ The source is TypeScript. The committed `dist/` directory is the compiled JavaSc
|
|
|
407
417
|
npm install
|
|
408
418
|
npm run typecheck
|
|
409
419
|
npm run build
|
|
420
|
+
npm run check:dist
|
|
410
421
|
npm test
|
|
411
422
|
npm run test:coverage
|
|
412
423
|
npm run benchmark:llm:raw-audit
|
|
@@ -416,11 +427,11 @@ npm run release:check
|
|
|
416
427
|
npm pack --dry-run
|
|
417
428
|
```
|
|
418
429
|
|
|
419
|
-
When editing TypeScript under `src/`, rebuild before committing so `dist/` stays current.
|
|
430
|
+
When editing TypeScript under `src/`, rebuild before committing so `dist/` stays current. `npm run check:dist` verifies that the checked-in generated files still match the latest build output.
|
|
420
431
|
|
|
421
432
|
`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.
|
|
422
433
|
|
|
423
|
-
`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 checks, and README benchmark-claim boundary checks. It never publishes, never deletes raw benchmark artifacts, and never launches a measured Codex benchmark.
|
|
434
|
+
`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.
|
|
424
435
|
|
|
425
436
|
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.
|
|
426
437
|
|
package/SKILL.md
CHANGED
|
@@ -19,9 +19,11 @@ 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.
|
|
26
|
+
- Turn broad maintenance, automation, efficiency, or "improve this project" requests into an analyze-first backlog and execution plan before editing.
|
|
25
27
|
- Analyze existing code and canonicalize code-backed project behavior, features, policies, constraints, terminology, domain rules, and open questions into the wiki.
|
|
26
28
|
- Build and query an optional SQLite code evidence index for large repositories.
|
|
27
29
|
- Migrate an existing wiki/docs structure.
|
|
@@ -48,6 +50,16 @@ Use `npx` or `npm exec` only when no local runner exists and registry access is
|
|
|
48
50
|
|
|
49
51
|
If the resolved runner fails, report the real error and stop or fix the cause. Do not manually recreate bootstrap or migration output as a fallback.
|
|
50
52
|
|
|
53
|
+
Broad improvement automation requests are discovery-and-execution work, not routine bootstrap updates. When the user says something like "improve this project", "start improvement automation", "개선 자동화 시작해", or asks for maintainability/automation/efficiency improvements without naming a concrete lifecycle flag:
|
|
54
|
+
|
|
55
|
+
- Do not map the request directly to `$PROJECT_LIBRARIAN update` or a single diagnostic command.
|
|
56
|
+
- Inspect the repo's wiki contract, code structure, package scripts, CI, tests, release gates, generated surfaces, dependency posture, and obvious maintenance bottlenecks.
|
|
57
|
+
- Use code-evidence tools or reports when the repo is large or the question is structural; use direct targeted reads for small repos and simple lookups.
|
|
58
|
+
- Produce a ranked backlog with file/command evidence, confidence, expected verification, and risk.
|
|
59
|
+
- Persist durable planning output under `wiki/plans/` when the request changes project-planning state, then refresh the index and run lint when practical.
|
|
60
|
+
- If the user asked to start or continue work, implement safe high-priority items in a branch, update generated outputs, and verify with targeted tests plus the smallest broad gate that proves the claim.
|
|
61
|
+
- Keep unresolved, high-risk, or dependency-changing candidates in the backlog instead of silently skipping them.
|
|
62
|
+
|
|
51
63
|
2. For project bootstrap requests, choose the matching command and run it from the project root. The examples below use `$PROJECT_LIBRARIAN` to mean the resolved runner from step 1:
|
|
52
64
|
|
|
53
65
|
```bash
|
|
@@ -79,6 +91,12 @@ Map lifecycle requests to these internal operations:
|
|
|
79
91
|
- Search the wiki: `$PROJECT_LIBRARIAN --query "search terms"`.
|
|
80
92
|
- Refresh wiki routing/index: `$PROJECT_LIBRARIAN --refresh-index`.
|
|
81
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`.
|
|
82
100
|
- Check stale/pending pages: `$PROJECT_LIBRARIAN --prune-check`.
|
|
83
101
|
- Draft a GitHub issue body for a skill problem or side effect: `$PROJECT_LIBRARIAN --issue-draft --issue-title "Issue title"`.
|
|
84
102
|
- After explicit user approval in a GitHub-backed repository, create the issue through GitHub CLI: `$PROJECT_LIBRARIAN --issue-create --issue-title "Issue title"`.
|
|
@@ -121,7 +139,9 @@ Use `--lint` for read-only validation:
|
|
|
121
139
|
$PROJECT_LIBRARIAN --lint
|
|
122
140
|
```
|
|
123
141
|
|
|
124
|
-
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
|
|
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.
|
|
125
145
|
|
|
126
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`.
|
|
127
147
|
|
|
@@ -309,6 +329,10 @@ These modes preserve this project's stricter source-of-truth boundaries:
|
|
|
309
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.
|
|
310
330
|
- `--refresh-index`: updates a managed auto-discovered block in `wiki/index.md` for wiki files not routed by the hand-written index.
|
|
311
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.
|
|
312
336
|
- `--prune-check`: read-only report of active wiki pages with pending/proposed/stale review signals.
|
|
313
337
|
|
|
314
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.
|
|
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;
|
package/dist/hooks.js
CHANGED
|
@@ -244,29 +244,118 @@ 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],
|
|
250
318
|
];
|
|
251
319
|
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
320
|
+
const fileReads = files.map(([relativePath, maxChars]) => {
|
|
321
|
+
const text = readIfExists(relativePath, maxChars);
|
|
322
|
+
return { relativePath, text };
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const sections = fileReads
|
|
326
|
+
.map(({ relativePath, text }) => {
|
|
255
327
|
if (!text) return "";
|
|
256
328
|
return \`## \${relativePath}\\n\\n\${text}\`;
|
|
257
329
|
})
|
|
258
330
|
.filter(Boolean);
|
|
331
|
+
const handoffPointer = sessionHandoffPointer();
|
|
332
|
+
const handoffFullInjection = sessionHandoffFullInjection();
|
|
333
|
+
|
|
334
|
+
const missingFiles = fileReads
|
|
335
|
+
.filter(({ text }) => !text)
|
|
336
|
+
.map(({ relativePath }) => relativePath);
|
|
337
|
+
|
|
338
|
+
const inclusionNotice = missingFiles.length === 0
|
|
339
|
+
? [
|
|
340
|
+
"Injected context: wiki/startup.md and wiki/index.md are ALREADY included below this line.",
|
|
341
|
+
"Do not re-read these two files this session; route any further reads through the index.",
|
|
342
|
+
]
|
|
343
|
+
: [
|
|
344
|
+
\`Project wiki startup files were not fully included; missing or empty: \${missingFiles.join(", ")}.\`,
|
|
345
|
+
"Run Project Librarian to bootstrap or restore the missing wiki files before relying on wiki-first routing.",
|
|
346
|
+
];
|
|
259
347
|
|
|
260
348
|
const additionalContext = [
|
|
261
349
|
"[Project wiki startup review]",
|
|
262
|
-
|
|
263
|
-
"Do not re-read these two files this session; route any further reads through the index.",
|
|
350
|
+
...inclusionNotice,
|
|
264
351
|
"Use ./wiki as the project-planning source of truth only. Start with compact routing context; read detailed project canonical, decision, or meta files on demand.",
|
|
265
352
|
"Project canonical content language is selected from user/project context; do not assume a fixed default language.",
|
|
266
353
|
"When project planning content is added, changed, or removed, update ./wiki in the same turn.",
|
|
267
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.",
|
|
268
355
|
"",
|
|
269
356
|
...sections,
|
|
357
|
+
handoffPointer,
|
|
358
|
+
handoffFullInjection,
|
|
270
359
|
].join("\\n");
|
|
271
360
|
|
|
272
361
|
process.stdout.write(JSON.stringify({
|
|
@@ -7,6 +7,7 @@ const hooks_1 = require("./hooks");
|
|
|
7
7
|
const install_skill_1 = require("./install-skill");
|
|
8
8
|
const modes_1 = require("./modes");
|
|
9
9
|
const migration_1 = require("./migration");
|
|
10
|
+
const session_handoff_1 = require("./session-handoff");
|
|
10
11
|
const templates_1 = require("./templates");
|
|
11
12
|
const wiki_visualizer_1 = require("./wiki-visualizer");
|
|
12
13
|
const workspace_1 = require("./workspace");
|
|
@@ -39,6 +40,16 @@ Options:
|
|
|
39
40
|
--wiki-visualize-out <path> With --wiki-visualize, write under a custom .project-wiki/ path.
|
|
40
41
|
--refresh-index Update the managed auto-discovered wiki index block.
|
|
41
42
|
--capture-inbox Append a candidate note with --title, --content, and optional --category.
|
|
43
|
+
--handoff-save Save local generated session handoff state under .project-wiki/session/.
|
|
44
|
+
--handoff-show Print the current local session handoff.
|
|
45
|
+
--handoff-status Print JSON status for the local session handoff.
|
|
46
|
+
--handoff-clear Remove generated session handoff files.
|
|
47
|
+
--handoff-promote-inbox Promote selected generated handoff facts into wiki/inbox/project-candidates.md.
|
|
48
|
+
--handoff-injection-enable Opt in to capped full handoff injection in startup hooks.
|
|
49
|
+
--handoff-injection-disable Remove the generated full handoff injection opt-in.
|
|
50
|
+
--handoff-injection-status Print JSON status for the full handoff injection experiment.
|
|
51
|
+
--goal, --state, --blocked With --handoff-save, provide resume context fields.
|
|
52
|
+
--next, --decision With --handoff-save, repeat for next actions and decisions.
|
|
42
53
|
--glossary-init Create and route the optional glossary page.
|
|
43
54
|
--agents <list> With init/update, write only selected agent surfaces: codex, claude, cursor, gemini, or all. Existing project skill/setup surfaces are preserved by default.
|
|
44
55
|
--prune-check Report active pages with stale or unresolved signals.
|
|
@@ -161,6 +172,24 @@ if (args_1.codeIndexIncrementalMode && args_1.codeIndexFullMode) {
|
|
|
161
172
|
console.error("Use one code index update mode at a time: --incremental or --code-index-full.");
|
|
162
173
|
process.exit(1);
|
|
163
174
|
}
|
|
175
|
+
const activeHandoffModes = [
|
|
176
|
+
args_1.handoffSaveMode ? "--handoff-save" : "",
|
|
177
|
+
args_1.handoffShowMode ? "--handoff-show" : "",
|
|
178
|
+
args_1.handoffStatusMode ? "--handoff-status" : "",
|
|
179
|
+
args_1.handoffClearMode ? "--handoff-clear" : "",
|
|
180
|
+
args_1.handoffPromoteInboxMode ? "--handoff-promote-inbox" : "",
|
|
181
|
+
args_1.handoffInjectionEnableMode ? "--handoff-injection-enable" : "",
|
|
182
|
+
args_1.handoffInjectionDisableMode ? "--handoff-injection-disable" : "",
|
|
183
|
+
args_1.handoffInjectionStatusMode ? "--handoff-injection-status" : "",
|
|
184
|
+
].filter(Boolean);
|
|
185
|
+
if (activeHandoffModes.length > 1) {
|
|
186
|
+
console.error(`Use one session handoff mode at a time: ${activeHandoffModes.join(", ")}.`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
if (args_1.handoffInputMode && !args_1.handoffSaveMode) {
|
|
190
|
+
console.error("--goal, --state, --blocked, --next, --decision, --open-question, --last-success-command, --last-failure-command, and --verification are only supported with --handoff-save.");
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
164
193
|
if (args_1.command === "install" || args_1.command === "install-skill") {
|
|
165
194
|
(0, install_skill_1.runInstallSkillMode)();
|
|
166
195
|
process.exit(0);
|
|
@@ -176,6 +205,33 @@ else {
|
|
|
176
205
|
runInitCommand();
|
|
177
206
|
}
|
|
178
207
|
function runInitCommand() {
|
|
208
|
+
const activeHandoffMode = activeHandoffModes[0];
|
|
209
|
+
if (activeHandoffMode) {
|
|
210
|
+
try {
|
|
211
|
+
if (args_1.handoffSaveMode)
|
|
212
|
+
(0, session_handoff_1.runHandoffSaveMode)();
|
|
213
|
+
else if (args_1.handoffShowMode)
|
|
214
|
+
(0, session_handoff_1.runHandoffShowMode)();
|
|
215
|
+
else if (args_1.handoffStatusMode)
|
|
216
|
+
(0, session_handoff_1.runHandoffStatusMode)();
|
|
217
|
+
else if (args_1.handoffClearMode)
|
|
218
|
+
(0, session_handoff_1.runHandoffClearMode)();
|
|
219
|
+
else if (args_1.handoffPromoteInboxMode)
|
|
220
|
+
(0, session_handoff_1.runHandoffPromoteInboxMode)();
|
|
221
|
+
else if (args_1.handoffInjectionEnableMode)
|
|
222
|
+
(0, session_handoff_1.runHandoffInjectionEnableMode)();
|
|
223
|
+
else if (args_1.handoffInjectionDisableMode)
|
|
224
|
+
(0, session_handoff_1.runHandoffInjectionDisableMode)();
|
|
225
|
+
else if (args_1.handoffInjectionStatusMode)
|
|
226
|
+
(0, session_handoff_1.runHandoffInjectionStatusMode)();
|
|
227
|
+
exitAfterStdoutDrain(0);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
179
235
|
const activeCodeModes = activeCodeEvidenceCliModes();
|
|
180
236
|
if (activeCodeModes.length > 1) {
|
|
181
237
|
console.error("Use one code evidence mode at a time: --code-index, --code-index-health, --code-query, --code-report, --code-status, --code-files, --code-impact, --code-context-pack, or --code-search-symbol.");
|
package/dist/mcp-server.js
CHANGED
|
@@ -255,6 +255,29 @@ const PROMPTS = [
|
|
|
255
255
|
].join("\n");
|
|
256
256
|
},
|
|
257
257
|
},
|
|
258
|
+
{
|
|
259
|
+
name: "maintenance_improvement_review",
|
|
260
|
+
title: "Maintenance Improvement Review",
|
|
261
|
+
description: "Turn broad maintainability, automation, or efficiency requests into an analyze-first backlog and execution plan.",
|
|
262
|
+
arguments: [
|
|
263
|
+
{ name: "request", description: "The user's broad improvement or automation request.", required: true },
|
|
264
|
+
{ name: "scope", description: "Optional repository area, package, workflow, or concern to prioritize." },
|
|
265
|
+
],
|
|
266
|
+
get: (args) => {
|
|
267
|
+
const request = requiredPromptArg(args, "request");
|
|
268
|
+
const scope = optionalPromptArg(args, "scope", "whole repository unless the user named a narrower scope");
|
|
269
|
+
return [
|
|
270
|
+
"Handle this as analyze-first maintenance improvement work, not as a routine bootstrap/update command.",
|
|
271
|
+
"1. Read project-librarian://wiki/startup, project-librarian://wiki/index, and project-librarian://code/status before deciding the work path.",
|
|
272
|
+
"2. Inspect repo, wiki, package scripts, CI, tests, release gates, generated surfaces, dependencies, and code-structure evidence relevant to the request.",
|
|
273
|
+
"3. Produce a ranked backlog with concrete file/command evidence, confidence, risk, and a verification path for each candidate.",
|
|
274
|
+
"4. Persist a durable plan under wiki/plans/ when project-planning state changes, then refresh the wiki index and run lint when practical.",
|
|
275
|
+
"5. If execution is requested, implement safe high-priority items first, update generated outputs, and verify with targeted tests plus the smallest broad gate that proves the claim.",
|
|
276
|
+
`Request: ${request}`,
|
|
277
|
+
`Scope: ${scope}`,
|
|
278
|
+
].join("\n");
|
|
279
|
+
},
|
|
280
|
+
},
|
|
258
281
|
{
|
|
259
282
|
name: "retrieval_quality_review",
|
|
260
283
|
title: "Retrieval Quality Review",
|
package/dist/modes.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.buildRefreshIndexBlock = buildRefreshIndexBlock;
|
|
|
37
37
|
exports.runQueryMode = runQueryMode;
|
|
38
38
|
exports.runWikiImpactMode = runWikiImpactMode;
|
|
39
39
|
exports.projectCandidatesContent = projectCandidatesContent;
|
|
40
|
+
exports.appendProjectCandidate = appendProjectCandidate;
|
|
40
41
|
exports.appendCaptureInbox = appendCaptureInbox;
|
|
41
42
|
exports.runIssueDraftMode = runIssueDraftMode;
|
|
42
43
|
exports.runIssueCreateMode = runIssueCreateMode;
|
|
@@ -338,23 +339,29 @@ function projectCandidatesContent() {
|
|
|
338
339
|
| --- | --- | --- | --- | --- |
|
|
339
340
|
`;
|
|
340
341
|
}
|
|
341
|
-
function
|
|
342
|
+
function appendProjectCandidate(input = {}) {
|
|
342
343
|
(0, workspace_1.mkdirp)("wiki/inbox");
|
|
343
344
|
const relativePath = "wiki/inbox/project-candidates.md";
|
|
344
345
|
const existed = (0, workspace_1.exists)(relativePath);
|
|
345
346
|
if (!existed)
|
|
346
347
|
(0, workspace_1.write)(relativePath, projectCandidatesContent());
|
|
347
|
-
|
|
348
|
+
const candidateTitle = input.title ?? args_1.captureTitle;
|
|
349
|
+
const candidateContent = input.content ?? args_1.captureContent;
|
|
350
|
+
const candidateCategory = input.category ?? args_1.captureCategory;
|
|
351
|
+
if (!candidateTitle && !candidateContent)
|
|
348
352
|
return existed ? "exists" : "created";
|
|
349
|
-
const title = (
|
|
350
|
-
const content = (
|
|
351
|
-
const row = `| ${workspace_1.today} | ${title} | ${
|
|
353
|
+
const title = (candidateTitle || "Untitled candidate").replace(/\|/g, "/");
|
|
354
|
+
const content = (candidateContent || "").replace(/\r?\n/g, "<br>").replace(/\|/g, "/");
|
|
355
|
+
const row = `| ${workspace_1.today} | ${title} | ${candidateCategory.replace(/\|/g, "/")} | ${content} | pending |`;
|
|
352
356
|
const current = (0, workspace_1.read)(relativePath);
|
|
353
357
|
if (current.includes(row))
|
|
354
358
|
return "exists";
|
|
355
359
|
(0, workspace_1.write)(relativePath, `${current.trimEnd()}\n${row}\n`);
|
|
356
360
|
return "updated";
|
|
357
361
|
}
|
|
362
|
+
function appendCaptureInbox() {
|
|
363
|
+
return appendProjectCandidate();
|
|
364
|
+
}
|
|
358
365
|
function gitOutput(args) {
|
|
359
366
|
try {
|
|
360
367
|
return childProcess.execFileSync("git", args, {
|
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.handoffSentinel = exports.handoffGeneratedBy = exports.handoffSchema = exports.handoffInjectionStatePath = exports.handoffStatePath = exports.handoffPath = exports.handoffDirectory = void 0;
|
|
37
|
+
exports.redactSecrets = redactSecrets;
|
|
38
|
+
exports.saveHandoffFromCli = saveHandoffFromCli;
|
|
39
|
+
exports.getHandoffStatus = getHandoffStatus;
|
|
40
|
+
exports.showHandoff = showHandoff;
|
|
41
|
+
exports.clearHandoff = clearHandoff;
|
|
42
|
+
exports.promoteHandoffToInbox = promoteHandoffToInbox;
|
|
43
|
+
exports.enableHandoffInjection = enableHandoffInjection;
|
|
44
|
+
exports.disableHandoffInjection = disableHandoffInjection;
|
|
45
|
+
exports.getHandoffInjectionStatus = getHandoffInjectionStatus;
|
|
46
|
+
exports.runHandoffSaveMode = runHandoffSaveMode;
|
|
47
|
+
exports.runHandoffShowMode = runHandoffShowMode;
|
|
48
|
+
exports.runHandoffStatusMode = runHandoffStatusMode;
|
|
49
|
+
exports.runHandoffClearMode = runHandoffClearMode;
|
|
50
|
+
exports.runHandoffPromoteInboxMode = runHandoffPromoteInboxMode;
|
|
51
|
+
exports.runHandoffInjectionEnableMode = runHandoffInjectionEnableMode;
|
|
52
|
+
exports.runHandoffInjectionDisableMode = runHandoffInjectionDisableMode;
|
|
53
|
+
exports.runHandoffInjectionStatusMode = runHandoffInjectionStatusMode;
|
|
54
|
+
const childProcess = __importStar(require("node:child_process"));
|
|
55
|
+
const crypto = __importStar(require("node:crypto"));
|
|
56
|
+
const fs = __importStar(require("node:fs"));
|
|
57
|
+
const path = __importStar(require("node:path"));
|
|
58
|
+
const args_1 = require("./args");
|
|
59
|
+
const modes_1 = require("./modes");
|
|
60
|
+
const templates_1 = require("./templates");
|
|
61
|
+
const workspace_1 = require("./workspace");
|
|
62
|
+
const workspace_2 = require("./workspace");
|
|
63
|
+
exports.handoffDirectory = ".project-wiki/session";
|
|
64
|
+
exports.handoffPath = `${exports.handoffDirectory}/last-handoff.md`;
|
|
65
|
+
exports.handoffStatePath = `${exports.handoffDirectory}/handoff-state.json`;
|
|
66
|
+
exports.handoffInjectionStatePath = `${exports.handoffDirectory}/injection-state.json`;
|
|
67
|
+
exports.handoffSchema = "project-librarian-session-handoff/v1";
|
|
68
|
+
exports.handoffGeneratedBy = "project-librarian-session-handoff";
|
|
69
|
+
exports.handoffSentinel = "<!-- PROJECT-LIBRARIAN-GENERATED: session-handoff/v1 -->";
|
|
70
|
+
const maxHandoffChars = 8000;
|
|
71
|
+
const maxInjectedHandoffChars = 2500;
|
|
72
|
+
const maxFieldChars = 800;
|
|
73
|
+
const maxGitFactsChars = 2000;
|
|
74
|
+
const staleAfterMs = 7 * 24 * 60 * 60 * 1000;
|
|
75
|
+
function rootRealPath() {
|
|
76
|
+
return fs.realpathSync(workspace_1.root);
|
|
77
|
+
}
|
|
78
|
+
function assertInsideRoot(absolutePath) {
|
|
79
|
+
const relative = path.relative(rootRealPath(), absolutePath);
|
|
80
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
81
|
+
throw new Error(`refusing to access path outside project root: ${absolutePath}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function resolveUnderRoot(relativePath) {
|
|
85
|
+
if (path.isAbsolute(relativePath))
|
|
86
|
+
throw new Error(`expected repository-relative path: ${relativePath}`);
|
|
87
|
+
const absolutePath = path.resolve(workspace_1.root, relativePath);
|
|
88
|
+
assertInsideRoot(absolutePath);
|
|
89
|
+
return absolutePath;
|
|
90
|
+
}
|
|
91
|
+
function ensureDirectoryNoSymlink(relativePath) {
|
|
92
|
+
const segments = relativePath.split(/[\\/]+/).filter(Boolean);
|
|
93
|
+
let current = rootRealPath();
|
|
94
|
+
for (const segment of segments) {
|
|
95
|
+
current = path.join(current, segment);
|
|
96
|
+
try {
|
|
97
|
+
const stat = fs.lstatSync(current);
|
|
98
|
+
if (stat.isSymbolicLink())
|
|
99
|
+
throw new Error(`refusing to use symlinked directory: ${path.relative(rootRealPath(), current)}`);
|
|
100
|
+
if (!stat.isDirectory())
|
|
101
|
+
throw new Error(`refusing to use non-directory path: ${path.relative(rootRealPath(), current)}`);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error.code !== "ENOENT")
|
|
105
|
+
throw error;
|
|
106
|
+
fs.mkdirSync(current);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
assertInsideRoot(fs.realpathSync(current));
|
|
110
|
+
}
|
|
111
|
+
function assertWritableFilePath(relativePath) {
|
|
112
|
+
const absolutePath = resolveUnderRoot(relativePath);
|
|
113
|
+
ensureDirectoryNoSymlink(path.dirname(relativePath));
|
|
114
|
+
try {
|
|
115
|
+
const stat = fs.lstatSync(absolutePath);
|
|
116
|
+
if (stat.isSymbolicLink())
|
|
117
|
+
throw new Error(`refusing to write symlinked file: ${relativePath}`);
|
|
118
|
+
if (!stat.isFile())
|
|
119
|
+
throw new Error(`refusing to overwrite non-file path: ${relativePath}`);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== "ENOENT")
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
return absolutePath;
|
|
126
|
+
}
|
|
127
|
+
function writeFileNoFollow(relativePath, content) {
|
|
128
|
+
const absolutePath = assertWritableFilePath(relativePath);
|
|
129
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
130
|
+
const fd = fs.openSync(absolutePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | noFollow, 0o600);
|
|
131
|
+
try {
|
|
132
|
+
fs.writeFileSync(fd, content, "utf8");
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
fs.closeSync(fd);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function readFileNoSymlink(relativePath) {
|
|
139
|
+
const absolutePath = resolveUnderRoot(relativePath);
|
|
140
|
+
const stat = fs.lstatSync(absolutePath);
|
|
141
|
+
if (stat.isSymbolicLink())
|
|
142
|
+
throw new Error(`refusing to read symlinked file: ${relativePath}`);
|
|
143
|
+
if (!stat.isFile())
|
|
144
|
+
throw new Error(`expected file: ${relativePath}`);
|
|
145
|
+
return fs.readFileSync(absolutePath, "utf8");
|
|
146
|
+
}
|
|
147
|
+
function removeGeneratedFile(relativePath, isGenerated) {
|
|
148
|
+
const absolutePath = resolveUnderRoot(relativePath);
|
|
149
|
+
if (!fs.existsSync(absolutePath))
|
|
150
|
+
return "absent";
|
|
151
|
+
const text = readFileNoSymlink(relativePath);
|
|
152
|
+
if (!isGenerated(text))
|
|
153
|
+
throw new Error(`refusing to remove non-generated file: ${relativePath}`);
|
|
154
|
+
fs.unlinkSync(absolutePath);
|
|
155
|
+
return "removed";
|
|
156
|
+
}
|
|
157
|
+
function capText(value, maxChars = maxFieldChars) {
|
|
158
|
+
const compact = value.replace(/\r\n/g, "\n").trim();
|
|
159
|
+
if (compact.length <= maxChars)
|
|
160
|
+
return compact;
|
|
161
|
+
return `${compact.slice(0, Math.max(0, maxChars - 24)).trimEnd()}\n[truncated]`;
|
|
162
|
+
}
|
|
163
|
+
function redactSecrets(text) {
|
|
164
|
+
let next = text;
|
|
165
|
+
next = next.replace(/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]");
|
|
166
|
+
next = next.replace(/\bsk-[A-Za-z0-9_-]{16,}\b/g, "[REDACTED_OPENAI_KEY]");
|
|
167
|
+
next = next.replace(/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, "[REDACTED_GITHUB_TOKEN]");
|
|
168
|
+
next = next.replace(/\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g, "[REDACTED_SLACK_TOKEN]");
|
|
169
|
+
next = next.replace(/\bAKIA[0-9A-Z]{16}\b/g, "[REDACTED_AWS_ACCESS_KEY]");
|
|
170
|
+
next = next.replace(/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g, "[REDACTED_JWT]");
|
|
171
|
+
next = next.replace(/(Authorization\s*[:=]\s*)(?:Bearer\s+)?[A-Za-z0-9._~+/=-]{12,}/gi, "$1[REDACTED_AUTHORIZATION]");
|
|
172
|
+
next = next.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]");
|
|
173
|
+
return next;
|
|
174
|
+
}
|
|
175
|
+
function normalizeList(value) {
|
|
176
|
+
if (Array.isArray(value))
|
|
177
|
+
return value.flatMap((item) => normalizeList(item));
|
|
178
|
+
if (typeof value !== "string")
|
|
179
|
+
return [];
|
|
180
|
+
return value.split(/\n/).map((item) => capText(redactSecrets(item))).filter(Boolean);
|
|
181
|
+
}
|
|
182
|
+
function normalizeString(value) {
|
|
183
|
+
return typeof value === "string" ? capText(redactSecrets(value)) : "";
|
|
184
|
+
}
|
|
185
|
+
function arrayValue(record, snakeKey, camelKey) {
|
|
186
|
+
if (snakeKey === camelKey)
|
|
187
|
+
return normalizeList(record[snakeKey]);
|
|
188
|
+
return normalizeList(record[snakeKey]).concat(normalizeList(record[camelKey]));
|
|
189
|
+
}
|
|
190
|
+
function stringValue(record, snakeKey, camelKey) {
|
|
191
|
+
return normalizeString(record[snakeKey] ?? record[camelKey]);
|
|
192
|
+
}
|
|
193
|
+
function payloadFromRecord(record) {
|
|
194
|
+
return {
|
|
195
|
+
blocked: arrayValue(record, "blocked", "blocked").slice(0, 6),
|
|
196
|
+
currentState: stringValue(record, "current_state", "currentState"),
|
|
197
|
+
goal: stringValue(record, "goal", "goal"),
|
|
198
|
+
lastFailureCommand: stringValue(record, "last_failure_command", "lastFailureCommand"),
|
|
199
|
+
lastSuccessCommand: stringValue(record, "last_success_command", "lastSuccessCommand"),
|
|
200
|
+
nextActions: arrayValue(record, "next_actions", "nextActions").slice(0, 3),
|
|
201
|
+
openQuestions: arrayValue(record, "open_questions", "openQuestions").slice(0, 6),
|
|
202
|
+
recentDecisions: arrayValue(record, "recent_decisions", "recentDecisions").slice(0, 6),
|
|
203
|
+
verification: arrayValue(record, "verification", "verification").slice(0, 6),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function cliPayloadRecord() {
|
|
207
|
+
return {
|
|
208
|
+
blocked: args_1.handoffBlocked,
|
|
209
|
+
current_state: args_1.handoffState,
|
|
210
|
+
goal: args_1.handoffGoal,
|
|
211
|
+
last_failure_command: args_1.handoffLastFailureCommand,
|
|
212
|
+
last_success_command: args_1.handoffLastSuccessCommand,
|
|
213
|
+
next_actions: args_1.handoffNextActions,
|
|
214
|
+
open_questions: args_1.handoffOpenQuestions,
|
|
215
|
+
recent_decisions: args_1.handoffDecisions,
|
|
216
|
+
verification: args_1.handoffVerification,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function mergePayloads(base, overlay) {
|
|
220
|
+
return {
|
|
221
|
+
blocked: overlay.blocked.length > 0 ? overlay.blocked : base.blocked,
|
|
222
|
+
currentState: overlay.currentState || base.currentState,
|
|
223
|
+
goal: overlay.goal || base.goal,
|
|
224
|
+
lastFailureCommand: overlay.lastFailureCommand || base.lastFailureCommand,
|
|
225
|
+
lastSuccessCommand: overlay.lastSuccessCommand || base.lastSuccessCommand,
|
|
226
|
+
nextActions: overlay.nextActions.length > 0 ? overlay.nextActions : base.nextActions,
|
|
227
|
+
openQuestions: overlay.openQuestions.length > 0 ? overlay.openQuestions : base.openQuestions,
|
|
228
|
+
recentDecisions: overlay.recentDecisions.length > 0 ? overlay.recentDecisions : base.recentDecisions,
|
|
229
|
+
verification: overlay.verification.length > 0 ? overlay.verification : base.verification,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function readStdinText() {
|
|
233
|
+
try {
|
|
234
|
+
if (process.stdin.isTTY)
|
|
235
|
+
return "";
|
|
236
|
+
return fs.readFileSync(0, "utf8").trim();
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return "";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function payloadFromStdin() {
|
|
243
|
+
const text = readStdinText();
|
|
244
|
+
if (!text)
|
|
245
|
+
return emptyPayload();
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(text);
|
|
248
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
249
|
+
throw new Error("expected a JSON object");
|
|
250
|
+
}
|
|
251
|
+
return payloadFromRecord(parsed);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
255
|
+
throw new Error(`invalid --handoff-save JSON payload: ${message}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function emptyPayload() {
|
|
259
|
+
return {
|
|
260
|
+
blocked: [],
|
|
261
|
+
currentState: "",
|
|
262
|
+
goal: "",
|
|
263
|
+
lastFailureCommand: "",
|
|
264
|
+
lastSuccessCommand: "",
|
|
265
|
+
nextActions: [],
|
|
266
|
+
openQuestions: [],
|
|
267
|
+
recentDecisions: [],
|
|
268
|
+
verification: [],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function runGit(args) {
|
|
272
|
+
const gitArgs = [
|
|
273
|
+
"--no-pager",
|
|
274
|
+
"-c", "core.fsmonitor=false",
|
|
275
|
+
"-c", "core.hooksPath=/dev/null",
|
|
276
|
+
"-c", "diff.external=",
|
|
277
|
+
...args,
|
|
278
|
+
];
|
|
279
|
+
try {
|
|
280
|
+
return childProcess.execFileSync("git", gitArgs, {
|
|
281
|
+
cwd: workspace_1.root,
|
|
282
|
+
encoding: "utf8",
|
|
283
|
+
env: {
|
|
284
|
+
...process.env,
|
|
285
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
286
|
+
GIT_PAGER: "cat",
|
|
287
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
288
|
+
},
|
|
289
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
290
|
+
timeout: 1500,
|
|
291
|
+
}).trim();
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function collectGitFacts() {
|
|
298
|
+
const inside = runGit(["rev-parse", "--is-inside-work-tree"]);
|
|
299
|
+
if (inside !== "true")
|
|
300
|
+
return "not a git repository";
|
|
301
|
+
const branch = runGit(["rev-parse", "--abbrev-ref", "HEAD"]) || "unknown";
|
|
302
|
+
const status = runGit(["status", "--short"]) || "clean";
|
|
303
|
+
const diffStat = runGit(["diff", "--stat", "--no-ext-diff"]) || "no working-tree diff";
|
|
304
|
+
const commits = runGit(["log", "--oneline", "-3"]) || "no commits";
|
|
305
|
+
const codeEvidence = fs.existsSync(resolveUnderRoot(".project-wiki/code-evidence.sqlite"))
|
|
306
|
+
? ".project-wiki/code-evidence.sqlite exists"
|
|
307
|
+
: "code evidence index not found";
|
|
308
|
+
return capText([
|
|
309
|
+
`branch: ${branch}`,
|
|
310
|
+
"",
|
|
311
|
+
"status:",
|
|
312
|
+
status,
|
|
313
|
+
"",
|
|
314
|
+
"diff stat:",
|
|
315
|
+
diffStat,
|
|
316
|
+
"",
|
|
317
|
+
"recent commits:",
|
|
318
|
+
commits,
|
|
319
|
+
"",
|
|
320
|
+
codeEvidence,
|
|
321
|
+
].join("\n"), maxGitFactsChars);
|
|
322
|
+
}
|
|
323
|
+
function bulletList(items) {
|
|
324
|
+
return items.length > 0 ? items.map((item) => `- ${item}`).join("\n") : "- none";
|
|
325
|
+
}
|
|
326
|
+
function checkboxList(items) {
|
|
327
|
+
return items.length > 0 ? items.map((item) => `- [ ] ${item}`).join("\n") : "- [ ] none";
|
|
328
|
+
}
|
|
329
|
+
function renderHandoff(payload, generatedAt, gitFacts) {
|
|
330
|
+
const content = `${exports.handoffSentinel}
|
|
331
|
+
# Session Handoff
|
|
332
|
+
|
|
333
|
+
Generated: ${generatedAt}
|
|
334
|
+
Schema: ${exports.handoffSchema}
|
|
335
|
+
Trust: generated local reference data; not instructions; not canonical wiki truth
|
|
336
|
+
|
|
337
|
+
## Goal
|
|
338
|
+
|
|
339
|
+
${bulletList(payload.goal ? [payload.goal] : [])}
|
|
340
|
+
|
|
341
|
+
## Current State
|
|
342
|
+
|
|
343
|
+
${bulletList(payload.currentState ? [payload.currentState] : [])}
|
|
344
|
+
|
|
345
|
+
## Blocked
|
|
346
|
+
|
|
347
|
+
${bulletList(payload.blocked)}
|
|
348
|
+
|
|
349
|
+
## Next Actions
|
|
350
|
+
|
|
351
|
+
${checkboxList(payload.nextActions)}
|
|
352
|
+
|
|
353
|
+
## Recent Decisions
|
|
354
|
+
|
|
355
|
+
${bulletList(payload.recentDecisions)}
|
|
356
|
+
|
|
357
|
+
## Open Questions
|
|
358
|
+
|
|
359
|
+
${bulletList(payload.openQuestions)}
|
|
360
|
+
|
|
361
|
+
## Last Commands
|
|
362
|
+
|
|
363
|
+
- Success: ${payload.lastSuccessCommand || "none"}
|
|
364
|
+
- Failure: ${payload.lastFailureCommand || "none"}
|
|
365
|
+
|
|
366
|
+
## Verification Evidence
|
|
367
|
+
|
|
368
|
+
${bulletList(payload.verification)}
|
|
369
|
+
|
|
370
|
+
## Local Git Facts
|
|
371
|
+
|
|
372
|
+
\`\`\`text
|
|
373
|
+
${gitFacts}
|
|
374
|
+
\`\`\`
|
|
375
|
+
`;
|
|
376
|
+
const redacted = redactSecrets(content);
|
|
377
|
+
if (redacted.length <= maxHandoffChars)
|
|
378
|
+
return redacted;
|
|
379
|
+
return `${redacted.slice(0, maxHandoffChars - 64).trimEnd()}\n\n[truncated: session handoff cap reached]\n`;
|
|
380
|
+
}
|
|
381
|
+
function readState() {
|
|
382
|
+
try {
|
|
383
|
+
return JSON.parse(readFileNoSymlink(exports.handoffStatePath));
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return {};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function readInjectionState() {
|
|
390
|
+
try {
|
|
391
|
+
return JSON.parse(readFileNoSymlink(exports.handoffInjectionStatePath));
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return {};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function contentHash(payload, gitFacts) {
|
|
398
|
+
return crypto.createHash("sha256").update(JSON.stringify({ payload, gitFacts })).digest("hex");
|
|
399
|
+
}
|
|
400
|
+
function saveHandoffFromCli(now = new Date()) {
|
|
401
|
+
const stdinPayload = payloadFromStdin();
|
|
402
|
+
const flagPayload = payloadFromRecord(cliPayloadRecord());
|
|
403
|
+
const payload = mergePayloads(stdinPayload, flagPayload);
|
|
404
|
+
const gitFacts = collectGitFacts();
|
|
405
|
+
const hash = contentHash(payload, gitFacts);
|
|
406
|
+
const existingState = readState();
|
|
407
|
+
if (existingState.content_hash === hash && fs.existsSync(resolveUnderRoot(exports.handoffPath))) {
|
|
408
|
+
const stat = fs.lstatSync(resolveUnderRoot(exports.handoffPath));
|
|
409
|
+
if (stat.isSymbolicLink())
|
|
410
|
+
throw new Error(`refusing to reuse symlinked file: ${exports.handoffPath}`);
|
|
411
|
+
if (!stat.isFile())
|
|
412
|
+
throw new Error(`refusing to reuse non-file path: ${exports.handoffPath}`);
|
|
413
|
+
return {
|
|
414
|
+
status: "exists",
|
|
415
|
+
handoffPath: exports.handoffPath,
|
|
416
|
+
statePath: exports.handoffStatePath,
|
|
417
|
+
sizeBytes: existingState.size_bytes ?? stat.size,
|
|
418
|
+
updatedAt: existingState.updated_at ?? "",
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const updatedAt = now.toISOString();
|
|
422
|
+
const content = renderHandoff(payload, updatedAt, gitFacts);
|
|
423
|
+
writeFileNoFollow(exports.handoffPath, content);
|
|
424
|
+
const sizeBytes = Buffer.byteLength(content, "utf8");
|
|
425
|
+
const state = {
|
|
426
|
+
content_hash: hash,
|
|
427
|
+
full_injection_enabled: false,
|
|
428
|
+
generated: true,
|
|
429
|
+
generated_by: exports.handoffGeneratedBy,
|
|
430
|
+
path: exports.handoffPath,
|
|
431
|
+
schema: exports.handoffSchema,
|
|
432
|
+
size_bytes: sizeBytes,
|
|
433
|
+
updated_at: updatedAt,
|
|
434
|
+
};
|
|
435
|
+
writeFileNoFollow(exports.handoffStatePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
436
|
+
return { status: "written", handoffPath: exports.handoffPath, statePath: exports.handoffStatePath, sizeBytes, updatedAt };
|
|
437
|
+
}
|
|
438
|
+
function getHandoffStatus(now = new Date()) {
|
|
439
|
+
const absoluteHandoffPath = resolveUnderRoot(exports.handoffPath);
|
|
440
|
+
if (!fs.existsSync(absoluteHandoffPath)) {
|
|
441
|
+
return {
|
|
442
|
+
ageSeconds: null,
|
|
443
|
+
exists: false,
|
|
444
|
+
path: exports.handoffPath,
|
|
445
|
+
reason: "handoff file not found",
|
|
446
|
+
safeToInject: false,
|
|
447
|
+
schema: exports.handoffSchema,
|
|
448
|
+
sizeBytes: 0,
|
|
449
|
+
stale: false,
|
|
450
|
+
statePath: exports.handoffStatePath,
|
|
451
|
+
updatedAt: "",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const stat = fs.lstatSync(absoluteHandoffPath);
|
|
455
|
+
if (stat.isSymbolicLink()) {
|
|
456
|
+
return {
|
|
457
|
+
ageSeconds: null,
|
|
458
|
+
exists: true,
|
|
459
|
+
path: exports.handoffPath,
|
|
460
|
+
reason: "handoff path is a symlink",
|
|
461
|
+
safeToInject: false,
|
|
462
|
+
schema: exports.handoffSchema,
|
|
463
|
+
sizeBytes: 0,
|
|
464
|
+
stale: true,
|
|
465
|
+
statePath: exports.handoffStatePath,
|
|
466
|
+
updatedAt: "",
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const state = readState();
|
|
470
|
+
const updatedAt = state.updated_at || stat.mtime.toISOString();
|
|
471
|
+
const updatedMs = Date.parse(updatedAt);
|
|
472
|
+
const ageSeconds = Number.isFinite(updatedMs) ? Math.max(0, Math.floor((now.getTime() - updatedMs) / 1000)) : null;
|
|
473
|
+
const stale = ageSeconds === null || ageSeconds * 1000 > staleAfterMs;
|
|
474
|
+
const sizeBytes = stat.size;
|
|
475
|
+
const generated = state.generated === true && state.generated_by === exports.handoffGeneratedBy;
|
|
476
|
+
const safeToInject = generated && !stale && sizeBytes <= maxHandoffChars;
|
|
477
|
+
return {
|
|
478
|
+
ageSeconds,
|
|
479
|
+
exists: true,
|
|
480
|
+
path: exports.handoffPath,
|
|
481
|
+
reason: safeToInject ? "ok" : generated ? "stale or over size cap" : "missing generated state",
|
|
482
|
+
safeToInject,
|
|
483
|
+
schema: state.schema || exports.handoffSchema,
|
|
484
|
+
sizeBytes,
|
|
485
|
+
stale,
|
|
486
|
+
statePath: exports.handoffStatePath,
|
|
487
|
+
updatedAt,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function showHandoff() {
|
|
491
|
+
const status = getHandoffStatus();
|
|
492
|
+
if (!status.exists)
|
|
493
|
+
return "Project Librarian handoff: none found.\n";
|
|
494
|
+
const text = readFileNoSymlink(exports.handoffPath);
|
|
495
|
+
return [
|
|
496
|
+
`Project Librarian handoff: updated ${status.updatedAt || "unknown"}, ${status.sizeBytes} bytes, stale=${String(status.stale)}`,
|
|
497
|
+
"",
|
|
498
|
+
text,
|
|
499
|
+
].join("\n");
|
|
500
|
+
}
|
|
501
|
+
function clearHandoff() {
|
|
502
|
+
const handoffResult = removeGeneratedFile(exports.handoffPath, (text) => text.includes(exports.handoffSentinel));
|
|
503
|
+
const stateResult = removeGeneratedFile(exports.handoffStatePath, (text) => {
|
|
504
|
+
try {
|
|
505
|
+
const state = JSON.parse(text);
|
|
506
|
+
return state.generated === true && state.generated_by === exports.handoffGeneratedBy;
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
return `Project Librarian handoff cleared: ${exports.handoffPath}=${handoffResult}, ${exports.handoffStatePath}=${stateResult}`;
|
|
513
|
+
}
|
|
514
|
+
function sectionText(markdown, heading) {
|
|
515
|
+
const pattern = new RegExp(`^## ${heading}\\n\\n([\\s\\S]*?)(?=\\n## |\\n$)`, "m");
|
|
516
|
+
return markdown.match(pattern)?.[1]?.trim() ?? "";
|
|
517
|
+
}
|
|
518
|
+
function compactPromotionContent(markdown) {
|
|
519
|
+
const sections = [
|
|
520
|
+
["Goal", sectionText(markdown, "Goal")],
|
|
521
|
+
["Current State", sectionText(markdown, "Current State")],
|
|
522
|
+
["Blocked", sectionText(markdown, "Blocked")],
|
|
523
|
+
["Next Actions", sectionText(markdown, "Next Actions")],
|
|
524
|
+
["Recent Decisions", sectionText(markdown, "Recent Decisions")],
|
|
525
|
+
["Open Questions", sectionText(markdown, "Open Questions")],
|
|
526
|
+
["Verification Evidence", sectionText(markdown, "Verification Evidence")],
|
|
527
|
+
].filter(([, text]) => text && text !== "- none" && text !== "- [ ] none");
|
|
528
|
+
const body = sections.map(([title, text]) => `### ${title}\n${text}`).join("\n\n");
|
|
529
|
+
return capText([
|
|
530
|
+
"Promoted from generated local session handoff. Review before canonicalizing into project truth.",
|
|
531
|
+
"",
|
|
532
|
+
body || "No structured handoff facts were available.",
|
|
533
|
+
].join("\n"), 1600);
|
|
534
|
+
}
|
|
535
|
+
function promoteHandoffToInbox() {
|
|
536
|
+
const status = getHandoffStatus();
|
|
537
|
+
if (!status.exists)
|
|
538
|
+
throw new Error("cannot promote handoff: no generated handoff found");
|
|
539
|
+
if (!status.safeToInject)
|
|
540
|
+
throw new Error(`cannot promote handoff: ${status.reason}`);
|
|
541
|
+
if (!(0, workspace_2.exists)("wiki/index.md"))
|
|
542
|
+
throw new Error("cannot promote handoff: initialize Project Librarian wiki before writing wiki/inbox");
|
|
543
|
+
const markdown = readFileNoSymlink(exports.handoffPath);
|
|
544
|
+
if (!markdown.includes(exports.handoffSentinel))
|
|
545
|
+
throw new Error(`cannot promote handoff: missing generated sentinel in ${exports.handoffPath}`);
|
|
546
|
+
const firstGoal = sectionText(markdown, "Goal").split(/\r?\n/).find((line) => line.replace(/^[-\s[\]x]+/i, "").trim()) || "Session handoff";
|
|
547
|
+
const title = `Session handoff: ${firstGoal.replace(/^[-\s[\]x]+/i, "").trim() || "untitled"}`;
|
|
548
|
+
const inboxStatus = (0, modes_1.appendProjectCandidate)({
|
|
549
|
+
category: "session-handoff",
|
|
550
|
+
content: compactPromotionContent(markdown),
|
|
551
|
+
title,
|
|
552
|
+
});
|
|
553
|
+
const indexStatus = (0, workspace_2.upsertMarkedSection)("wiki/index.md", "<!-- PROJECT-WIKI-INBOX:START -->", "<!-- PROJECT-WIKI-INBOX:END -->", templates_1.inboxIndexBlock);
|
|
554
|
+
return `Project Librarian handoff promoted to wiki inbox: wiki/inbox/project-candidates.md=${inboxStatus}, wiki/index.md inbox router=${indexStatus}`;
|
|
555
|
+
}
|
|
556
|
+
function enableHandoffInjection(now = new Date()) {
|
|
557
|
+
const updatedAt = now.toISOString();
|
|
558
|
+
const state = {
|
|
559
|
+
content_hash: "",
|
|
560
|
+
full_injection_enabled: true,
|
|
561
|
+
generated: true,
|
|
562
|
+
generated_by: exports.handoffGeneratedBy,
|
|
563
|
+
path: exports.handoffInjectionStatePath,
|
|
564
|
+
schema: exports.handoffSchema,
|
|
565
|
+
size_bytes: 0,
|
|
566
|
+
updated_at: updatedAt,
|
|
567
|
+
};
|
|
568
|
+
const content = `${JSON.stringify(state, null, 2)}\n`;
|
|
569
|
+
state.size_bytes = Buffer.byteLength(content, "utf8");
|
|
570
|
+
writeFileNoFollow(exports.handoffInjectionStatePath, `${JSON.stringify(state, null, 2)}\n`);
|
|
571
|
+
return `Project Librarian handoff full injection enabled: ${exports.handoffInjectionStatePath}`;
|
|
572
|
+
}
|
|
573
|
+
function disableHandoffInjection() {
|
|
574
|
+
const result = removeGeneratedFile(exports.handoffInjectionStatePath, (text) => {
|
|
575
|
+
try {
|
|
576
|
+
const state = JSON.parse(text);
|
|
577
|
+
return state.generated === true && state.generated_by === exports.handoffGeneratedBy;
|
|
578
|
+
}
|
|
579
|
+
catch {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
return `Project Librarian handoff full injection disabled: ${exports.handoffInjectionStatePath}=${result}`;
|
|
584
|
+
}
|
|
585
|
+
function getHandoffInjectionStatus() {
|
|
586
|
+
const state = readInjectionState();
|
|
587
|
+
const enabled = state.generated === true && state.generated_by === exports.handoffGeneratedBy && state.full_injection_enabled === true;
|
|
588
|
+
const handoff = getHandoffStatus();
|
|
589
|
+
const safeToInject = enabled && handoff.exists && !handoff.stale && handoff.reason === "ok";
|
|
590
|
+
return {
|
|
591
|
+
enabled,
|
|
592
|
+
handoffPath: exports.handoffPath,
|
|
593
|
+
maxInjectedChars: maxInjectedHandoffChars,
|
|
594
|
+
path: exports.handoffInjectionStatePath,
|
|
595
|
+
reason: !enabled ? "full injection is not enabled" : safeToInject ? "ok" : handoff.reason,
|
|
596
|
+
safeToInject,
|
|
597
|
+
updatedAt: state.updated_at || "",
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function runHandoffSaveMode() {
|
|
601
|
+
const result = saveHandoffFromCli();
|
|
602
|
+
console.log(`Project Librarian handoff ${result.status}: ${result.handoffPath}`);
|
|
603
|
+
console.log(`State: ${result.statePath}`);
|
|
604
|
+
console.log(`Size: ${result.sizeBytes} bytes`);
|
|
605
|
+
console.log("Resume: project-librarian --handoff-show");
|
|
606
|
+
}
|
|
607
|
+
function runHandoffShowMode() {
|
|
608
|
+
process.stdout.write(showHandoff());
|
|
609
|
+
}
|
|
610
|
+
function runHandoffStatusMode() {
|
|
611
|
+
console.log(JSON.stringify(getHandoffStatus(), null, 2));
|
|
612
|
+
}
|
|
613
|
+
function runHandoffClearMode() {
|
|
614
|
+
console.log(clearHandoff());
|
|
615
|
+
}
|
|
616
|
+
function runHandoffPromoteInboxMode() {
|
|
617
|
+
console.log(promoteHandoffToInbox());
|
|
618
|
+
}
|
|
619
|
+
function runHandoffInjectionEnableMode() {
|
|
620
|
+
console.log(enableHandoffInjection());
|
|
621
|
+
}
|
|
622
|
+
function runHandoffInjectionDisableMode() {
|
|
623
|
+
console.log(disableHandoffInjection());
|
|
624
|
+
}
|
|
625
|
+
function runHandoffInjectionStatusMode() {
|
|
626
|
+
console.log(JSON.stringify(getHandoffInjectionStatus(), null, 2));
|
|
627
|
+
}
|
package/dist/templates.js
CHANGED
|
@@ -86,6 +86,7 @@ During conversation:
|
|
|
86
86
|
- Classify new project-planning content with \`wiki/meta/document-taxonomy.md\` before writing or consolidating it.
|
|
87
87
|
- Do not store non-project LLM memory, assistant preferences, collaboration reminders, or workflow instructions in project wiki canonical or decision docs.
|
|
88
88
|
- Follow \`wiki/AGENTS.md\` for detailed rules when editing files under \`wiki/\`.
|
|
89
|
+
- Treat broad maintenance/improvement automation requests that do not name a concrete command (for example "improve this project", "start improvement automation", or "개선 자동화 시작해") as analyze-first project work, not as a plain bootstrap/update. Inspect repo, wiki, CI, test, release, dependency, and code-structure evidence; produce a ranked backlog with evidence and verification paths; persist the plan in \`wiki/plans/\` when project-planning content changes; then execute safe high-priority items with tests.
|
|
89
90
|
- Let \`.githooks/prepare-commit-msg\` append wiki trailers automatically for staged wiki, hook, AGENTS, or project-librarian files.
|
|
90
91
|
- ${exports.wikiTrustContract}
|
|
91
92
|
- ${exports.codeEvidenceTrustContract}
|
|
@@ -724,6 +725,7 @@ Canonical: [[meta/operating-model]], [[meta/decision-policy]], [[meta/document-t
|
|
|
724
725
|
| ${workspace_1.today} | Commit automation writes the \`Wiki-scope\` trailer. | Reviewers should see whether a commit touched startup, canonical docs, decisions, or wiki operations. | Leave wiki impact implicit in the diff. | Trailer format becomes too noisy. | [[meta/operating-model]] |
|
|
725
726
|
| ${workspace_1.today} | Migration may mark rows \`needs-human-review\`. | Ambiguous, risky, or high-impact legacy content should not be closed automatically. | Force every migrated row into adopted/rejected/resolved. | Human review queues become too large. | [[meta/operating-model]] |
|
|
726
727
|
| ${workspace_1.today} | Capture stores candidates in \`wiki/inbox/\`. | Useful ideas are not lost, but unreviewed content does not become canonical truth. | Save all conversation content directly into canonical docs. | Inbox content is frequently abandoned. | [[meta/operating-model]] |
|
|
728
|
+
| ${workspace_1.today} | Session handoff state lives under \`.project-wiki/session/\`. | Last-session operational memory helps resume work, but it is generated local reference data rather than reviewed project truth. | Store rolling execution memory in \`wiki/startup.md\` or canonical pages. | Handoff data needs to become durable project planning truth. | [[meta/operating-model]] |
|
|
727
729
|
`,
|
|
728
730
|
"wiki/meta/document-taxonomy.md": exports.documentTaxonomy,
|
|
729
731
|
"wiki/decisions/decision-pack-template.md": `${(0, exports.metadata)("project-decision-template", "short", "wiki/meta/decision-policy.md", "decision pack format changes", "template")}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-librarian",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Create and maintain compact project context for humans and LLM coding agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"benchmark:guidance": "npm run build && node benchmarks/tools/guidance-probe-runner.js",
|
|
50
50
|
"benchmark:guidance:dry-run": "npm run build && node benchmarks/tools/guidance-probe-runner.js --dry-run --markdown",
|
|
51
51
|
"benchmark:injection-sentinel": "node benchmarks/tools/injection-sentinel.js",
|
|
52
|
+
"benchmark:handoff-resume:preview": "node benchmarks/tools/session-handoff-resume-preview.js",
|
|
52
53
|
"benchmark:agent-surface-smoke": "node benchmarks/tools/agent-surface-smoke.js",
|
|
53
54
|
"benchmark:claim-ledger": "node benchmarks/tools/benchmark-claim-ledger.js benchmarks/llm/samples/codex-measured-report.json benchmarks/reports/llm/payload-preview.json",
|
|
54
55
|
"benchmark:real-corpus:demo": "npm run build && node benchmarks/tools/real-corpus-offline-demo.js",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"benchmark:release:diagnose": "npm run benchmark:llm -- --sanitized-pack --runs 1 --warmup-runs 0 --model gpt-5.5 --out benchmarks/reports/llm/diagnostic-current.json --markdown benchmarks/reports/llm/diagnostic-current.md",
|
|
57
58
|
"benchmark:release": "npm run benchmark:llm -- --sanitized-pack --full-matrix --runs 3 --warmup-runs 1 --min-runs-for-claim 3 --require-clean --require-claimable --model gpt-5.5 --out benchmarks/reports/llm/current.json --markdown benchmarks/reports/llm/current.md",
|
|
58
59
|
"build": "tsc && chmod +x dist/init-project-wiki.js",
|
|
60
|
+
"check:dist": "node benchmarks/tools/release-readiness.js --only-dist-parity",
|
|
59
61
|
"perf:code-efficiency": "node benchmarks/tools/code-performance-efficiency.js --full",
|
|
60
62
|
"release:check": "node benchmarks/tools/release-readiness.js",
|
|
61
63
|
"typecheck": "tsc --noEmit",
|