hive-lite 0.1.0
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.md +443 -0
- package/bin/hive.js +6 -0
- package/docs/cli-semantics.md +386 -0
- package/docs/skills/hive-lite-finish/SKILL.md +282 -0
- package/docs/skills/hive-lite-finish/agents/openai.yaml +4 -0
- package/docs/skills/hive-lite-finish/references/safety.md +95 -0
- package/docs/skills/hive-lite-finish/references/verdicts.md +123 -0
- package/docs/skills/hive-lite-map-maintainer/SKILL.md +203 -0
- package/docs/skills/hive-lite-map-maintainer/agents/openai.yaml +7 -0
- package/docs/skills/hive-lite-map-maintainer/references/lifecycle.md +114 -0
- package/docs/skills/hive-lite-map-maintainer/references/repair-rules.md +201 -0
- package/docs/skills/hive-lite-start-prompt/SKILL.md +283 -0
- package/docs/skills/hive-lite-start-prompt/agents/openai.yaml +4 -0
- package/docs/skills/hive-lite-start-prompt/references/input-calibration.md +82 -0
- package/docs/skills/hive-lite-start-prompt/references/preflight.md +116 -0
- package/package.json +40 -0
- package/src/cli.js +910 -0
- package/src/lib/change.js +642 -0
- package/src/lib/context.js +1104 -0
- package/src/lib/evidence.js +230 -0
- package/src/lib/fsx.js +54 -0
- package/src/lib/git.js +128 -0
- package/src/lib/glob.js +47 -0
- package/src/lib/health.js +1012 -0
- package/src/lib/id.js +13 -0
- package/src/lib/map.js +713 -0
- package/src/lib/next.js +341 -0
- package/src/lib/risk.js +122 -0
- package/src/lib/roles.js +109 -0
- package/src/lib/scope.js +168 -0
- package/src/lib/skills.js +349 -0
- package/src/lib/status.js +344 -0
- package/src/lib/yaml.js +223 -0
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { createId } = require('./id');
|
|
4
|
+
const { ensureDir, exists, readJson, writeJson, writeText } = require('./fsx');
|
|
5
|
+
const { currentBranch, currentHead, grep } = require('./git');
|
|
6
|
+
const { matchesPattern } = require('./glob');
|
|
7
|
+
const { hiveDir, loadProjectMap } = require('./map');
|
|
8
|
+
const {
|
|
9
|
+
inferRoleFromPath,
|
|
10
|
+
isInternalBehaviorRole,
|
|
11
|
+
isUiManualRole,
|
|
12
|
+
normalizeRole,
|
|
13
|
+
} = require('./roles');
|
|
14
|
+
const { normalizeAreaScope, patternDisplay } = require('./scope');
|
|
15
|
+
|
|
16
|
+
const DECOMPOSITION_LIMITS = {
|
|
17
|
+
relevantFilesWarning: 12,
|
|
18
|
+
relevantFilesBlocking: 20,
|
|
19
|
+
writableFilesWarning: 8,
|
|
20
|
+
writableFilesBlocking: 12,
|
|
21
|
+
highAreaScore: 6,
|
|
22
|
+
areaRatio: 1.5,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const AREA_NAMESPACE_TERMS = {
|
|
26
|
+
dashboard: ['dashboard', 'ui', 'frontend', 'view'],
|
|
27
|
+
server: ['server', 'api', 'backend'],
|
|
28
|
+
contracts: ['contract', 'contracts', 'schema'],
|
|
29
|
+
cli: ['cli', 'command', 'terminal'],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function normalize(value) {
|
|
33
|
+
return String(value || '').toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tokenize(value) {
|
|
37
|
+
const lower = normalize(value);
|
|
38
|
+
const latin = lower.match(/[a-z0-9][a-z0-9_-]{1,}/g) || [];
|
|
39
|
+
const cjk = lower.match(/[\u4e00-\u9fff]{2,}/g) || [];
|
|
40
|
+
return [...new Set([...latin, ...cjk])];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function basenameTokens(file) {
|
|
44
|
+
return tokenize(path.basename(file || '').replace(/([a-z])([A-Z])/g, '$1 $2'));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function areaNamespace(area) {
|
|
48
|
+
return normalize(area && area.id).split(/[._:-]/).filter(Boolean)[0] || '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validationPlanForArea(map, area) {
|
|
52
|
+
const profileIds = ((area.validation || {}).profiles || []).filter(Boolean);
|
|
53
|
+
const profilesById = new Map(map.validationProfiles.map((profile) => [profile.id, profile]));
|
|
54
|
+
const selected = profileIds
|
|
55
|
+
.map((id) => profilesById.get(id))
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.map((profile) => ({
|
|
58
|
+
profile: profile.id,
|
|
59
|
+
command: profile.command || null,
|
|
60
|
+
type: profile.type || (profile.command ? 'command' : 'manual'),
|
|
61
|
+
required: profile.required !== false && profile.type !== 'manual',
|
|
62
|
+
description: profile.description || '',
|
|
63
|
+
instructions: profile.instructions || [],
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
if (selected.length === 0 && map.project.defaults && map.project.defaults.validation_cmd) {
|
|
67
|
+
selected.push({
|
|
68
|
+
profile: 'baseline',
|
|
69
|
+
command: map.project.defaults.validation_cmd,
|
|
70
|
+
type: 'command',
|
|
71
|
+
required: true,
|
|
72
|
+
description: 'Baseline project validation.',
|
|
73
|
+
instructions: [],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return selected;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function scoreArea(root, area, intent, tokens, options = {}) {
|
|
80
|
+
let score = 0;
|
|
81
|
+
const signals = [];
|
|
82
|
+
const areaText = normalize([
|
|
83
|
+
area.id,
|
|
84
|
+
area.name,
|
|
85
|
+
area.description,
|
|
86
|
+
...(area.aliases || []),
|
|
87
|
+
...(area.concepts || []),
|
|
88
|
+
].join(' '));
|
|
89
|
+
|
|
90
|
+
if (options.area && area.id === options.area) {
|
|
91
|
+
score += 20;
|
|
92
|
+
signals.push(`forced area: ${area.id}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const namespace = areaNamespace(area);
|
|
96
|
+
const namespaceTerms = AREA_NAMESPACE_TERMS[namespace] || (namespace ? [namespace] : []);
|
|
97
|
+
const matchedNamespaceTerms = namespaceTerms.filter((term) => tokens.includes(term));
|
|
98
|
+
if (matchedNamespaceTerms.length > 0) {
|
|
99
|
+
score += 6;
|
|
100
|
+
signals.push(`area namespace: ${namespace} (${matchedNamespaceTerms.join(', ')})`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (const alias of area.aliases || []) {
|
|
104
|
+
const normalized = normalize(alias);
|
|
105
|
+
if (normalized && normalize(intent).includes(normalized)) {
|
|
106
|
+
score += normalized.includes(' ') ? 8 : 6;
|
|
107
|
+
signals.push(`alias: ${alias}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const concept of area.concepts || []) {
|
|
112
|
+
const normalized = normalize(concept);
|
|
113
|
+
if (normalized && normalize(intent).includes(normalized)) {
|
|
114
|
+
score += 4;
|
|
115
|
+
signals.push(`concept: ${concept}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const token of tokens) {
|
|
120
|
+
if (areaText.includes(token)) {
|
|
121
|
+
score += 2;
|
|
122
|
+
signals.push(`token: ${token}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const entry of area.entrypoints || []) {
|
|
127
|
+
const fileTokens = basenameTokens(entry.path);
|
|
128
|
+
const matched = fileTokens.filter((token) => tokens.includes(token));
|
|
129
|
+
if (matched.length > 0) {
|
|
130
|
+
score += 5 + matched.length;
|
|
131
|
+
signals.push(`entrypoint: ${entry.path}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { area, score, signals };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function grepHints(root, tokens, maxFiles, options = {}) {
|
|
139
|
+
const fileScores = new Map();
|
|
140
|
+
for (const token of tokens.filter((item) => /^[a-z0-9_-]{3,}$/.test(item)).slice(0, 6)) {
|
|
141
|
+
for (const hit of grep(root, token, 30)) {
|
|
142
|
+
if (hit.path.startsWith('.hive/') || hit.path.includes('node_modules/')) continue;
|
|
143
|
+
if (options.allowedPatterns && options.allowedPatterns.length > 0) {
|
|
144
|
+
if (!options.allowedPatterns.some((pattern) => matchesPattern(hit.path, pattern))) continue;
|
|
145
|
+
}
|
|
146
|
+
const prev = fileScores.get(hit.path) || { path: hit.path, score: 0, hits: [] };
|
|
147
|
+
prev.score += 1;
|
|
148
|
+
if (prev.hits.length < 3) prev.hits.push({ token, line: hit.line, text: hit.text });
|
|
149
|
+
fileScores.set(hit.path, prev);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [...fileScores.values()]
|
|
153
|
+
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
154
|
+
.slice(0, maxFiles);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function confidenceFor(top, second) {
|
|
158
|
+
if (!top || top.score <= 0) return 'low';
|
|
159
|
+
if (top.score >= 10 && (!second || top.score > second.score * 1.5)) return 'high';
|
|
160
|
+
if (top.score >= 6) return 'medium';
|
|
161
|
+
return 'low';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function candidateAreasFromScored(scored) {
|
|
165
|
+
return scored
|
|
166
|
+
.filter((item) => item.score > 0)
|
|
167
|
+
.slice(0, 4)
|
|
168
|
+
.map((item) => ({
|
|
169
|
+
id: item.area.id,
|
|
170
|
+
name: item.area.name || '',
|
|
171
|
+
score: item.score,
|
|
172
|
+
signals: item.signals.slice(0, 6),
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function riskLevelFor(area) {
|
|
177
|
+
return area && area.risk && area.risk.default_level ? area.risk.default_level : 'low';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function roleForEntry(entry) {
|
|
181
|
+
return normalizeRole(entry.role) || inferRoleFromPath(entry.path);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function rolesForArea(area) {
|
|
185
|
+
return [...new Set((area.entrypoints || []).map(roleForEntry).filter(Boolean))];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function evidenceClassesForRoles(roles) {
|
|
189
|
+
const classes = new Set();
|
|
190
|
+
for (const role of roles) {
|
|
191
|
+
if (isUiManualRole(role)) classes.add('direct_manual_verifiable');
|
|
192
|
+
else if (role === 'api_contract' || role === 'schema_logic') classes.add('schema_or_contract');
|
|
193
|
+
else if (isInternalBehaviorRole(role)) classes.add('internal_logic');
|
|
194
|
+
else if (role === 'config') classes.add('config_review');
|
|
195
|
+
else if (role === 'test') classes.add('test_change');
|
|
196
|
+
else classes.add('unknown_policy');
|
|
197
|
+
}
|
|
198
|
+
return [...classes];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validationSurfaceForProfile(profile) {
|
|
202
|
+
if (normalize(profile.type) === 'manual') return null;
|
|
203
|
+
const id = String(profile.profile || profile.id || '').toLowerCase();
|
|
204
|
+
if (!id) return 'unknown';
|
|
205
|
+
const first = id.split(/[._:-]/).filter(Boolean)[0];
|
|
206
|
+
return first || id;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function validationSurfacesForArea(map, area) {
|
|
210
|
+
const profilesById = new Map(map.validationProfiles.map((profile) => [profile.id, profile]));
|
|
211
|
+
return ((area.validation || {}).profiles || [])
|
|
212
|
+
.map((id) => profilesById.get(id) || { id })
|
|
213
|
+
.map(validationSurfaceForProfile)
|
|
214
|
+
.filter(Boolean);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function splitDir(root) {
|
|
218
|
+
return path.join(hiveDir(root), 'context', 'splits');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function splitJsonPath(root, splitId) {
|
|
222
|
+
return path.join(splitDir(root), `${splitId}.json`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function splitMarkdownPath(root, splitId) {
|
|
226
|
+
return path.join(splitDir(root), `${splitId}.md`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function phaseIdForArea(areaId) {
|
|
230
|
+
return `phase_${String(areaId || 'unknown').replace(/[^a-zA-Z0-9]+/g, '_').replace(/^_+|_+$/g, '').toLowerCase()}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function relativePath(root, file) {
|
|
234
|
+
return path.relative(root, file).replace(/\\/g, '/');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function shellQuote(value) {
|
|
238
|
+
const text = String(value || '');
|
|
239
|
+
if (/^[A-Za-z0-9_/:.,@%+=-]+$/.test(text)) return text;
|
|
240
|
+
return `'${text.replace(/'/g, "'\\''")}'`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function commandLine(parts) {
|
|
244
|
+
return parts.map(shellQuote).join(' ');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeCommandArgv(value) {
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
const parts = value.map((item) => String(item || '')).filter(Boolean);
|
|
250
|
+
if (parts.length > 0) return parts;
|
|
251
|
+
}
|
|
252
|
+
return ['hive-lite'];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function sameArgv(left, right) {
|
|
256
|
+
if (left.length !== right.length) return false;
|
|
257
|
+
return left.every((item, index) => item === right[index]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function findCommandForPhase(argv, note, phase) {
|
|
261
|
+
return commandLine([
|
|
262
|
+
...argv,
|
|
263
|
+
'find',
|
|
264
|
+
phase.findIntent,
|
|
265
|
+
'--from-split',
|
|
266
|
+
note.id,
|
|
267
|
+
'--phase',
|
|
268
|
+
phase.id,
|
|
269
|
+
'--area',
|
|
270
|
+
phase.primaryAreaId,
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function intentHasMultiStepMarkers(intent) {
|
|
275
|
+
const text = normalize(intent);
|
|
276
|
+
return [
|
|
277
|
+
/同时|顺便|还要|以及|并且|一起|全局|整体|全栈|端到端|前后端|架构|迁移|改版|重构/,
|
|
278
|
+
/\b(both|across|between)\b[\s\S]{0,60}\b(places|surfaces|areas|components|commands|outputs)\b/,
|
|
279
|
+
/\b(share|same|shared)\b[\s\S]{0,60}\b(wording|copy|label|labels|output|display)\b/,
|
|
280
|
+
/\b(ui|frontend|dashboard)\b[\s\S]{0,40}\b(api|server|backend|schema|contract|database|db)\b/,
|
|
281
|
+
/\b(api|server|backend|schema|contract|database|db)\b[\s\S]{0,40}\b(ui|frontend|dashboard)\b/,
|
|
282
|
+
].some((pattern) => pattern.test(text));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function signal(code, message, data = {}) {
|
|
286
|
+
return {
|
|
287
|
+
code,
|
|
288
|
+
message,
|
|
289
|
+
severity: data.severity || 'blocking',
|
|
290
|
+
blocking: data.blocking !== false,
|
|
291
|
+
...data,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function highScoringAreas(scored) {
|
|
296
|
+
const topScore = scored[0] ? scored[0].score : 0;
|
|
297
|
+
return scored
|
|
298
|
+
.filter((item) => item.score >= DECOMPOSITION_LIMITS.highAreaScore)
|
|
299
|
+
.filter((item) => topScore === 0 || item.score >= topScore / 2)
|
|
300
|
+
.slice(0, 5);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function competingHighScoringAreas(scored) {
|
|
304
|
+
const highAreas = highScoringAreas(scored);
|
|
305
|
+
const topScore = highAreas[0] ? highAreas[0].score : 0;
|
|
306
|
+
if (highAreas.length < 2 || topScore <= 0) return highAreas;
|
|
307
|
+
return highAreas.filter((item, index) => (
|
|
308
|
+
index === 0 || topScore <= item.score * DECOMPOSITION_LIMITS.areaRatio
|
|
309
|
+
));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function decompositionSignals({ map, area, confidence, intent, relevant, scope, scored, validationPlan, constrainedAreaId }) {
|
|
313
|
+
if (!area) return [];
|
|
314
|
+
const signals = [];
|
|
315
|
+
const highAreas = highScoringAreas(scored)
|
|
316
|
+
.filter((item) => !constrainedAreaId || item.area.id === constrainedAreaId);
|
|
317
|
+
const competingAreas = competingHighScoringAreas(scored)
|
|
318
|
+
.filter((item) => !constrainedAreaId || item.area.id === constrainedAreaId);
|
|
319
|
+
|
|
320
|
+
if (competingAreas.length >= 2) {
|
|
321
|
+
signals.push(signal('MULTIPLE_HIGH_SCORING_AREAS', 'Multiple areas scored high enough that this intent may not fit one Context Packet.', {
|
|
322
|
+
areas: competingAreas.map((item) => ({ id: item.area.id, score: item.score })),
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (confidence === 'medium' && competingAreas.length >= 2) {
|
|
327
|
+
signals.push(signal('FIND_CONFIDENCE_AMBIGUOUS', 'Top area is not clearly separated from other candidate areas.', {
|
|
328
|
+
areas: competingAreas.map((item) => ({ id: item.area.id, score: item.score })),
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (relevant.length > DECOMPOSITION_LIMITS.relevantFilesBlocking) {
|
|
333
|
+
signals.push(signal('TOO_MANY_RELEVANT_FILES', 'Relevant files exceed the safe edit-context limit.', {
|
|
334
|
+
count: relevant.length,
|
|
335
|
+
threshold: DECOMPOSITION_LIMITS.relevantFilesBlocking,
|
|
336
|
+
}));
|
|
337
|
+
} else if (relevant.length > DECOMPOSITION_LIMITS.relevantFilesWarning) {
|
|
338
|
+
signals.push(signal('TOO_MANY_RELEVANT_FILES', 'Relevant files are high for a single Context Packet.', {
|
|
339
|
+
severity: 'warning',
|
|
340
|
+
blocking: false,
|
|
341
|
+
count: relevant.length,
|
|
342
|
+
threshold: DECOMPOSITION_LIMITS.relevantFilesWarning,
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (scope.writableDirect.length > DECOMPOSITION_LIMITS.writableFilesBlocking) {
|
|
347
|
+
signals.push(signal('TOO_MANY_WRITABLE_FILES', 'Direct writable files exceed the safe change boundary.', {
|
|
348
|
+
count: scope.writableDirect.length,
|
|
349
|
+
threshold: DECOMPOSITION_LIMITS.writableFilesBlocking,
|
|
350
|
+
}));
|
|
351
|
+
} else if (scope.writableDirect.length > DECOMPOSITION_LIMITS.writableFilesWarning) {
|
|
352
|
+
signals.push(signal('TOO_MANY_WRITABLE_FILES', 'Direct writable files are high for one Change Record.', {
|
|
353
|
+
severity: 'warning',
|
|
354
|
+
blocking: false,
|
|
355
|
+
count: scope.writableDirect.length,
|
|
356
|
+
threshold: DECOMPOSITION_LIMITS.writableFilesWarning,
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const areasForPolicy = competingAreas.length >= 2 ? competingAreas.map((item) => item.area) : [area];
|
|
361
|
+
const evidenceClasses = new Set();
|
|
362
|
+
for (const itemArea of areasForPolicy) {
|
|
363
|
+
for (const item of evidenceClassesForRoles(rolesForArea(itemArea))) evidenceClasses.add(item);
|
|
364
|
+
}
|
|
365
|
+
if (competingAreas.length >= 2 && evidenceClasses.size > 1) {
|
|
366
|
+
signals.push(signal('MULTIPLE_EVIDENCE_CLASSES', 'Candidate areas imply different evidence policies.', {
|
|
367
|
+
classes: [...evidenceClasses],
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const validationSurfaces = new Set(validationPlan.map(validationSurfaceForProfile).filter(Boolean));
|
|
372
|
+
if (competingAreas.length >= 2) {
|
|
373
|
+
for (const item of competingAreas.slice(1)) {
|
|
374
|
+
for (const surface of validationSurfacesForArea(map, item.area)) validationSurfaces.add(surface);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (validationSurfaces.size > 1) {
|
|
378
|
+
signals.push(signal('MULTIPLE_VALIDATION_SURFACES', 'The intent appears to involve more than one validation surface.', {
|
|
379
|
+
surfaces: [...validationSurfaces],
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const riskLevels = new Set(areasForPolicy.map(riskLevelFor));
|
|
384
|
+
if (riskLevels.size > 1) {
|
|
385
|
+
signals.push(signal('MIXED_RISK_LEVELS', 'Candidate areas have mixed risk levels.', {
|
|
386
|
+
levels: [...riskLevels],
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (intentHasMultiStepMarkers(intent) && highAreas.length >= 2) {
|
|
391
|
+
signals.push(signal('INTENT_HAS_MULTI_STEP_MARKERS', 'Intent wording suggests multiple phases or surfaces.', {
|
|
392
|
+
areas: highAreas.map((item) => ({ id: item.area.id, score: item.score })),
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return signals;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function candidatePhaseSeeds(scored, intent) {
|
|
400
|
+
return highScoringAreas(scored).map((item, index) => {
|
|
401
|
+
const area = item.area;
|
|
402
|
+
const roles = rolesForArea(area);
|
|
403
|
+
return {
|
|
404
|
+
id: phaseIdForArea(area.id),
|
|
405
|
+
order: (index + 1) * 10,
|
|
406
|
+
areaId: area.id,
|
|
407
|
+
areaName: area.name || '',
|
|
408
|
+
score: item.score,
|
|
409
|
+
phaseHint: area.name || area.id,
|
|
410
|
+
findIntent: `${intent} [focus: ${area.id}]`,
|
|
411
|
+
roles,
|
|
412
|
+
evidenceClasses: evidenceClassesForRoles(roles),
|
|
413
|
+
validationProfiles: ((area.validation || {}).profiles || []).filter(Boolean),
|
|
414
|
+
risk: riskLevelFor(area),
|
|
415
|
+
reason: item.signals.slice(0, 4).join(', '),
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function preconditionsForPhase(phase, allPhases) {
|
|
421
|
+
const sorted = allPhases.slice().sort((a, b) => (a.order || 0) - (b.order || 0));
|
|
422
|
+
const previous = sorted.filter((item) => (item.order || 0) < (phase.order || 0));
|
|
423
|
+
return {
|
|
424
|
+
requiredAcceptedPhases: previous.slice(-1).map((item) => item.id),
|
|
425
|
+
recommendedAfterPhases: previous.slice(0, -1).map((item) => item.id),
|
|
426
|
+
reason: previous.length
|
|
427
|
+
? 'Earlier phases are recommended because they may define assumptions for this phase.'
|
|
428
|
+
: 'This is the first recommended phase for the split note.',
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function buildSplitMarkdown(note) {
|
|
433
|
+
const currentArgv = normalizeCommandArgv(note.cliCommand && note.cliCommand.argv);
|
|
434
|
+
const packageAliasArgv = ['hive-lite'];
|
|
435
|
+
const lines = [
|
|
436
|
+
`# Hive Lite Split Note: ${note.id}`,
|
|
437
|
+
'',
|
|
438
|
+
'This note is a context artifact, not a task list or workflow state machine.',
|
|
439
|
+
'',
|
|
440
|
+
'## Original Intent',
|
|
441
|
+
note.source.originalIntent,
|
|
442
|
+
'',
|
|
443
|
+
'## Why This Was Split',
|
|
444
|
+
note.reason.summary,
|
|
445
|
+
'',
|
|
446
|
+
...note.reason.signals.map((item) => `- ${item}`),
|
|
447
|
+
'',
|
|
448
|
+
'## Phases',
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
for (const phase of note.phases) {
|
|
452
|
+
lines.push(
|
|
453
|
+
'',
|
|
454
|
+
`### ${phase.title}`,
|
|
455
|
+
'',
|
|
456
|
+
`Area: ${phase.primaryAreaId}`,
|
|
457
|
+
'',
|
|
458
|
+
'Run with this Hive Lite CLI invocation:',
|
|
459
|
+
'',
|
|
460
|
+
'```bash',
|
|
461
|
+
findCommandForPhase(currentArgv, note, phase),
|
|
462
|
+
'```',
|
|
463
|
+
'',
|
|
464
|
+
...(!sameArgv(currentArgv, packageAliasArgv) ? [
|
|
465
|
+
'If the package alias is installed:',
|
|
466
|
+
'',
|
|
467
|
+
'```bash',
|
|
468
|
+
findCommandForPhase(packageAliasArgv, note, phase),
|
|
469
|
+
'```',
|
|
470
|
+
'',
|
|
471
|
+
] : []),
|
|
472
|
+
'Expected Evidence:',
|
|
473
|
+
...(phase.expectedEvidence.length ? phase.expectedEvidence.map((item) => `- ${item}`) : ['- (not configured)']),
|
|
474
|
+
'',
|
|
475
|
+
'Non-goals:',
|
|
476
|
+
...(phase.nonGoals.length ? phase.nonGoals.map((item) => `- ${item}`) : ['- Do not combine this phase with other areas.']),
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
lines.push(
|
|
481
|
+
'',
|
|
482
|
+
'## Rule',
|
|
483
|
+
'',
|
|
484
|
+
'Each phase must independently run:',
|
|
485
|
+
'',
|
|
486
|
+
'```text',
|
|
487
|
+
'find -> check -> validate -> accept',
|
|
488
|
+
'```',
|
|
489
|
+
'',
|
|
490
|
+
'Do not combine phases into one coding prompt.',
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
return `${lines.join('\n')}\n`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function createSplitNote(root, packet, phaseSeeds, decomposition, options = {}) {
|
|
497
|
+
const splitId = createId('split');
|
|
498
|
+
ensureDir(splitDir(root));
|
|
499
|
+
const jsonPath = splitJsonPath(root, splitId);
|
|
500
|
+
const markdownPath = splitMarkdownPath(root, splitId);
|
|
501
|
+
const cliCommandArgv = normalizeCommandArgv(options.cliCommandArgv);
|
|
502
|
+
const note = {
|
|
503
|
+
schemaVersion: 'hive-lite/split-note/v1',
|
|
504
|
+
id: splitId,
|
|
505
|
+
createdAt: new Date().toISOString(),
|
|
506
|
+
cliCommand: {
|
|
507
|
+
kind: sameArgv(cliCommandArgv, ['hive-lite']) ? 'package_alias' : 'current_invocation',
|
|
508
|
+
argv: cliCommandArgv,
|
|
509
|
+
rendered: commandLine(cliCommandArgv),
|
|
510
|
+
},
|
|
511
|
+
source: {
|
|
512
|
+
kind: 'find',
|
|
513
|
+
contextPacketId: packet.id,
|
|
514
|
+
originalIntent: packet.intent.raw,
|
|
515
|
+
},
|
|
516
|
+
reason: {
|
|
517
|
+
mode: 'needs_decomposition',
|
|
518
|
+
signals: decomposition.map((item) => item.code),
|
|
519
|
+
summary: 'This intent cannot safely become one Context Packet and one Change Record.',
|
|
520
|
+
},
|
|
521
|
+
phases: phaseSeeds.map((phase) => ({
|
|
522
|
+
id: phase.id,
|
|
523
|
+
order: phase.order,
|
|
524
|
+
title: phase.phaseHint,
|
|
525
|
+
findIntent: phase.findIntent,
|
|
526
|
+
primaryAreaId: phase.areaId,
|
|
527
|
+
expectedEvidence: [
|
|
528
|
+
...(phase.validationProfiles || []).map((profile) => `validation:${profile}`),
|
|
529
|
+
...(phase.evidenceClasses || []).map((item) => `evidence:${item}`),
|
|
530
|
+
],
|
|
531
|
+
nonGoals: phaseSeeds
|
|
532
|
+
.filter((other) => other.areaId !== phase.areaId)
|
|
533
|
+
.map((other) => `Do not change ${other.areaId} in this phase.`),
|
|
534
|
+
preconditions: preconditionsForPhase(phase, phaseSeeds),
|
|
535
|
+
linkedContextIds: [],
|
|
536
|
+
linkedChangeIds: [],
|
|
537
|
+
})),
|
|
538
|
+
notes: [
|
|
539
|
+
'Each phase must rerun hive find.',
|
|
540
|
+
'Do not combine phases into one coding prompt.',
|
|
541
|
+
'Each phase should produce its own Change Record and acceptance evidence.',
|
|
542
|
+
],
|
|
543
|
+
};
|
|
544
|
+
writeJson(jsonPath, note);
|
|
545
|
+
writeText(markdownPath, buildSplitMarkdown(note));
|
|
546
|
+
return {
|
|
547
|
+
id: splitId,
|
|
548
|
+
jsonPath: relativePath(root, jsonPath),
|
|
549
|
+
markdownPath: relativePath(root, markdownPath),
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function acceptedPhasesForSplit(root, splitId) {
|
|
554
|
+
const dir = path.join(hiveDir(root), 'changes');
|
|
555
|
+
if (!exists(dir)) return new Map();
|
|
556
|
+
const accepted = new Map();
|
|
557
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
558
|
+
if (!entry.isDirectory() || !entry.name.startsWith('chg_')) continue;
|
|
559
|
+
const file = path.join(dir, entry.name, 'change.json');
|
|
560
|
+
if (!exists(file)) continue;
|
|
561
|
+
try {
|
|
562
|
+
const change = readJson(file);
|
|
563
|
+
const origin = change.originSplit || {};
|
|
564
|
+
if (origin.splitId !== splitId || !origin.phaseId) continue;
|
|
565
|
+
if (!change.humanDecision || change.humanDecision.status !== 'accepted') continue;
|
|
566
|
+
const current = accepted.get(origin.phaseId) || [];
|
|
567
|
+
current.push({
|
|
568
|
+
changeId: change.id,
|
|
569
|
+
commit: change.humanDecision.commit || null,
|
|
570
|
+
acceptedAt: change.humanDecision.decidedAt || null,
|
|
571
|
+
});
|
|
572
|
+
accepted.set(origin.phaseId, current);
|
|
573
|
+
} catch {
|
|
574
|
+
// Ignore malformed historical change records.
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return accepted;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function loadSplitNote(root, splitId) {
|
|
581
|
+
if (!splitId) return null;
|
|
582
|
+
const file = splitJsonPath(root, splitId);
|
|
583
|
+
if (!exists(file)) return null;
|
|
584
|
+
return readJson(file);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function phaseDependencyStatus(root, originSplit) {
|
|
588
|
+
if (!originSplit || !originSplit.splitId || !originSplit.phaseId) return null;
|
|
589
|
+
const note = loadSplitNote(root, originSplit.splitId);
|
|
590
|
+
if (!note || !Array.isArray(note.phases)) {
|
|
591
|
+
return {
|
|
592
|
+
canStartNormally: false,
|
|
593
|
+
splitFound: false,
|
|
594
|
+
phaseFound: false,
|
|
595
|
+
requiredAcceptedPhases: [],
|
|
596
|
+
recommendedAfterPhases: [],
|
|
597
|
+
missingRequiredAcceptedPhases: [],
|
|
598
|
+
readyRequiredAcceptedPhases: [],
|
|
599
|
+
missingRecommendedAfterPhases: [],
|
|
600
|
+
readyRecommendedAfterPhases: [],
|
|
601
|
+
message: 'Split note was not found; dependency status cannot be derived.',
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const phase = note.phases.find((item) => item.id === originSplit.phaseId);
|
|
605
|
+
if (!phase) {
|
|
606
|
+
return {
|
|
607
|
+
canStartNormally: false,
|
|
608
|
+
splitFound: true,
|
|
609
|
+
phaseFound: false,
|
|
610
|
+
requiredAcceptedPhases: [],
|
|
611
|
+
recommendedAfterPhases: [],
|
|
612
|
+
missingRequiredAcceptedPhases: [],
|
|
613
|
+
readyRequiredAcceptedPhases: [],
|
|
614
|
+
missingRecommendedAfterPhases: [],
|
|
615
|
+
readyRecommendedAfterPhases: [],
|
|
616
|
+
message: 'Phase was not found in the split note; dependency status cannot be derived.',
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
const accepted = acceptedPhasesForSplit(root, originSplit.splitId);
|
|
620
|
+
const preconditions = phase.preconditions || {};
|
|
621
|
+
const required = preconditions.requiredAcceptedPhases || [];
|
|
622
|
+
const recommended = preconditions.recommendedAfterPhases || [];
|
|
623
|
+
const readyRequired = required.filter((phaseId) => accepted.has(phaseId));
|
|
624
|
+
const missingRequired = required.filter((phaseId) => !accepted.has(phaseId));
|
|
625
|
+
const readyRecommended = recommended.filter((phaseId) => accepted.has(phaseId));
|
|
626
|
+
const missingRecommended = recommended.filter((phaseId) => !accepted.has(phaseId));
|
|
627
|
+
return {
|
|
628
|
+
canStartNormally: missingRequired.length === 0,
|
|
629
|
+
splitFound: true,
|
|
630
|
+
phaseFound: true,
|
|
631
|
+
requiredAcceptedPhases: required.map((phaseId) => ({
|
|
632
|
+
phaseId,
|
|
633
|
+
status: accepted.has(phaseId) ? 'accepted' : 'missing',
|
|
634
|
+
acceptedChangeIds: (accepted.get(phaseId) || []).map((item) => item.changeId),
|
|
635
|
+
})),
|
|
636
|
+
recommendedAfterPhases: recommended.map((phaseId) => ({
|
|
637
|
+
phaseId,
|
|
638
|
+
status: accepted.has(phaseId) ? 'accepted' : 'missing',
|
|
639
|
+
acceptedChangeIds: (accepted.get(phaseId) || []).map((item) => item.changeId),
|
|
640
|
+
})),
|
|
641
|
+
missingRequiredAcceptedPhases: missingRequired,
|
|
642
|
+
readyRequiredAcceptedPhases: readyRequired,
|
|
643
|
+
missingRecommendedAfterPhases: missingRecommended,
|
|
644
|
+
readyRecommendedAfterPhases: readyRecommended,
|
|
645
|
+
message: missingRequired.length
|
|
646
|
+
? `This phase has unmet required phase dependencies: ${missingRequired.join(', ')}.`
|
|
647
|
+
: 'Required phase dependencies are satisfied.',
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function originSplitFromOptions(options = {}) {
|
|
652
|
+
const splitId = options.fromSplit || options.fromsplit || null;
|
|
653
|
+
const phaseId = options.phase || null;
|
|
654
|
+
if (!splitId && !phaseId) return null;
|
|
655
|
+
return {
|
|
656
|
+
splitId: splitId && splitId !== true ? String(splitId) : null,
|
|
657
|
+
phaseId: phaseId && phaseId !== true ? String(phaseId) : null,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function phaseFromOriginSplit(root, originSplit) {
|
|
662
|
+
if (!originSplit || !originSplit.splitId || !originSplit.phaseId) return null;
|
|
663
|
+
const note = loadSplitNote(root, originSplit.splitId);
|
|
664
|
+
if (!note || !Array.isArray(note.phases)) return null;
|
|
665
|
+
return note.phases.find((item) => item.id === originSplit.phaseId) || null;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function validAreaId(map, areaId) {
|
|
669
|
+
if (!areaId || areaId === true) return null;
|
|
670
|
+
const normalized = String(areaId);
|
|
671
|
+
return map.areas.some((area) => area.id === normalized) ? normalized : null;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function requestedAreaId(options = {}) {
|
|
675
|
+
if (!options.area || options.area === true) return null;
|
|
676
|
+
return String(options.area);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function missingSplitReference(phaseStatus) {
|
|
680
|
+
return Boolean(phaseStatus && (phaseStatus.splitFound === false || phaseStatus.phaseFound === false));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function splitRoutingWarnings(originSplit, phaseStatus, originPhaseAreaId, requestedArea) {
|
|
684
|
+
const warnings = [];
|
|
685
|
+
if (phaseStatus && phaseStatus.splitFound === false) {
|
|
686
|
+
warnings.push({
|
|
687
|
+
code: 'SPLIT_NOTE_NOT_FOUND',
|
|
688
|
+
message: `Split note ${originSplit.splitId} was not found; this copied split command is not an edit permit.`,
|
|
689
|
+
});
|
|
690
|
+
} else if (phaseStatus && phaseStatus.phaseFound === false) {
|
|
691
|
+
warnings.push({
|
|
692
|
+
code: 'SPLIT_PHASE_NOT_FOUND',
|
|
693
|
+
message: `Phase ${originSplit.phaseId} was not found in split ${originSplit.splitId}; this copied split command is not an edit permit.`,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
if (originPhaseAreaId && requestedArea && requestedArea !== originPhaseAreaId) {
|
|
697
|
+
warnings.push({
|
|
698
|
+
code: 'SPLIT_PHASE_AREA_OVERRIDE',
|
|
699
|
+
message: `Ignoring --area ${requestedArea}; split phase ${originSplit.phaseId} requires ${originPhaseAreaId}.`,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
return warnings;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function scopeWarnings(scope) {
|
|
706
|
+
const warnings = [];
|
|
707
|
+
if (scope.writableDirect.length === 0) {
|
|
708
|
+
warnings.push({
|
|
709
|
+
code: 'MISSING_DIRECT_WRITABLE_SCOPE',
|
|
710
|
+
message: 'No narrow writable_direct scope is available.',
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
return warnings;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function reviewGatedNotices(scope) {
|
|
717
|
+
const notices = [];
|
|
718
|
+
for (const item of scope.writableConditional) {
|
|
719
|
+
notices.push({
|
|
720
|
+
code: 'CONDITIONAL_WRITABLE_SCOPE',
|
|
721
|
+
tier: 'conditional',
|
|
722
|
+
pattern: item.pattern,
|
|
723
|
+
message: `Conditional writable scope requires review: ${patternDisplay(item)}`,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
for (const item of scope.writableBroadFallback) {
|
|
727
|
+
notices.push({
|
|
728
|
+
code: 'BROAD_WRITABLE_SCOPE',
|
|
729
|
+
tier: 'broad_fallback',
|
|
730
|
+
pattern: item.pattern,
|
|
731
|
+
message: `Broad writable fallback requires review: ${patternDisplay(item)}`,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
return notices;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function contextMode(area, scope, validationPlan, relevant, decomposition = []) {
|
|
738
|
+
if (!area) return 'needs_map';
|
|
739
|
+
if (decomposition.some((item) => item.blocking)) return 'needs_decomposition';
|
|
740
|
+
if (scope.writableDirect.length === 0) return 'discovery_context';
|
|
741
|
+
if (relevant.length === 0 || !relevant.some((file) => file.source === 'project_map')) return 'discovery_context';
|
|
742
|
+
if (validationPlan.length === 0) return 'discovery_context';
|
|
743
|
+
return 'edit_context';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function recommendedActions(mode, intent, contextId, phaseStatus) {
|
|
747
|
+
const focus = JSON.stringify(intent);
|
|
748
|
+
if (missingSplitReference(phaseStatus)) {
|
|
749
|
+
return [
|
|
750
|
+
'Do not give this Context Packet to a coding agent.',
|
|
751
|
+
'Find the current split note with hive-lite status --json, or rerun the original broad find command to create a fresh split note.',
|
|
752
|
+
];
|
|
753
|
+
}
|
|
754
|
+
if (mode === 'edit_context') return [];
|
|
755
|
+
if (mode === 'needs_decomposition') {
|
|
756
|
+
return [
|
|
757
|
+
'Do not generate a coding prompt from this Context Packet.',
|
|
758
|
+
'Ask the human to choose one smaller phase, then run hive-lite find again for that phase.',
|
|
759
|
+
];
|
|
760
|
+
}
|
|
761
|
+
return [
|
|
762
|
+
`hive-lite map prompt --focus ${focus} --from-find ${contextId}`,
|
|
763
|
+
'Review and narrow .hive/map/areas.yaml, then run hive-lite find again.',
|
|
764
|
+
];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function agentInstructionsFor(mode) {
|
|
768
|
+
if (mode === 'edit_context') {
|
|
769
|
+
return [
|
|
770
|
+
'Only modify Writable Scope unless you explicitly report stuck.',
|
|
771
|
+
'Do not change files listed under Do Not Touch.',
|
|
772
|
+
'Conditional or broad fallback files require human review.',
|
|
773
|
+
'If the fix requires broader scope, stop and ask for a refined context packet.',
|
|
774
|
+
'After editing, stop and let the human run hive check.',
|
|
775
|
+
];
|
|
776
|
+
}
|
|
777
|
+
if (mode === 'discovery_context') {
|
|
778
|
+
return [
|
|
779
|
+
'Use this packet for investigation only; the map is not precise enough for safe editing.',
|
|
780
|
+
'Do not edit until the Project Map has a narrower writable_direct scope.',
|
|
781
|
+
'Report which files should become entrypoints or writable_direct candidates.',
|
|
782
|
+
];
|
|
783
|
+
}
|
|
784
|
+
if (mode === 'needs_decomposition') {
|
|
785
|
+
return [
|
|
786
|
+
'Do not edit code from this packet.',
|
|
787
|
+
'Do not merge candidate areas into one broad prompt.',
|
|
788
|
+
'Split the intent into smaller phases and rerun hive find for the selected phase.',
|
|
789
|
+
];
|
|
790
|
+
}
|
|
791
|
+
return [
|
|
792
|
+
'Do not edit code from this packet.',
|
|
793
|
+
'Improve the Project Map first, then rerun hive find.',
|
|
794
|
+
];
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function buildContextMarkdown(packet) {
|
|
798
|
+
const lines = [
|
|
799
|
+
`# Hive Context Packet ${packet.id}`,
|
|
800
|
+
'',
|
|
801
|
+
'## Mode',
|
|
802
|
+
packet.mode,
|
|
803
|
+
'',
|
|
804
|
+
'## Intent',
|
|
805
|
+
packet.intent.summary,
|
|
806
|
+
'',
|
|
807
|
+
'## Likely Area',
|
|
808
|
+
packet.area ? `- ${packet.area.id} (${packet.area.confidence})` : '- unknown',
|
|
809
|
+
'',
|
|
810
|
+
...(!packet.area ? [
|
|
811
|
+
'## Candidate Areas',
|
|
812
|
+
...(packet.candidateAreas.length
|
|
813
|
+
? packet.candidateAreas.map((item) => `- ${item.id} (score ${item.score}): ${item.signals.join(', ') || 'matched weakly'}`)
|
|
814
|
+
: ['- (none matched)']),
|
|
815
|
+
'',
|
|
816
|
+
'## Map Guidance',
|
|
817
|
+
'- Add aliases/concepts that match the intent wording.',
|
|
818
|
+
'- Add entrypoints for files an agent should inspect first.',
|
|
819
|
+
`- Consider: hive-lite map prompt --focus ${JSON.stringify(packet.intent.raw)} --from-find ${packet.id}`,
|
|
820
|
+
'',
|
|
821
|
+
] : []),
|
|
822
|
+
...(packet.warnings.length ? [
|
|
823
|
+
'## Warnings',
|
|
824
|
+
...packet.warnings.map((item) => `- ${item.code}: ${item.message}`),
|
|
825
|
+
'',
|
|
826
|
+
] : []),
|
|
827
|
+
...(packet.reviewGated.length ? [
|
|
828
|
+
'## Review-Gated Scope',
|
|
829
|
+
...packet.reviewGated.map((item) => `- ${item.code}: ${item.message}`),
|
|
830
|
+
'',
|
|
831
|
+
] : []),
|
|
832
|
+
...(packet.decompositionSignals && packet.decompositionSignals.length ? [
|
|
833
|
+
'## Decomposition Required',
|
|
834
|
+
...packet.decompositionSignals.map((item) => `- ${item.code}: ${item.message}`),
|
|
835
|
+
'',
|
|
836
|
+
...(packet.splitNote ? [
|
|
837
|
+
'## Split Note',
|
|
838
|
+
`- ${packet.splitNote.markdownPath}`,
|
|
839
|
+
'',
|
|
840
|
+
] : []),
|
|
841
|
+
'## Candidate Phase Seeds',
|
|
842
|
+
...(packet.candidatePhaseSeeds.length
|
|
843
|
+
? packet.candidatePhaseSeeds.map((item) => `- ${item.id} (${item.areaId}): rerun find with ${JSON.stringify(item.findIntent)}`)
|
|
844
|
+
: ['- (none)']),
|
|
845
|
+
'',
|
|
846
|
+
] : []),
|
|
847
|
+
...(packet.phaseDependencyStatus && packet.phaseDependencyStatus.canStartNormally === false ? [
|
|
848
|
+
'## Phase Dependency Status',
|
|
849
|
+
`- ${packet.phaseDependencyStatus.message}`,
|
|
850
|
+
...packet.phaseDependencyStatus.missingRequiredAcceptedPhases.map((item) => `- Missing required phase: ${item}`),
|
|
851
|
+
'',
|
|
852
|
+
] : []),
|
|
853
|
+
'## Relevant Files',
|
|
854
|
+
...packet.relevantFiles.map((file) => `- ${file.path} (${file.role || file.source}): ${file.reason}`),
|
|
855
|
+
'',
|
|
856
|
+
'## Writable Scope',
|
|
857
|
+
...(packet.writableScope.length ? packet.writableScope.map((item) => `- ${item}`) : ['- (none; this is not an edit permit)']),
|
|
858
|
+
'',
|
|
859
|
+
'## Conditional Writable Scope',
|
|
860
|
+
...(packet.scope.writableConditional.length ? packet.scope.writableConditional.map((item) => `- ${patternDisplay(item)}`) : ['- (none configured)']),
|
|
861
|
+
'',
|
|
862
|
+
'## Broad Fallback Scope',
|
|
863
|
+
...(packet.scope.writableBroadFallback.length ? packet.scope.writableBroadFallback.map((item) => `- ${patternDisplay(item)}`) : ['- (none configured)']),
|
|
864
|
+
'',
|
|
865
|
+
'## Readable Scope',
|
|
866
|
+
...(packet.readableScope.length ? packet.readableScope.map((item) => `- ${item}`) : ['- (local code search inside relevant files only)']),
|
|
867
|
+
'',
|
|
868
|
+
'## Do Not Touch',
|
|
869
|
+
...(packet.doNotTouch.length ? packet.doNotTouch.map((item) => `- ${item}`) : ['- (none configured)']),
|
|
870
|
+
'',
|
|
871
|
+
'## Validation',
|
|
872
|
+
...(packet.validationPlan.length ? packet.validationPlan.map((item) => {
|
|
873
|
+
if (item.command) return `- ${item.required ? 'Required' : 'Optional'}: ${item.command}`;
|
|
874
|
+
return `- ${item.required ? 'Required' : 'Optional'} manual: ${item.description || item.profile}`;
|
|
875
|
+
}) : ['- (none configured; do not invent a technology-specific command)']),
|
|
876
|
+
'',
|
|
877
|
+
'## Risk',
|
|
878
|
+
`- ${packet.risk.level}: ${packet.risk.reasons.join('; ') || 'no configured risk signal'}`,
|
|
879
|
+
'',
|
|
880
|
+
'## Agent Instructions',
|
|
881
|
+
...packet.agentInstructions.map((item) => `- ${item}`),
|
|
882
|
+
];
|
|
883
|
+
|
|
884
|
+
if (packet.questions.length > 0) {
|
|
885
|
+
lines.push('', '## Questions', ...packet.questions.map((item) => `- ${item}`));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (packet.recommendedActions.length > 0) {
|
|
889
|
+
lines.push('', '## Recommended Actions', ...packet.recommendedActions.map((item) => `- ${item}`));
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return `${lines.join('\n')}\n`;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function createContextPacket(root, intent, options = {}) {
|
|
896
|
+
const map = loadProjectMap(root);
|
|
897
|
+
const originSplit = originSplitFromOptions(options);
|
|
898
|
+
const originPhase = phaseFromOriginSplit(root, originSplit);
|
|
899
|
+
const phaseStatus = phaseDependencyStatus(root, originSplit);
|
|
900
|
+
const originPhaseAreaId = validAreaId(map, originPhase && originPhase.primaryAreaId);
|
|
901
|
+
const requestedArea = requestedAreaId(options);
|
|
902
|
+
const constrainedAreaId = originPhaseAreaId || validAreaId(map, requestedArea);
|
|
903
|
+
const findOptions = { ...options };
|
|
904
|
+
if (originPhaseAreaId) {
|
|
905
|
+
findOptions.area = originPhaseAreaId;
|
|
906
|
+
} else if (constrainedAreaId) {
|
|
907
|
+
findOptions.area = constrainedAreaId;
|
|
908
|
+
}
|
|
909
|
+
const tokens = tokenize(intent);
|
|
910
|
+
const maxFiles = Number(options.maxFiles || 8);
|
|
911
|
+
const allScored = map.areas
|
|
912
|
+
.map((area) => scoreArea(root, area, intent, tokens, findOptions))
|
|
913
|
+
.sort((a, b) => b.score - a.score);
|
|
914
|
+
const scored = constrainedAreaId
|
|
915
|
+
? allScored.filter((item) => item.area.id === constrainedAreaId)
|
|
916
|
+
: allScored;
|
|
917
|
+
const top = scored[0] || null;
|
|
918
|
+
const second = scored[1] || null;
|
|
919
|
+
const confidence = confidenceFor(top, second);
|
|
920
|
+
const area = confidence === 'low' && !constrainedAreaId ? null : top && top.score > 0 ? top.area : null;
|
|
921
|
+
const candidateAreas = candidateAreasFromScored(scored);
|
|
922
|
+
const scope = area ? normalizeAreaScope(root, area) : {
|
|
923
|
+
readable: [],
|
|
924
|
+
writableDirect: [],
|
|
925
|
+
writableConditional: [],
|
|
926
|
+
writableBroadFallback: [],
|
|
927
|
+
forbidden: [],
|
|
928
|
+
quality: 'unknown',
|
|
929
|
+
};
|
|
930
|
+
const restrictGrepToReadable = area && (confidence === 'high' || findOptions.area) && scope.readable.length > 0;
|
|
931
|
+
const grepFiles = grepHints(root, tokens, maxFiles, {
|
|
932
|
+
allowedPatterns: restrictGrepToReadable ? scope.readable : [],
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
const relevant = [];
|
|
936
|
+
const seen = new Set();
|
|
937
|
+
if (area) {
|
|
938
|
+
for (const entry of area.entrypoints || []) {
|
|
939
|
+
if (!entry.path || seen.has(entry.path)) continue;
|
|
940
|
+
seen.add(entry.path);
|
|
941
|
+
relevant.push({
|
|
942
|
+
path: entry.path,
|
|
943
|
+
role: entry.role || 'entrypoint',
|
|
944
|
+
reason: entry.reason || `project map entrypoint for ${area.id}`,
|
|
945
|
+
source: 'project_map',
|
|
946
|
+
confidence,
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
for (const hit of grepFiles) {
|
|
951
|
+
if (seen.has(hit.path) || relevant.length >= maxFiles) continue;
|
|
952
|
+
seen.add(hit.path);
|
|
953
|
+
relevant.push({
|
|
954
|
+
path: hit.path,
|
|
955
|
+
role: 'grep_hit',
|
|
956
|
+
reason: `matched intent terms: ${hit.hits.map((item) => item.token).join(', ')}`,
|
|
957
|
+
source: 'git_grep',
|
|
958
|
+
confidence: 'medium',
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const id = createId('ctx');
|
|
963
|
+
const validationPlan = area ? validationPlanForArea(map, area) : [];
|
|
964
|
+
const warnings = [];
|
|
965
|
+
if (!area) {
|
|
966
|
+
warnings.push({
|
|
967
|
+
code: 'NO_CONFIDENT_AREA',
|
|
968
|
+
message: 'No project map area matched this intent with enough confidence.',
|
|
969
|
+
});
|
|
970
|
+
} else {
|
|
971
|
+
warnings.push(...scopeWarnings(scope));
|
|
972
|
+
if (relevant.length === 0 || !relevant.some((file) => file.source === 'project_map')) {
|
|
973
|
+
warnings.push({
|
|
974
|
+
code: 'MISSING_ENTRYPOINT',
|
|
975
|
+
message: 'No project map entrypoint was available for this context.',
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
if (validationPlan.length === 0) {
|
|
979
|
+
warnings.push({
|
|
980
|
+
code: 'MISSING_VALIDATION',
|
|
981
|
+
message: 'No validation profile or default validation command is configured.',
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const reviewGated = area ? reviewGatedNotices(scope) : [];
|
|
986
|
+
warnings.push(...splitRoutingWarnings(originSplit, phaseStatus, originPhaseAreaId, requestedArea));
|
|
987
|
+
const decomposition = decompositionSignals({
|
|
988
|
+
map,
|
|
989
|
+
area,
|
|
990
|
+
confidence,
|
|
991
|
+
intent,
|
|
992
|
+
relevant,
|
|
993
|
+
scope,
|
|
994
|
+
scored,
|
|
995
|
+
validationPlan,
|
|
996
|
+
constrainedAreaId,
|
|
997
|
+
});
|
|
998
|
+
const phaseSeeds = decomposition.length > 0 ? candidatePhaseSeeds(scored, intent) : [];
|
|
999
|
+
let mode = contextMode(area, scope, validationPlan, relevant, decomposition);
|
|
1000
|
+
if (missingSplitReference(phaseStatus) && mode === 'edit_context') mode = 'discovery_context';
|
|
1001
|
+
const actions = recommendedActions(mode, intent, id, phaseStatus);
|
|
1002
|
+
const packet = {
|
|
1003
|
+
version: 1,
|
|
1004
|
+
id,
|
|
1005
|
+
createdAt: new Date().toISOString(),
|
|
1006
|
+
repo: {
|
|
1007
|
+
root,
|
|
1008
|
+
head: currentHead(root),
|
|
1009
|
+
branch: currentBranch(root),
|
|
1010
|
+
},
|
|
1011
|
+
intent: {
|
|
1012
|
+
raw: intent,
|
|
1013
|
+
kind: options.kind || 'unknown',
|
|
1014
|
+
summary: intent,
|
|
1015
|
+
},
|
|
1016
|
+
area: area ? {
|
|
1017
|
+
id: area.id,
|
|
1018
|
+
confidence,
|
|
1019
|
+
matchedSignals: top.signals,
|
|
1020
|
+
alternatives: scored.slice(1, 4).filter((item) => item.score > 0).map((item) => ({
|
|
1021
|
+
id: item.area.id,
|
|
1022
|
+
score: item.score,
|
|
1023
|
+
signals: item.signals.slice(0, 4),
|
|
1024
|
+
})),
|
|
1025
|
+
} : null,
|
|
1026
|
+
mode,
|
|
1027
|
+
confidence: {
|
|
1028
|
+
level: area ? confidence : 'low',
|
|
1029
|
+
topScore: top ? top.score : 0,
|
|
1030
|
+
reasons: area ? top.signals.slice(0, 8) : ['no unique area'],
|
|
1031
|
+
},
|
|
1032
|
+
candidateAreas,
|
|
1033
|
+
originSplit,
|
|
1034
|
+
phaseDependencyStatus: phaseStatus,
|
|
1035
|
+
relevantFiles: relevant,
|
|
1036
|
+
scope,
|
|
1037
|
+
writableScope: scope.writableDirect.slice(),
|
|
1038
|
+
readableScope: scope.readable.slice(),
|
|
1039
|
+
doNotTouch: scope.forbidden.slice(),
|
|
1040
|
+
validationPlan,
|
|
1041
|
+
warnings,
|
|
1042
|
+
reviewGated,
|
|
1043
|
+
decompositionSignals: decomposition,
|
|
1044
|
+
candidatePhaseSeeds: phaseSeeds,
|
|
1045
|
+
recommendedActions: actions,
|
|
1046
|
+
contextBoundary: area ? {
|
|
1047
|
+
maxPrimaryAreas: 1,
|
|
1048
|
+
primaryAreaId: area.id,
|
|
1049
|
+
stopIfRequiresConditionalScope: true,
|
|
1050
|
+
stopIfNeedsAreas: phaseSeeds
|
|
1051
|
+
.map((item) => item.areaId)
|
|
1052
|
+
.filter((areaId) => areaId !== area.id),
|
|
1053
|
+
} : null,
|
|
1054
|
+
risk: {
|
|
1055
|
+
level: area && area.risk && area.risk.default_level ? area.risk.default_level : (area ? 'low' : 'unknown'),
|
|
1056
|
+
tags: area && area.risk ? (area.risk.tags || []) : [],
|
|
1057
|
+
reasons: area ? [`area ${area.id}`, `${confidence} confidence`] : ['low confidence; no unique area'],
|
|
1058
|
+
},
|
|
1059
|
+
agentInstructions: agentInstructionsFor(mode),
|
|
1060
|
+
questions: area ? [] : [
|
|
1061
|
+
'No high-confidence area was found. Add aliases/entrypoints to .hive/map/areas.yaml or rerun with --area <id>.',
|
|
1062
|
+
],
|
|
1063
|
+
explain: {
|
|
1064
|
+
areaScores: scored.slice(0, 8).map((item) => ({
|
|
1065
|
+
id: item.area.id,
|
|
1066
|
+
score: item.score,
|
|
1067
|
+
signals: item.signals.slice(0, 8),
|
|
1068
|
+
})),
|
|
1069
|
+
selectedArea: area ? area.id : null,
|
|
1070
|
+
scopeQuality: scope.quality,
|
|
1071
|
+
relevantFileSources: relevant.map((file) => ({
|
|
1072
|
+
path: file.path,
|
|
1073
|
+
source: file.source,
|
|
1074
|
+
role: file.role || '',
|
|
1075
|
+
reason: file.reason,
|
|
1076
|
+
})),
|
|
1077
|
+
validationQuality: validationPlan.length > 0 ? 'configured' : 'missing',
|
|
1078
|
+
warnings,
|
|
1079
|
+
reviewGated,
|
|
1080
|
+
decompositionSignals: decomposition,
|
|
1081
|
+
candidatePhaseSeeds: phaseSeeds,
|
|
1082
|
+
grepScope: restrictGrepToReadable ? 'readable_scope' : 'repo',
|
|
1083
|
+
recommendedActions: actions,
|
|
1084
|
+
},
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
if (mode === 'needs_decomposition') {
|
|
1088
|
+
packet.splitNote = createSplitNote(root, packet, phaseSeeds, decomposition, options);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const contextDir = path.join(hiveDir(root), 'context');
|
|
1092
|
+
ensureDir(contextDir);
|
|
1093
|
+
const jsonPath = path.join(contextDir, `${id}.json`);
|
|
1094
|
+
const mdPath = path.join(contextDir, `${id}.md`);
|
|
1095
|
+
writeJson(jsonPath, packet);
|
|
1096
|
+
writeText(mdPath, buildContextMarkdown(packet));
|
|
1097
|
+
return { packet, jsonPath, mdPath };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
module.exports = {
|
|
1101
|
+
buildContextMarkdown,
|
|
1102
|
+
createContextPacket,
|
|
1103
|
+
tokenize,
|
|
1104
|
+
};
|