project-knowledge 0.1.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +201 -58
  3. package/_site/_test/ai-profile-test.js +59 -1
  4. package/_site/_test/baseline-schema-test.js +4 -3
  5. package/_site/_test/claude-workbench-test.js +72 -0
  6. package/_site/_test/draft-apply-test.js +12 -6
  7. package/_site/_test/kb-v2-templates-test.js +31 -43
  8. package/_site/_test/knowledge-store-logs-supervision-test.js +143 -0
  9. package/_site/_test/package-startup-test.js +108 -0
  10. package/_site/_test/project-control-panel-task14-test.js +151 -0
  11. package/_site/_test/task15-20-integration-test.js +194 -0
  12. package/_site/_test/task15-20-ui-flow-test.js +144 -0
  13. package/_site/_test/ui-smoke-test.js +2 -2
  14. package/_site/index.html +1640 -90
  15. package/_site/lib/ai-adapter.js +3 -3
  16. package/_site/lib/ai-workspace.js +120 -0
  17. package/_site/lib/analysis-orchestrator.js +117 -32
  18. package/_site/lib/claude-cli-runner.js +862 -0
  19. package/_site/lib/context-pack-builder.js +19 -11
  20. package/_site/lib/draft-apply.js +80 -31
  21. package/_site/lib/index-builder.js +100 -0
  22. package/_site/lib/job-orchestrator.js +15 -11
  23. package/_site/lib/kb-v3.js +188 -0
  24. package/_site/lib/kb-validator.js +84 -0
  25. package/_site/lib/knowledge-store.js +141 -0
  26. package/_site/lib/llm-client.js +103 -56
  27. package/_site/lib/prompt-registry.js +102 -0
  28. package/_site/lib/structured-logger.js +120 -0
  29. package/_site/lib/supervision.js +103 -0
  30. package/_site/server.js +887 -30
  31. package/_site/vendor/tailwind-browser.js +947 -0
  32. package/_site/vendor/vue.global.prod.js +9 -0
  33. package/ai-profiles.json +13 -3
  34. package/bin/project-knowledge.js +51 -0
  35. package/docs/development-progress.md +141 -0
  36. package/package.json +11 -2
  37. package/scripts/gen-commit-doc.ps1 +1 -1
  38. package/scripts/list-features.ps1 +1 -1
  39. package/scripts/register-scheduled-task.bat +3 -1
package/CHANGELOG.md CHANGED
@@ -9,6 +9,47 @@
9
9
  - **Removed**:删除文件 / 目录
10
10
  - **Fixed**:修复错误
11
11
 
12
+ ## [1.0.0] — 2026-06-15
13
+
14
+ First public npm release of `project-knowledge`. The tool ships as a single
15
+ zero-dependency Node server plus a vendored Vue 3 + Tailwind dashboard.
16
+
17
+ ### Added
18
+ - **Server & API** — `_site/server.js` provides the dashboard, REST API, and
19
+ background job runner using only Node built-ins.
20
+ - **Dashboard** — Vue 3 + Tailwind single-page UI in `_site/index.html`:
21
+ project control panel, collapsible summary groups, issues panel with
22
+ deduplication, supervision view.
23
+ - **AI pipeline**
24
+ - `ai-adapter` — pluggable adapter dispatch (mock / Claude CLI / custom LLM)
25
+ - `analysis-orchestrator` — initial scan + commit-batch analysis
26
+ - `context-pack-builder` — packs context per LLM call
27
+ - `claude-cli-runner` + `claude-workbench` integration
28
+ - `ai-workspace`, `prompt-registry`, `llm-client`
29
+ - `draft-apply` — reviewable drafts before writeback
30
+ - **Knowledge store v3** — `kb-v3` simplified layout, plus `knowledge-store`
31
+ external store abstraction and `kb-validator` for schema checks.
32
+ - **Supervision & logging** — `supervision` + `structured-logger` for audit
33
+ of every AI job (mode, adapter, tokens, status).
34
+ - **Dashboard features (TASK-014 … TASK-020)**
35
+ - TASK-014: project control panel simplification
36
+ - TASK-015: project delete functionality
37
+ - TASK-016: source branch tagging
38
+ - TASK-017: collapsible project groups in summary panel
39
+ - TASK-018: issues panel duplication
40
+ - TASK-019: KB storage framework + summary panel persistence across project switches
41
+ - TASK-020: kb-v3 simplified layout
42
+ - **Vendored browser libs** — `_site/vendor/{vue.global.prod.js, tailwind-browser.js}`
43
+ - **Tag-driven publish** — `.github/workflows/publish.yml` publishes to npm on
44
+ `v*` tags with provenance.
45
+
46
+ ### Changed
47
+ - `package.json` — version 1.0.0; `files` updated to include `_site/vendor/`.
48
+ - Expanded test suite under `_site/_test/` (AI profile, baseline schema,
49
+ draft-apply, kb-v2/v3 templates, knowledge-store + logs + supervision,
50
+ claude-workbench, project-control-panel task14, task15-20 integration +
51
+ UI flow, ui-smoke).
52
+
12
53
  ## 2026-06-08
13
54
 
14
55
  ### Changed
package/README.md CHANGED
@@ -1,79 +1,222 @@
1
- # SanQian.Xu 项目知识库
1
+ # project-knowledge
2
+
3
+ > Knowledge-base manager with Git integration, AI-driven analysis, and bilingual (zh-CN / en-US) knowledge output.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/project-knowledge.svg)](https://www.npmjs.com/package/project-knowledge)
6
+ [![Node](https://img.shields.io/node/v/project-knowledge.svg)](https://www.npmjs.com/package/project-knowledge)
7
+
8
+ `project-knowledge` runs a local web dashboard that scans Git repositories,
9
+ hands batches of commits to an AI agent (Claude CLI / mock adapter / custom
10
+ LLM), and writes the resulting knowledge entries back to a structured
11
+ knowledge base per project — modules, framework docs, commit changelogs,
12
+ quality reviews, and operation notes.
13
+
14
+ It is built to be a single-developer tool: zero runtime dependencies (Node
15
+ built-ins only for the server), a vendored Vue 3 + Tailwind dashboard, and a
16
+ file-based store you can read, grep, and version-control.
17
+
18
+ ---
19
+
20
+ ## Features
21
+
22
+ - **Project registry** — declare projects in `projects.json`; the dashboard
23
+ drives scan / analyze / commit-draft / publish flows per project.
24
+ - **Git scanner** — walks the working tree of each registered project,
25
+ extracts features and recent commits, and feeds the AI pipeline.
26
+ - **AI pipeline**
27
+ - `analysis-orchestrator` — initial scan + commit-batch analysis modes.
28
+ - `ai-adapter` — pluggable profiles (mock / Claude CLI / custom LLM via
29
+ `llm-client`); `ai-profiles.json` controls which adapter runs.
30
+ - `context-pack-builder` — packs relevant context for each LLM call.
31
+ - `claude-cli-runner` + `claude-workbench` integration — runs Claude Code
32
+ in a sandboxed workspace and surfaces its session log.
33
+ - `prompt-registry` — versioned prompt templates (`claude-prompts.json`).
34
+ - `draft-apply` — turns LLM output into reviewable draft files; the user
35
+ approves, edits, or rejects each draft before it lands.
36
+ - **Knowledge store (kb-v3)** — simplified layout: `framework.md`,
37
+ `modules/`, `commits/`, `features/`, `references/`, `operations/`,
38
+ `quality/`, plus a generated `kb-manifest.json` per project.
39
+ - **Dashboard** — Vue 3 + Tailwind single-page UI: project control panel,
40
+ collapsible summary groups, issues panel with dedup, supervision view
41
+ showing structured logs and AI job runs.
42
+ - **Supervision & structured logging** — every AI job is logged with
43
+ structured fields (run id, mode, adapter, token usage, status) so you can
44
+ audit what the agent did and replay.
45
+ - **Hook integration** — installs a Windows scheduled task / post-commit
46
+ hook so each Git commit can trigger an analysis job automatically.
47
+ - **Validator & PR context packs** — `kb-validator` checks the knowledge
48
+ base against the baseline schema; `buildPrContextPack` exports a focused
49
+ pack for a PR so reviewers (or an LLM) get just the relevant context.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ npm install -g project-knowledge
55
+ ```
56
+
57
+ Requirements:
58
+
59
+ - Node.js ≥ 18
60
+ - Git on `PATH`
61
+ - (Optional) [Claude Code CLI](https://docs.claude.com/en/docs/claude-code)
62
+ if you want the Claude adapter (the `mock-agent` adapter works without it)
63
+ - Windows has the most complete hook/scheduled-task flow. The dashboard,
64
+ registry, AI profiles, and knowledge-store APIs use install-relative paths.
2
65
 
3
- > 统一管理 `D:\SanQian.Xu` 下多个项目的项目框架、功能模块、git 提交的功能变更。
66
+ ## Quick start
4
67
 
5
- ## 目录速览
68
+ Global install:
6
69
 
70
+ ```bash
71
+ npm install -g project-knowledge
72
+ project-knowledge # starts server on http://localhost:7777
73
+ project-knowledge start --port 9000
7
74
  ```
8
- project-knowledge-base/
9
- ├── INDEX.md # 项目速查表(按角色/技术栈/标签)
10
- ├── projects.json # 项目元信息(脚本读取)
11
- ├── iterations.json # 里程碑定义
12
- ├── projects/
13
- │ ├── claude-code-ui/ # Claude Code UI(克隆参考项目)
14
- │ ├── claude-devsprite/ # Claude-DevSprite(CLI + Web Dashboard)
15
- │ ├── token-consumption-leaderboard/ # Token 消耗排行榜
16
- │ ├── quant-platform/ # 量化金融数据平台
17
- │ ├── web-remote-control/ # WebRTC 远程桌面
18
- │ └── tokenrank-cloud/ # TokenRank Cloud
19
- ├── scripts/
20
- │ ├── gen-commit-doc.ps1 # 核心:git log → 功能文档
21
- │ ├── gen-commit-doc.sh # Bash 版
22
- │ ├── list-features.ps1 # 列出某项目所有功能
23
- │ └── register-scheduled-task.bat # 注册每日定时任务
24
- └── templates/ # 文档模板
75
+
76
+ Or run from source:
77
+
78
+ ```bash
79
+ git clone https://github.com/SanQianX/project-knowledge-base.git
80
+ cd project-knowledge-base
81
+ npm install
82
+ npm start # node _site/server.js
83
+ ```
84
+
85
+ Then open <http://localhost:7777>. Override the port with
86
+ `KB_SITE_PORT=9000 npm start`.
87
+
88
+ ### First-time setup
89
+
90
+ 1. Open the dashboard → **Projects** → register a project (path + slug).
91
+ On first run, v1.0.1 creates missing runtime JSON files automatically. If
92
+ `projects.json` or `ai-profiles.json` is empty or invalid, the server recovers
93
+ with a safe default; invalid JSON is backed up as `*.invalid-<timestamp>.bak`.
94
+
95
+ 2. Open **Settings** / **AI profiles** to add MiniMax / GLM / GPT /
96
+ Anthropic-compatible models. The npm package ships with a safe mock profile
97
+ and no API keys.
98
+ 3. Run **Scan** → **Initial analysis** → review drafts → **Apply drafts**.
99
+ 4. (Optional) **Install hook** so future commits trigger an analysis job.
100
+
101
+ ### Upgrading from v1.0.0
102
+
103
+ v1.0.0 did not publish a `bin` command, so a global install could complete
104
+ without creating the `project-knowledge` executable. Reinstall v1.0.1:
105
+
106
+ ```bash
107
+ npm uninstall -g project-knowledge
108
+ npm install -g project-knowledge@latest
109
+ project-knowledge --version
110
+ project-knowledge
25
111
  ```
26
112
 
27
- ## 阅读路径
113
+ ## Architecture
28
114
 
29
- 1. **第一次来** → 看 [INDEX.md](./INDEX.md) 了解项目概况
30
- 2. **找某个项目的框架** → `projects/<slug>/framework.md`
31
- 3. **找某个功能模块** `projects/<slug>/modules/00-index.md`
32
- 4. **找某次 git 提交的功能变更** `projects/<slug>/commits/00-index.md`
33
- 5. **找项目与源码的对应关系** `projects/<slug>/references/source-map.md`
115
+ ```
116
+ project-knowledge-base/
117
+ ├── _site/ # The dashboard + server (ships to npm)
118
+ │ ├── server.js # HTTP server, REST API, zero deps
119
+ │ ├── index.html # Vue 3 + Tailwind single-page UI
120
+ │ ├── lib/ # All server-side modules
121
+ │ │ ├── ai-adapter.js # Profile dispatch (mock / Claude / LLM)
122
+ │ │ ├── analysis-orchestrator.js
123
+ │ │ ├── context-pack-builder.js
124
+ │ │ ├── claude-cli-runner.js
125
+ │ │ ├── claude-workbench (workspace manager)
126
+ │ │ ├── draft-apply.js # LLM output → reviewable draft files
127
+ │ │ ├── ai-workspace.js
128
+ │ │ ├── job-orchestrator.js
129
+ │ │ ├── kb-v3.js # Knowledge base layout v3
130
+ │ │ ├── kb-validator.js
131
+ │ │ ├── knowledge-store.js # External store abstraction
132
+ │ │ ├── supervision.js # Audit / monitoring layer
133
+ │ │ ├── structured-logger.js
134
+ │ │ ├── prompt-registry.js
135
+ │ │ ├── llm-client.js
136
+ │ │ ├── index-builder.js
137
+ │ │ ├── scanner.js
138
+ │ │ ├── git-runner.js
139
+ │ │ └── hook-manager.js
140
+ │ ├── scripts/ # hook-trigger.js, safe-runner.js
141
+ │ ├── vendor/ # vue.global.prod.js, tailwind-browser.js
142
+ │ ├── _test/ # smoke + unit tests (run via `npm test`)
143
+ │ ├── start.bat / stop.bat # Windows convenience launchers
144
+ │ └── server.js
145
+ ├── templates/ # Markdown templates for generated docs
146
+ ├── scripts/ # PowerShell + bash commit-doc generators
147
+ ├── docs/ # Design docs and contracts
148
+ ├── tasks/ # Task tracking (TASK-001 …)
149
+ ├── projects/ # The actual knowledge base (one dir per project)
150
+ ├── ai-profiles.json # AI adapter profiles
151
+ ├── claude-prompts.json # Prompt templates
152
+ ├── projects.json # Project registry
153
+ └── iterations.json # Milestone definitions
154
+ ```
34
155
 
35
- ## 项目 slug 对照
156
+ The server is intentionally dependency-free — every line in `_site/server.js`
157
+ and `_site/lib/*.js` uses only Node built-ins, so the install footprint is
158
+ the dashboard assets and your data.
36
159
 
37
- | slug | 项目目录 | 显示名 |
38
- |------|----------|--------|
39
- | `claude-code-ui` | `Claude-Code-UI\Reference Project\claudecodeui` | Claude Code UI |
40
- | `claude-devsprite` | `Claude-DevSprite` | Claude-DevSprite |
41
- | `token-consumption-leaderboard` | `Token Consumption Leaderboard` | Token Consumption Leaderboard |
42
- | `quant-platform` | `quant-platform` | quant-platform |
43
- | `web-remote-control` | `web-remote-control` | web-remote-control |
44
- | `tokenrank-cloud` | `TokenRank Cloud` | TokenRank Cloud |
160
+ ## Configuration
45
161
 
46
- ## 如何更新知识库
162
+ | File | Purpose |
163
+ | --------------------- | -------------------------------------------------------------------- |
164
+ | `projects.json` | Registry of projects: slug, name, path, source control info. |
165
+ | `iterations.json` | Milestone definitions referenced by summary panels. |
166
+ | `ai-profiles.json` | One entry per AI adapter (id, type, model, prompt-id, options). |
167
+ | `claude-prompts.json` | Versioned prompt templates keyed by id; referenced by profiles. |
168
+ | `knowledge-store.json` | Runtime state of the external knowledge store (gitignored). |
169
+ | `logging.json` | Runtime logging config (gitignored). |
47
170
 
48
- ### 手动更新
171
+ Environment variables:
49
172
 
50
- - `projects/<slug>/modules/*.md` 的「最近一次重要变更」段:PR 作者
51
- - `projects/<slug>/framework.md`:项目 owner(季度审查)
52
- - 改 `INDEX.md`、`projects.json`、`iterations.json`:知识库 owner
173
+ - `KB_SITE_PORT` server port (default `7777`)
174
+ - `KB_SITE_HOST` — bind host (default `127.0.0.1`)
53
175
 
54
- ### 自动更新(git 提交 → 功能文档)
176
+ ## Testing
55
177
 
56
- ```powershell
57
- cd D:\SanQian.Xu\project-knowledge-base\scripts
58
- .\gen-commit-doc.ps1 -ProjectSlug claude-devsprite # 单项目
59
- .\gen-commit-doc.ps1 -ProjectSlug ALL # 全部项目
178
+ ```bash
179
+ npm test
60
180
  ```
61
181
 
62
- 脚本行为:
63
- - `feat` / `fix` / `refactor` / `perf` → 写入 `commits/`
64
- - `docs` / `chore` / `test` / `style` → 跳过(不写入 commits/)
65
- - feature-slug 的提交 **追加到已有功能文件**(不新建)
66
- - `docs:` / `chore:` 视为维护类变更,不进 `commits/`
182
+ Runs `_site/_test/run-all-tests.js`, which executes the smoke + unit tests
183
+ across AI adapters, draft-apply, kb-v2/v3 templates, knowledge store,
184
+ supervision, claude workbench, the project control panel (TASK-014),
185
+ TASK-015–020 integration, and the UI flow.
186
+
187
+ ## Development
67
188
 
68
- 详见 `templates/commit-feature.md` 的输出格式。
189
+ The dashboard is a static `index.html` that ships Vue 3 + Tailwind from
190
+ `_site/vendor/`. Edit `_site/index.html` directly and refresh the browser;
191
+ no build step. Restart the server after editing anything under `_site/lib/`.
192
+
193
+ Useful entry points:
194
+
195
+ - Add a new AI adapter → `_site/lib/ai-adapter.js` + a profile in
196
+ `ai-profiles.json`.
197
+ - Add a new prompt → `claude-prompts.json` + reference it from a profile.
198
+ - Add a new dashboard page → `_site/index.html` (Vue components) + a route
199
+ handler in `_site/server.js`.
200
+
201
+ ## Publishing
202
+
203
+ Releases are tag-driven. The `.github/workflows/publish.yml` workflow runs
204
+ `npm publish --provenance --access public` on every `v*` tag push, using the
205
+ `NPM_TOKEN` secret. To cut a release:
206
+
207
+ ```bash
208
+ # bump version in package.json
209
+ git commit -am "Release vX.Y.Z"
210
+ git tag -a vX.Y.Z -m "vX.Y.Z"
211
+ git push origin main
212
+ git push origin vX.Y.Z # triggers publish
213
+ ```
69
214
 
70
- ## 维护约定
215
+ ## License
71
216
 
72
- - **路径风格**:所有内部链接用 POSIX 风格(`/` 而非 `\`)
73
- - **命名风格**:文件名 kebab-case 全小写 ASCII
74
- - **Frontmatter**:每个 `.md` 顶部带 YAML frontmatter(用 `---` 包围)
75
- - **每文件一职责**:一个模块一文件、一次功能交付一文件
217
+ Currently `UNLICENSED` (see `package.json`). Revisit before public
218
+ consumption MIT or Apache-2.0 are reasonable defaults for a tool like this.
76
219
 
77
- ## 知识库自身的变更
220
+ ## Changelog
78
221
 
79
- [CHANGELOG.md](./CHANGELOG.md)
222
+ See [CHANGELOG.md](./CHANGELOG.md).
@@ -7,6 +7,8 @@ const {
7
7
  ADAPTERS, getAdapter, listAdapters,
8
8
  validateInitialOutput, validateCommitBatchOutput,
9
9
  } = require('../lib/ai-adapter');
10
+ const { readConfig: readLlmConfig } = require('../lib/llm-client');
11
+ const { buildClaudeEnvFromProfile } = require('../lib/claude-cli-runner');
10
12
 
11
13
  const ROOT = path.resolve(__dirname, '..', '..');
12
14
  const SERVER = path.join(ROOT, '_site', 'server.js');
@@ -15,6 +17,7 @@ const AI_PROFILES_JSON = path.join(ROOT, 'ai-profiles.json');
15
17
  const PORT = process.env.KB_AI_TEST_PORT || '7795';
16
18
  const BASE_URL = `http://127.0.0.1:${PORT}`;
17
19
  const TEMP_SLUG = 'task-005-temp';
20
+ const ORIGINAL_AI_PROFILES_TEXT = fs.readFileSync(AI_PROFILES_JSON, 'utf-8');
18
21
 
19
22
  function assert(cond, msg) { if (!cond) throw new Error(msg); }
20
23
 
@@ -52,6 +55,7 @@ async function cleanup() {
52
55
  delete cur[TEMP_SLUG];
53
56
  fs.writeFileSync(PROJECTS_JSON, JSON.stringify(cur, null, 2) + '\n', 'utf-8');
54
57
  }
58
+ fs.writeFileSync(AI_PROFILES_JSON, ORIGINAL_AI_PROFILES_TEXT, 'utf-8');
55
59
  }
56
60
 
57
61
  (async () => {
@@ -146,17 +150,68 @@ async function cleanup() {
146
150
  assert(!r.res.ok && r.res.status === 400, 'invalid profile id should 400');
147
151
  assert(Array.isArray(r.data.errors), 'should return errors list');
148
152
 
153
+ r = await json('PUT', '/api/ai-profiles', {
154
+ schema: 'ai-profiles/v1',
155
+ defaultProfileId: 'minimax-m3',
156
+ profiles: [
157
+ { id: 'mock-agent', name: 'Mock', enabled: true, implementation: 'mock-agent' },
158
+ { id: 'minimax-m3', name: 'Disabled LLM', enabled: false, implementation: 'claude-code-agent' },
159
+ ],
160
+ });
161
+ assert(!r.res.ok && r.res.status === 400, 'disabled default profile should 400');
162
+
149
163
  // 9. PUT /api/ai-profiles with valid config
150
164
  r = await json('PUT', '/api/ai-profiles', {
151
165
  schema: 'ai-profiles/v1',
152
166
  defaultProfileId: 'mock-agent',
153
167
  profiles: [
154
168
  { id: 'mock-agent', name: 'Mock', enabled: true, implementation: 'mock-agent' },
155
- { id: 'claude-code-agent', name: 'Claude Code', enabled: false, implementation: 'claude-code-agent' },
169
+ {
170
+ id: 'minimax-m3',
171
+ name: 'MiniMax M3',
172
+ enabled: false,
173
+ implementation: 'claude-code-agent',
174
+ baseUrl: 'https://example.test/anthropic',
175
+ apiKey: 'test-key',
176
+ model: 'test-model',
177
+ version: '2023-06-01',
178
+ temperature: 0.1,
179
+ maxTokens: 1234,
180
+ timeoutMs: 7654,
181
+ },
156
182
  ],
157
183
  });
158
184
  assert(r.res.ok, 'valid profile config should be accepted');
159
185
 
186
+ const llmCfg = readLlmConfig({ profileId: 'minimax-m3' });
187
+ assert(llmCfg.baseUrl === 'https://example.test/anthropic', 'llm client should read baseUrl from profile');
188
+ assert(llmCfg.apiKey === 'test-key', 'llm client should read apiKey from profile');
189
+ assert(llmCfg.model === 'test-model', 'llm client should read model from profile');
190
+ assert(llmCfg.timeoutMs === 7654, 'llm client should read timeoutMs from profile');
191
+
192
+ const claudeEnv = buildClaudeEnvFromProfile({
193
+ apiKey: 'test-key',
194
+ baseUrl: 'https://example.test/anthropic',
195
+ model: 'test-model',
196
+ timeoutMs: 7654,
197
+ });
198
+ assert(claudeEnv.ANTHROPIC_AUTH_TOKEN === 'test-key', 'claude env should set auth token');
199
+ assert(claudeEnv.ANTHROPIC_BASE_URL === 'https://example.test/anthropic', 'claude env should set base URL');
200
+ assert(claudeEnv.ANTHROPIC_MODEL === 'test-model', 'claude env should set model');
201
+ assert(claudeEnv.ANTHROPIC_DEFAULT_SONNET_MODEL === 'test-model', 'claude env should map sonnet alias');
202
+ assert(claudeEnv.ANTHROPIC_DEFAULT_OPUS_MODEL === 'test-model', 'claude env should map opus alias');
203
+ assert(claudeEnv.ANTHROPIC_DEFAULT_HAIKU_MODEL === 'test-model', 'claude env should map haiku alias');
204
+ assert(claudeEnv.API_TIMEOUT_MS === '7654', 'claude env should set timeout');
205
+ assert(claudeEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC === '1', 'claude env should disable nonessential traffic');
206
+
207
+ r = await json('POST', '/api/ai-profiles/test', {
208
+ profileId: 'mock-agent',
209
+ prompt: 'what model are you?',
210
+ });
211
+ assert(r.res.ok, 'mock profile test should return 200');
212
+ assert(r.data.ok && r.data.model === 'mock-agent', 'mock profile test should identify mock-agent');
213
+ assert(/what model are you/.test(r.data.text), 'mock profile test should echo prompt');
214
+
160
215
  // 10. Per-project ai profile selection
161
216
  r = await json('PUT', '/api/projects', {
162
217
  slug: TEMP_SLUG,
@@ -165,6 +220,9 @@ async function cleanup() {
165
220
  assert(r.res.ok, 'upsert should succeed');
166
221
  assert(r.data.repoStatus !== undefined, 'repoStatus should be returned');
167
222
 
223
+ r = await json('PUT', `/api/projects/${TEMP_SLUG}/ai-profile`, { aiProfileId: 'minimax-m3' });
224
+ assert(!r.res.ok && r.res.status === 400, 'disabled ai profile should not be assignable');
225
+
168
226
  r = await json('PUT', `/api/projects/${TEMP_SLUG}/ai-profile`, { aiProfileId: 'mock-agent' });
169
227
  assert(r.res.ok, 'set ai profile should succeed');
170
228
  assert(r.data.aiProfileId === 'mock-agent', 'aiProfileId should be set');
@@ -73,7 +73,7 @@ async function removeTempProject() {
73
73
 
74
74
  const stateResult = await json('GET', '/api/state');
75
75
  assert(stateResult.res.ok, '/api/state should return 200');
76
- assert(stateResult.data.projectSchemaVersion === 'v1', 'projectSchemaVersion should be v1');
76
+ assert(stateResult.data.projectSchemaVersion === 'v3', 'projectSchemaVersion should be v3');
77
77
  assert(stateResult.data.kbRoot.endsWith('project-knowledge-base'), 'kbRoot should point to project-knowledge-base');
78
78
 
79
79
  const projects = stateResult.data.projects;
@@ -85,7 +85,7 @@ async function removeTempProject() {
85
85
  assert(Object.prototype.hasOwnProperty.call(cfg, 'lastSeenCommit'), `${slug} missing lastSeenCommit`);
86
86
  assert(Object.prototype.hasOwnProperty.call(cfg, 'lastAnalyzedCommit'), `${slug} missing lastAnalyzedCommit`);
87
87
  assert(cfg.aiProfileId, `${slug} missing aiProfileId`);
88
- assert(cfg.kbSchemaVersion === 'v1', `${slug} missing kbSchemaVersion v1`);
88
+ assert(['v1', 'v2', 'v3'].includes(cfg.kbSchemaVersion), `${slug} missing supported kbSchemaVersion`);
89
89
  assert(cfg.goalStatus, `${slug} missing goalStatus`);
90
90
  assert(!String(cfg.kbPath || '').includes('\\SanQian.Xu\\kb\\'), `${slug} uses legacy kbPath`);
91
91
  }
@@ -106,7 +106,8 @@ async function removeTempProject() {
106
106
  assert(projectsResult.res.ok, '/api/projects should return 200');
107
107
  const temp = projectsResult.data[TEMP_SLUG];
108
108
  assert(temp, 'temp project should exist');
109
- assert(temp.kbPath.endsWith(`project-knowledge-base\\projects\\${TEMP_SLUG}`), 'legacy kbPath should be normalized');
109
+ assert(!String(temp.kbPath || '').includes('\\SanQian.Xu\\kb\\'), 'legacy kbPath should be normalized');
110
+ assert(String(temp.kbPath || '').endsWith(`\\${TEMP_SLUG}`), 'normalized kbPath should end with the slug');
110
111
  assert(temp.enabled === true, 'temp project should default enabled=true');
111
112
  // repoStatus is auto-validated on upsert; ROOT may or may not be a git repo.
112
113
  // Accept any of the well-defined statuses that prove normalization ran.
@@ -0,0 +1,72 @@
1
+ // Claude workbench regression test.
2
+ // Verifies permission-gated turns and persisted session metadata without spawning Claude.
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const runner = require('../lib/claude-cli-runner');
7
+
8
+ const ROOT = path.resolve(__dirname, '..', '..');
9
+ const TEMP_KB = path.join(ROOT, 'projects', 'claude-workbench-test-temp');
10
+ const TEMP_AI = path.join(ROOT, '_site', '_ai', 'claude-workbench-test-temp');
11
+
12
+ function assert(cond, msg) {
13
+ if (!cond) throw new Error(msg);
14
+ }
15
+
16
+ (async () => {
17
+ fs.rmSync(TEMP_KB, { recursive: true, force: true });
18
+ fs.rmSync(TEMP_AI, { recursive: true, force: true });
19
+ fs.mkdirSync(TEMP_KB, { recursive: true });
20
+
21
+ const started = runner.startSession({
22
+ slug: 'claude-workbench-test-temp',
23
+ projectPath: ROOT,
24
+ kbPath: TEMP_KB,
25
+ promptKey: 'initial-analysis',
26
+ aiProfile: {
27
+ id: 'test-profile',
28
+ implementation: 'claude-code-agent',
29
+ model: 'test-model',
30
+ },
31
+ vars: {
32
+ SLUG: 'claude-workbench-test-temp',
33
+ PROJECT_PATH: ROOT,
34
+ PRIMARY_LANGUAGE: 'JavaScript',
35
+ KNOWLEDGE_LANGUAGE: 'zh-CN',
36
+ },
37
+ });
38
+
39
+ assert(started.sessionId, 'startSession should return sessionId');
40
+ assert(started.pendingPermission, 'initial turn should require permission');
41
+
42
+ const state = runner.getState(started.sessionId);
43
+ assert(state.state === 'pending-permission', `expected pending-permission, got ${state.state}`);
44
+ assert(state.pendingPermission.requestId === started.pendingPermission.requestId, 'state should expose pending request');
45
+
46
+ const replay = [];
47
+ const unsubscribe = runner.subscribe(started.sessionId, event => replay.push(event.type));
48
+ unsubscribe();
49
+ assert(replay.includes('claude/permission-request'), 'subscribe should replay permission request');
50
+
51
+ const recordPath = path.join(TEMP_AI, 'claude-workbench', `${started.sessionId}.json`);
52
+ assert(fs.existsSync(recordPath), 'session record should be persisted');
53
+ const record = JSON.parse(fs.readFileSync(recordPath, 'utf-8'));
54
+ assert(record.claudeSessionId === null, 'no Claude session id before approval/spawn');
55
+ assert(record.events.some(e => e.type === 'claude/permission-request'), 'record should include permission event');
56
+
57
+ const resolved = runner.resolvePermission(started.sessionId, started.pendingPermission.requestId, { allow: false });
58
+ assert(resolved.ok && resolved.started === false, 'denying permission should not start Claude');
59
+ const after = runner.getState(started.sessionId);
60
+ assert(after.state === 'idle', `denied permission should return to idle, got ${after.state}`);
61
+ assert(!after.pendingPermission, 'pending permission should be cleared after denial');
62
+
63
+ runner.deleteSession(started.sessionId);
64
+ fs.rmSync(TEMP_KB, { recursive: true, force: true });
65
+ fs.rmSync(TEMP_AI, { recursive: true, force: true });
66
+ console.log('Claude workbench test passed');
67
+ })().catch(err => {
68
+ try { fs.rmSync(TEMP_KB, { recursive: true, force: true }); } catch {}
69
+ try { fs.rmSync(TEMP_AI, { recursive: true, force: true }); } catch {}
70
+ console.error(err);
71
+ process.exit(1);
72
+ });
@@ -103,7 +103,9 @@ async function cleanup() {
103
103
 
104
104
  // 3. List drafts
105
105
  const drafts = listDrafts(kbBase, runId);
106
- assert(drafts.length >= 3, `expected 3+ drafts (1 goal + 1 analysis + 2 changes + 1 feature), got ${drafts.length}`);
106
+ assert(drafts.length >= 2, `expected at least change + feature drafts, got ${drafts.length}`);
107
+ assert(drafts.some(d => d.path.startsWith('changes/')), 'expected a changes draft');
108
+ assert(drafts.some(d => d.path.startsWith('features/')), 'expected a features draft for feature commit');
107
109
  // The orchestrator does NOT produce a goal draft for commit runs. It produces changes/* and a feature/*.
108
110
 
109
111
  // 4. Build draft payloads from the on-disk drafts
@@ -291,7 +293,8 @@ async function cleanup() {
291
293
  // 14. List drafts
292
294
  r = await json('GET', `/api/projects/${slug}/drafts/${srvRunId}`);
293
295
  assert(r.res.ok, 'list drafts should succeed');
294
- assert(r.data.drafts.length >= 3, `expected 3+ drafts, got ${r.data.drafts.length}`);
296
+ assert(r.data.drafts.length >= 2, `expected at least change + feature drafts, got ${r.data.drafts.length}`);
297
+ assert(r.data.drafts.some(d => d.path.startsWith('changes/')), 'server should list a changes draft');
295
298
  const serverDrafts = r.data.drafts;
296
299
 
297
300
  // 15. Read raw draft text
@@ -301,10 +304,12 @@ async function cleanup() {
301
304
  assert(typeof r.data.content === 'string' && r.data.content.length > 0, 'raw content should be non-empty');
302
305
 
303
306
  // 16. Apply without allowGoalEdit (no goal in these drafts, so this should succeed)
304
- const payloads2 = serverDrafts.map(d => ({
305
- path: d.path,
306
- content: fs.readFileSync(path.join(kbPath, '_ai', 'drafts', srvRunId, d.path), 'utf-8'),
307
- }));
307
+ const payloads2 = [];
308
+ for (const d of serverDrafts) {
309
+ const raw = await json('GET', `/api/projects/${slug}/drafts/${srvRunId}/raw?path=${encodeURIComponent(d.path)}`);
310
+ assert(raw.res.ok, `raw draft should load for ${d.path}`);
311
+ payloads2.push({ path: d.path, content: raw.data.content, sourceBranch: d.sourceBranch, sourceHeadCommit: d.sourceHeadCommit });
312
+ }
308
313
  r = await json('POST', `/api/projects/${slug}/drafts/${srvRunId}/apply`, {
309
314
  drafts: payloads2,
310
315
  allowGoalEdit: false,
@@ -323,6 +328,7 @@ async function cleanup() {
323
328
 
324
329
  // 19. Refuse: try to apply a goal draft without allowGoalEdit
325
330
  fs.mkdirSync(path.join(kbPath, '_ai', 'drafts', 'goal-test'), { recursive: true });
331
+ fs.mkdirSync(path.join(kbPath, '_ai', 'runs'), { recursive: true });
326
332
  fs.writeFileSync(path.join(kbPath, '_ai', 'drafts', 'goal-test', 'project-goal.md'), '# Goal — bad');
327
333
  fs.writeFileSync(path.join(kbPath, '_ai', 'runs', 'goal-test.json'), JSON.stringify({ schema: 'ai-run/v1', runId: 'goal-test' }));
328
334
  r = await json('POST', `/api/projects/${slug}/drafts/goal-test/apply`, {