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.
- package/CHANGELOG.md +41 -0
- package/README.md +201 -58
- package/_site/_test/ai-profile-test.js +59 -1
- package/_site/_test/baseline-schema-test.js +4 -3
- package/_site/_test/claude-workbench-test.js +72 -0
- package/_site/_test/draft-apply-test.js +12 -6
- package/_site/_test/kb-v2-templates-test.js +31 -43
- package/_site/_test/knowledge-store-logs-supervision-test.js +143 -0
- package/_site/_test/package-startup-test.js +108 -0
- package/_site/_test/project-control-panel-task14-test.js +151 -0
- package/_site/_test/task15-20-integration-test.js +194 -0
- package/_site/_test/task15-20-ui-flow-test.js +144 -0
- package/_site/_test/ui-smoke-test.js +2 -2
- package/_site/index.html +1640 -90
- package/_site/lib/ai-adapter.js +3 -3
- package/_site/lib/ai-workspace.js +120 -0
- package/_site/lib/analysis-orchestrator.js +117 -32
- package/_site/lib/claude-cli-runner.js +862 -0
- package/_site/lib/context-pack-builder.js +19 -11
- package/_site/lib/draft-apply.js +80 -31
- package/_site/lib/index-builder.js +100 -0
- package/_site/lib/job-orchestrator.js +15 -11
- package/_site/lib/kb-v3.js +188 -0
- package/_site/lib/kb-validator.js +84 -0
- package/_site/lib/knowledge-store.js +141 -0
- package/_site/lib/llm-client.js +103 -56
- package/_site/lib/prompt-registry.js +102 -0
- package/_site/lib/structured-logger.js +120 -0
- package/_site/lib/supervision.js +103 -0
- package/_site/server.js +887 -30
- package/_site/vendor/tailwind-browser.js +947 -0
- package/_site/vendor/vue.global.prod.js +9 -0
- package/ai-profiles.json +13 -3
- package/bin/project-knowledge.js +51 -0
- package/docs/development-progress.md +141 -0
- package/package.json +11 -2
- package/scripts/gen-commit-doc.ps1 +1 -1
- package/scripts/list-features.ps1 +1 -1
- package/scripts/register-scheduled-task.bat +3 -1
package/_site/lib/ai-adapter.js
CHANGED
|
@@ -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
|
|
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
|
|
164
|
-
if (!
|
|
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
|
|
169
|
-
const
|
|
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(
|
|
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
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
fs.writeFileSync(
|
|
221
|
-
|
|
222
|
-
runRecord.drafts.push({ op: 'create-file', path:
|
|
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
|
|
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:
|
|
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
|
|
282
|
-
if (!
|
|
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
|
|
296
|
-
const
|
|
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(
|
|
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
|
-
|
|
354
|
-
|
|
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
|
|
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
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
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
|
|
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);
|