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
@@ -230,10 +230,10 @@ Constraints:
230
230
  const claudeCodeAgent = {
231
231
  id: 'claude-code-agent',
232
232
  name: 'Claude Code Agent (Anthropic-compatible)',
233
- description: 'Real LLM via ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN. Used by post-commit hook when projects.json sets aiProfileId=claude-code-agent.',
233
+ description: 'Real LLM through an Anthropic-compatible Messages API. AI profiles can reuse this implementation with different model providers.',
234
234
  async analyzeInitialProject({ project, contextPack }) {
235
235
  const user = buildInitialUser({ project, contextPack });
236
- const r = await completeJson({ system: INITIAL_SYSTEM, user, maxTokens: 3000 });
236
+ const r = await completeJson({ system: INITIAL_SYSTEM, user, maxTokens: 3000, profileId: project && project.aiProfileId });
237
237
  if (!r.parsed) {
238
238
  const e = new Error(`LLM returned no parseable JSON (${r.parseError && r.parseError.message || 'no output'})`);
239
239
  e.llmText = r.text;
@@ -243,7 +243,7 @@ const claudeCodeAgent = {
243
243
  },
244
244
  async analyzeCommitBatch({ project, commits, contextPack }) {
245
245
  const user = buildCommitUser({ project, commits, contextPack });
246
- const r = await completeJson({ system: COMMIT_SYSTEM, user, maxTokens: 4096 });
246
+ const r = await completeJson({ system: COMMIT_SYSTEM, user, maxTokens: 4096, profileId: project && project.aiProfileId });
247
247
  if (!r.parsed) {
248
248
  console.error(`[claude-code-agent] LLM did not return valid JSON. text head: ${r.text.slice(0, 500)} ... text tail: ${r.text.slice(-500)}`);
249
249
  const e = new Error(`LLM returned no parseable JSON (${r.parseError && r.parseError.message || 'no output'})`);
@@ -0,0 +1,120 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const APP_ROOT = path.resolve(__dirname, '..', '..');
5
+ const SITE_ROOT = path.resolve(__dirname, '..');
6
+ const AI_ROOT = path.join(SITE_ROOT, '_ai');
7
+ const STANDARD_SUBDIRS = ['drafts', 'runs', 'context-packs', 'backups', 'claude-workbench'];
8
+
9
+ function safeSlug(slug) {
10
+ return typeof slug === 'string' && /^[a-z0-9][a-z0-9-]{0,40}$/.test(slug);
11
+ }
12
+
13
+ function projectAIPath(slug) {
14
+ if (!safeSlug(slug)) throw new Error(`invalid slug for AI workspace: ${slug}`);
15
+ return path.join(AI_ROOT, slug);
16
+ }
17
+
18
+ function ensureProjectAIPath(slug) {
19
+ const root = projectAIPath(slug);
20
+ for (const sub of STANDARD_SUBDIRS) {
21
+ fs.mkdirSync(path.join(root, sub), { recursive: true });
22
+ }
23
+ return root;
24
+ }
25
+
26
+ function legacyAIPath(kbPath) {
27
+ return path.join(kbPath, '_ai');
28
+ }
29
+
30
+ function copyDir(src, dest) {
31
+ fs.mkdirSync(dest, { recursive: true });
32
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
33
+ const from = path.join(src, entry.name);
34
+ const to = path.join(dest, entry.name);
35
+ if (entry.isDirectory()) copyDir(from, to);
36
+ else if (entry.isFile()) fs.copyFileSync(from, to);
37
+ }
38
+ }
39
+
40
+ function migrateAIWorkspace({ slug, kbPath, preserveOriginal = true }) {
41
+ const legacy = legacyAIPath(kbPath);
42
+ const target = ensureProjectAIPath(slug);
43
+ const result = {
44
+ ok: true,
45
+ slug,
46
+ legacyPath: legacy,
47
+ targetPath: target,
48
+ copied: false,
49
+ preservedOriginal: preserveOriginal,
50
+ };
51
+ if (!fs.existsSync(legacy)) return result;
52
+ copyDir(legacy, target);
53
+ result.copied = true;
54
+ if (!preserveOriginal) {
55
+ fs.rmSync(legacy, { recursive: true, force: true });
56
+ result.preservedOriginal = false;
57
+ }
58
+ return result;
59
+ }
60
+
61
+ function runPath(slug, runId) {
62
+ return path.join(projectAIPath(slug), 'runs', `${runId}.json`);
63
+ }
64
+
65
+ function draftDir(slug, runId) {
66
+ return path.join(projectAIPath(slug), 'drafts', runId);
67
+ }
68
+
69
+ function contextPackDir(slug, runId) {
70
+ return path.join(projectAIPath(slug), 'context-packs', runId);
71
+ }
72
+
73
+ function findExistingRunPath({ slug, kbPath, runId }) {
74
+ const primary = path.join(projectAIPath(slug), 'runs', `${runId}.json`);
75
+ if (fs.existsSync(primary)) return primary;
76
+ const legacy = path.join(legacyAIPath(kbPath), 'runs', `${runId}.json`);
77
+ if (fs.existsSync(legacy)) return legacy;
78
+ return primary;
79
+ }
80
+
81
+ function findExistingDraftDir({ slug, kbPath, runId }) {
82
+ const primary = path.join(projectAIPath(slug), 'drafts', runId);
83
+ if (fs.existsSync(primary)) return primary;
84
+ const legacy = path.join(legacyAIPath(kbPath), 'drafts', runId);
85
+ if (fs.existsSync(legacy)) return legacy;
86
+ return primary;
87
+ }
88
+
89
+ function listProjectDirs(projectSlug = null, projects = null) {
90
+ if (projectSlug) return [projectAIPath(projectSlug)];
91
+ const dirs = [];
92
+ if (fs.existsSync(AI_ROOT)) {
93
+ for (const entry of fs.readdirSync(AI_ROOT, { withFileTypes: true })) {
94
+ if (entry.isDirectory() && safeSlug(entry.name)) dirs.push(path.join(AI_ROOT, entry.name));
95
+ }
96
+ }
97
+ if (projects) {
98
+ for (const [slug] of Object.entries(projects)) {
99
+ if (safeSlug(slug)) dirs.push(projectAIPath(slug));
100
+ }
101
+ }
102
+ return [...new Set(dirs.map(d => path.resolve(d)))];
103
+ }
104
+
105
+ module.exports = {
106
+ APP_ROOT,
107
+ SITE_ROOT,
108
+ AI_ROOT,
109
+ STANDARD_SUBDIRS,
110
+ projectAIPath,
111
+ ensureProjectAIPath,
112
+ legacyAIPath,
113
+ migrateAIWorkspace,
114
+ runPath,
115
+ draftDir,
116
+ contextPackDir,
117
+ findExistingRunPath,
118
+ findExistingDraftDir,
119
+ listProjectDirs,
120
+ };
@@ -15,6 +15,33 @@ const { getAdapter } = require('./ai-adapter');
15
15
  const { buildContextPack } = require('./context-pack-builder');
16
16
  const { execGit } = require('./git-runner');
17
17
  const { scanProject } = require('./scanner');
18
+ const aiWorkspace = require('./ai-workspace');
19
+ const { isV3Kb, frontmatter } = require('./kb-v3');
20
+
21
+ const AI_PROFILES_PATH = path.resolve(__dirname, '..', '..', 'ai-profiles.json');
22
+
23
+ function readAiProfiles() {
24
+ try {
25
+ const cfg = JSON.parse(fs.readFileSync(AI_PROFILES_PATH, 'utf-8'));
26
+ return Array.isArray(cfg.profiles) ? cfg.profiles : [];
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
31
+
32
+ function validateUsableProfile(profileId) {
33
+ const profile = readAiProfiles().find(item => item && item.id === profileId);
34
+ if (!profile && profileId === 'mock-agent') {
35
+ const mockAdapter = getAdapter('mock-agent');
36
+ if (mockAdapter) return { ok: true, adapter: mockAdapter };
37
+ }
38
+ if (!profile) return { ok: false, status: 400, error: `AI profile not configured: ${profileId}` };
39
+ const implementation = profile.implementation || profile.id;
40
+ const adapter = getAdapter(implementation);
41
+ if (!adapter) return { ok: false, status: 400, error: `unknown adapter: ${implementation}` };
42
+ if (profile.enabled === false) return { ok: false, status: 400, error: `AI profile disabled: ${profileId}` };
43
+ return { ok: true, adapter };
44
+ }
18
45
 
19
46
  function readTemplate(name) {
20
47
  const p = path.join(__dirname, '..', '..', 'templates', name);
@@ -89,6 +116,25 @@ function labels(project) {
89
116
  };
90
117
  }
91
118
 
119
+ function isV3Project(project, kbPath) {
120
+ return project.kbSchemaVersion === 'v3' || isV3Kb(kbPath);
121
+ }
122
+
123
+ function sourceMeta(project, headCommitAtRun = null) {
124
+ return {
125
+ sourceBranch: project.currentBranch || null,
126
+ sourceDefaultBranch: project.defaultBranch || null,
127
+ sourceRemote: project.remoteUrl || null,
128
+ sourceRunId: null,
129
+ sourceHeadCommit: headCommitAtRun || project.headCommit || null,
130
+ };
131
+ }
132
+
133
+ function prependDraftFrontmatter(text, meta) {
134
+ const clean = String(text || '').replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
135
+ return `${frontmatter(meta)}${clean}`;
136
+ }
137
+
92
138
  function renderGoalDraft(project, draft) {
93
139
  const tpl = readTemplate('project-goal.md');
94
140
  if (!tpl) return null;
@@ -160,15 +206,19 @@ async function runInitialAnalysis(project, options = {}) {
160
206
  const slug = project.slug;
161
207
  const kbPath = path.resolve(project.kbPath);
162
208
  const aiProfileId = project.aiProfileId || 'mock-agent';
163
- const adapter = getAdapter(aiProfileId);
164
- if (!adapter) return { ok: false, status: 400, error: `unknown adapter: ${aiProfileId}` };
209
+ const profileCheck = validateUsableProfile(aiProfileId);
210
+ if (!profileCheck.ok) return profileCheck;
211
+ const adapter = profileCheck.adapter;
165
212
  if (!fs.existsSync(kbPath)) return { ok: false, status: 400, error: 'project KB not initialized' };
213
+ const v3 = isV3Project(project, kbPath);
166
214
 
167
215
  const runId = `initial-${shortRunId(Date.now().toString(36) + Math.random().toString(36))}`;
168
- const draftsDir = path.join(kbPath, '_ai', 'drafts', runId);
169
- const runsDir = path.join(kbPath, '_ai', 'runs');
216
+ const aiRoot = v3 ? aiWorkspace.ensureProjectAIPath(slug) : path.join(kbPath, '_ai');
217
+ const draftsDir = path.join(aiRoot, 'drafts', runId);
218
+ const runsDir = path.join(aiRoot, 'runs');
170
219
  fs.mkdirSync(draftsDir, { recursive: true });
171
220
  fs.mkdirSync(runsDir, { recursive: true });
221
+ const meta = sourceMeta(project);
172
222
 
173
223
  const runRecord = {
174
224
  schema: 'ai-run/v1',
@@ -183,6 +233,9 @@ async function runInitialAnalysis(project, options = {}) {
183
233
  drafts: [],
184
234
  goalAccepted: false,
185
235
  headCommitAtRun: project.headCommit || null,
236
+ sourceBranch: meta.sourceBranch,
237
+ sourceDefaultBranch: meta.sourceDefaultBranch,
238
+ sourceRemote: meta.sourceRemote,
186
239
  };
187
240
 
188
241
  try {
@@ -193,7 +246,7 @@ async function runInitialAnalysis(project, options = {}) {
193
246
  trigger: 'initial',
194
247
  commits: [],
195
248
  });
196
- runRecord.contextPackPath = path.relative(kbPath, path.join(kbPath, '_ai', 'context-packs', runId, 'context-pack.json'));
249
+ runRecord.contextPackPath = path.relative(aiRoot, path.join(aiRoot, 'context-packs', runId, 'context-pack.json')).replace(/\\/g, '/');
197
250
 
198
251
  // 2. Run analyzer
199
252
  const output = await adapter.analyzeInitialProject({ project, contextPack: pack });
@@ -214,18 +267,20 @@ async function runInitialAnalysis(project, options = {}) {
214
267
  const analysisText = renderAnalysisDraft(project, output.analysisDraft || {});
215
268
  const featureTexts = (output.features || []).map(f => ({ slug: f.slug, text: renderFeatureDraft(project, f) }));
216
269
 
217
- const goalPath = path.join(draftsDir, 'project-goal.md');
218
- const analysisPath = path.join(draftsDir, 'project-analysis.md');
219
- fs.writeFileSync(goalPath, goalText || '', 'utf-8');
220
- fs.writeFileSync(analysisPath, analysisText || '', 'utf-8');
221
- runRecord.drafts.push({ op: 'create-file', path: 'project-goal.md', fromDraft: 'goalDraft' });
222
- runRecord.drafts.push({ op: 'create-file', path: 'project-analysis.md', fromDraft: 'analysisDraft' });
270
+ const draftMeta = { ...meta, sourceRunId: runId, sourceHeadCommit: runRecord.headCommitAtRun };
271
+ const goalRel = v3 ? 'GOAL.md' : 'project-goal.md';
272
+ const analysisRel = v3 ? 'ARCHITECTURE.md' : 'project-analysis.md';
273
+ fs.writeFileSync(path.join(draftsDir, goalRel), prependDraftFrontmatter(goalText || `# ${slug} Goal\n`, draftMeta), 'utf-8');
274
+ fs.writeFileSync(path.join(draftsDir, analysisRel), prependDraftFrontmatter(analysisText || `# ${slug} Architecture\n`, draftMeta), 'utf-8');
275
+ runRecord.drafts.push({ op: 'create-file', path: goalRel, fromDraft: 'goalDraft', sourceBranch: meta.sourceBranch });
276
+ runRecord.drafts.push({ op: 'create-file', path: analysisRel, fromDraft: 'analysisDraft', sourceBranch: meta.sourceBranch });
223
277
 
224
278
  for (const f of featureTexts) {
225
- const featurePath = path.join(draftsDir, 'features', `${f.slug}.md`);
279
+ const rel = v3 ? `modules/${f.slug}.md` : `features/${f.slug}.md`;
280
+ const featurePath = path.join(draftsDir, rel);
226
281
  fs.mkdirSync(path.dirname(featurePath), { recursive: true });
227
- fs.writeFileSync(featurePath, f.text || '', 'utf-8');
228
- runRecord.drafts.push({ op: 'create-file', path: `features/${f.slug}.md`, fromDraft: 'feature' });
282
+ fs.writeFileSync(featurePath, prependDraftFrontmatter(f.text || '', draftMeta), 'utf-8');
283
+ runRecord.drafts.push({ op: 'create-file', path: rel, fromDraft: v3 ? 'module' : 'feature', sourceBranch: meta.sourceBranch });
229
284
  }
230
285
  runRecord.outputPaths = runRecord.drafts.map(d => d.path);
231
286
 
@@ -278,9 +333,11 @@ async function runCommitAnalysis(project, options = {}) {
278
333
  const slug = project.slug;
279
334
  const kbPath = path.resolve(project.kbPath);
280
335
  const aiProfileId = project.aiProfileId || 'mock-agent';
281
- const adapter = getAdapter(aiProfileId);
282
- if (!adapter) return { ok: false, status: 400, error: `unknown adapter: ${aiProfileId}` };
336
+ const profileCheck = validateUsableProfile(aiProfileId);
337
+ if (!profileCheck.ok) return profileCheck;
338
+ const adapter = profileCheck.adapter;
283
339
  if (!fs.existsSync(kbPath)) return { ok: false, status: 400, error: 'project KB not initialized' };
340
+ const v3 = isV3Project(project, kbPath);
284
341
 
285
342
  // Run the scanner to discover pending commits since lastAnalyzedCommit.
286
343
  const scan = await scanProject({ slug, ...project }, { maxCommits: options.maxCommits || 200 });
@@ -292,10 +349,12 @@ async function runCommitAnalysis(project, options = {}) {
292
349
  }
293
350
 
294
351
  const runId = `commits-${shortRunId(Date.now().toString(36) + Math.random().toString(36))}`;
295
- const draftsDir = path.join(kbPath, '_ai', 'drafts', runId);
296
- const runsDir = path.join(kbPath, '_ai', 'runs');
352
+ const aiRoot = v3 ? aiWorkspace.ensureProjectAIPath(slug) : path.join(kbPath, '_ai');
353
+ const draftsDir = path.join(aiRoot, 'drafts', runId);
354
+ const runsDir = path.join(aiRoot, 'runs');
297
355
  fs.mkdirSync(draftsDir, { recursive: true });
298
356
  fs.mkdirSync(runsDir, { recursive: true });
357
+ const meta = sourceMeta(project, scan.headCommit || null);
299
358
 
300
359
  const runRecord = {
301
360
  schema: 'ai-run/v1',
@@ -311,6 +370,9 @@ async function runCommitAnalysis(project, options = {}) {
311
370
  commitCount: scan.commits.length,
312
371
  headCommitAtRun: scan.headCommit || null,
313
372
  lastAnalyzedCommitBefore: project.lastAnalyzedCommit || null,
373
+ sourceBranch: meta.sourceBranch,
374
+ sourceDefaultBranch: meta.sourceDefaultBranch,
375
+ sourceRemote: meta.sourceRemote,
314
376
  drafts: [],
315
377
  outputPaths: [],
316
378
  };
@@ -323,7 +385,7 @@ async function runCommitAnalysis(project, options = {}) {
323
385
  trigger: 'commits',
324
386
  commits: scan.commits,
325
387
  });
326
- runRecord.contextPackPath = path.relative(kbPath, path.join(kbPath, '_ai', 'context-packs', runId, 'context-pack.json'));
388
+ runRecord.contextPackPath = path.relative(aiRoot, path.join(aiRoot, 'context-packs', runId, 'context-pack.json')).replace(/\\/g, '/');
327
389
 
328
390
  // 2. Run analyzer
329
391
  const output = await adapter.analyzeCommitBatch({ project, commits: scan.commits, contextPack: pack });
@@ -350,11 +412,12 @@ async function runCommitAnalysis(project, options = {}) {
350
412
  const shortCommit = (commit.short || commit.hash || '').slice(0, 7);
351
413
  const draftChangePath = path.join(draftsDir, 'changes', `${shortCommit}.md`);
352
414
  fs.mkdirSync(path.dirname(draftChangePath), { recursive: true });
353
- fs.writeFileSync(draftChangePath, renderChangeDraft(project, change, commit), 'utf-8');
354
- runRecord.drafts.push({ op: 'create-file', path: `changes/${shortCommit}.md`, fromDraft: 'change' });
415
+ const draftMeta = { ...meta, sourceRunId: runId, sourceHeadCommit: runRecord.headCommitAtRun };
416
+ fs.writeFileSync(draftChangePath, prependDraftFrontmatter(renderChangeDraft(project, change, commit), draftMeta), 'utf-8');
417
+ runRecord.drafts.push({ op: 'create-file', path: `changes/${shortCommit}.md`, fromDraft: 'change', sourceBranch: meta.sourceBranch });
355
418
  runRecord.outputPaths.push(`changes/${shortCommit}.md`);
356
419
 
357
- if (change.classification === 'new-feature') {
420
+ if (!v3 && change.classification === 'new-feature') {
358
421
  const featureSlug = featureSlugFromSubject(commit.subject);
359
422
  const featurePath = path.join(draftsDir, 'features', `${featureSlug}.md`);
360
423
  fs.mkdirSync(path.dirname(featurePath), { recursive: true });
@@ -364,8 +427,8 @@ async function runCommitAnalysis(project, options = {}) {
364
427
  ? `${shortCommit} 引入的新功能:${commit.subject}`
365
428
  : `New feature introduced by ${shortCommit}: ${commit.subject}`,
366
429
  });
367
- fs.writeFileSync(featurePath, featureText, 'utf-8');
368
- runRecord.drafts.push({ op: 'create-file', path: `features/${featureSlug}.md`, fromDraft: 'feature' });
430
+ fs.writeFileSync(featurePath, prependDraftFrontmatter(featureText, draftMeta), 'utf-8');
431
+ runRecord.drafts.push({ op: 'create-file', path: `features/${featureSlug}.md`, fromDraft: 'feature', sourceBranch: meta.sourceBranch });
369
432
  runRecord.outputPaths.push(`features/${featureSlug}.md`);
370
433
  } else if (change.classification === 'refactor' || change.classification === 'infrastructure') {
371
434
  // Note a goal-impact line so the reviewer can see this changed the implementation shape.
@@ -396,28 +459,50 @@ function writeRun(runsDir, runRecord) {
396
459
  }
397
460
 
398
461
  function readRun(kbPath, runId) {
399
- const p = path.join(kbPath, '_ai', 'runs', `${runId}.json`);
462
+ const slug = path.basename(kbPath);
463
+ const p = aiWorkspace.findExistingRunPath({ slug, kbPath, runId });
400
464
  if (!fs.existsSync(p)) return null;
401
465
  return JSON.parse(fs.readFileSync(p, 'utf-8'));
402
466
  }
403
467
 
404
468
  function listRuns(kbPath) {
405
- const runsDir = path.join(kbPath, '_ai', 'runs');
406
- if (!fs.existsSync(runsDir)) return [];
407
- return fs.readdirSync(runsDir).filter(f => f.endsWith('.json')).map(f => {
408
- try { return JSON.parse(fs.readFileSync(path.join(runsDir, f), 'utf-8')); } catch { return null; }
409
- }).filter(Boolean);
469
+ const slug = path.basename(kbPath);
470
+ const dirs = [path.join(aiWorkspace.projectAIPath(slug), 'runs'), path.join(kbPath, '_ai', 'runs')];
471
+ const byId = new Map();
472
+ for (const runsDir of dirs) {
473
+ if (!fs.existsSync(runsDir)) continue;
474
+ for (const f of fs.readdirSync(runsDir).filter(file => file.endsWith('.json'))) {
475
+ try {
476
+ const run = JSON.parse(fs.readFileSync(path.join(runsDir, f), 'utf-8'));
477
+ byId.set(run.runId || f, run);
478
+ } catch {}
479
+ }
480
+ }
481
+ return [...byId.values()];
410
482
  }
411
483
 
412
484
  function listDrafts(kbPath, runId) {
413
- const dir = path.join(kbPath, '_ai', 'drafts', runId);
485
+ const slug = path.basename(kbPath);
486
+ const run = readRun(kbPath, runId) || {};
487
+ const dir = aiWorkspace.findExistingDraftDir({ slug, kbPath, runId });
414
488
  if (!fs.existsSync(dir)) return [];
415
489
  const out = [];
416
490
  const walk = (d) => {
417
491
  for (const e of fs.readdirSync(d, { withFileTypes: true })) {
418
492
  const full = path.join(d, e.name);
419
493
  if (e.isDirectory()) walk(full);
420
- else out.push({ path: path.relative(dir, full).replace(/\\/g, '/'), size: fs.statSync(full).size });
494
+ else {
495
+ const rel = path.relative(dir, full).replace(/\\/g, '/');
496
+ out.push({
497
+ path: rel,
498
+ size: fs.statSync(full).size,
499
+ sourceBranch: run.sourceBranch ?? 'unknown (pre-TASK-016)',
500
+ sourceDefaultBranch: run.sourceDefaultBranch ?? null,
501
+ sourceRemote: run.sourceRemote ?? null,
502
+ sourceRunId: run.runId || runId,
503
+ sourceHeadCommit: run.headCommitAtRun || null,
504
+ });
505
+ }
421
506
  }
422
507
  };
423
508
  walk(dir);