hive-lite 0.1.9 → 0.2.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 +3 -1
- package/docs/cli-semantics.md +53 -1
- package/docs/skills/hive-lite-bootstrap/SKILL.md +28 -0
- package/docs/skills/hive-lite-discovery/SKILL.md +151 -0
- package/docs/skills/hive-lite-discovery/agents/openai.yaml +4 -0
- package/docs/skills/hive-lite-finish/SKILL.md +62 -5
- package/docs/skills/hive-lite-map-maintainer/SKILL.md +28 -0
- package/docs/skills/hive-lite-map-maintainer/references/lifecycle.md +1 -1
- package/docs/skills/hive-lite-map-maintainer/references/repair-rules.md +7 -0
- package/docs/skills/hive-lite-start-prompt/SKILL.md +175 -52
- package/docs/skills/hive-lite-start-prompt/references/preflight.md +70 -20
- package/package.json +3 -3
- package/src/cli.js +146 -3
- package/src/lib/change.js +93 -0
- package/src/lib/context.js +304 -31
- package/src/lib/git.js +1 -0
- package/src/lib/health.js +208 -0
- package/src/lib/intent.js +620 -0
- package/src/lib/map.js +57 -0
- package/src/lib/operator.js +1067 -0
- package/src/lib/skills.js +1 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ensureDir, exists, readJson, readText, writeJson, writeText } = require('./fsx');
|
|
4
|
+
const { parseYaml } = require('./yaml');
|
|
5
|
+
|
|
6
|
+
const SCHEMA_VERSION = 'hive.start_intent.v1';
|
|
7
|
+
const INTENT_ID_PATTERN = /^int_[a-z0-9][a-z0-9_-]*$/;
|
|
8
|
+
|
|
9
|
+
const REQUIRED_SECTIONS = [
|
|
10
|
+
{
|
|
11
|
+
key: 'originalIdea',
|
|
12
|
+
title: 'Original Idea',
|
|
13
|
+
aliases: ['original idea'],
|
|
14
|
+
allowNone: false,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: 'agreedDirection',
|
|
18
|
+
title: 'Agreed Direction',
|
|
19
|
+
aliases: ['agreed direction'],
|
|
20
|
+
allowNone: false,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'userConfirmedDecisions',
|
|
24
|
+
title: 'User-Confirmed Decisions',
|
|
25
|
+
aliases: ['user confirmed decisions', 'user-confirmed decisions'],
|
|
26
|
+
allowNone: true,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: 'nonGoals',
|
|
30
|
+
title: 'Non-Goals',
|
|
31
|
+
aliases: ['non goals', 'non-goals'],
|
|
32
|
+
allowNone: false,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'acceptanceSignals',
|
|
36
|
+
title: 'Acceptance Signals',
|
|
37
|
+
aliases: ['acceptance signals'],
|
|
38
|
+
allowNone: false,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'unknownsForHiveLiteToResolve',
|
|
42
|
+
title: 'Unknowns For Hive Lite To Resolve',
|
|
43
|
+
aliases: ['unknowns for hive lite to resolve'],
|
|
44
|
+
allowNone: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: 'preHiveObservationsHints',
|
|
48
|
+
title: 'Pre-Hive Observations / Hints',
|
|
49
|
+
aliases: [
|
|
50
|
+
'pre hive observations hints',
|
|
51
|
+
'pre-hive observations hints',
|
|
52
|
+
'pre hive observations',
|
|
53
|
+
'pre-hive observations',
|
|
54
|
+
'pre hive hints',
|
|
55
|
+
'pre-hive hints',
|
|
56
|
+
],
|
|
57
|
+
allowNone: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
key: 'blockingQuestions',
|
|
61
|
+
title: 'Blocking Questions',
|
|
62
|
+
aliases: ['blocking questions'],
|
|
63
|
+
allowNone: true,
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const SECTION_BY_NORMALIZED = new Map();
|
|
68
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
69
|
+
for (const alias of section.aliases) SECTION_BY_NORMALIZED.set(normalizeHeading(alias), section);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const FORBIDDEN_SECTION_HEADINGS = new Map([
|
|
73
|
+
['direct writable', 'Direct Writable'],
|
|
74
|
+
['final file scope', 'Final File Scope'],
|
|
75
|
+
['implementation plan', 'Implementation Plan'],
|
|
76
|
+
['validation plan', 'Validation Plan'],
|
|
77
|
+
['files to modify', 'Files To Modify'],
|
|
78
|
+
['files to edit', 'Files To Edit'],
|
|
79
|
+
['files to change', 'Files To Change'],
|
|
80
|
+
['edit targets', 'Edit Targets'],
|
|
81
|
+
['coding agent prompt', 'Coding Agent Prompt'],
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const EXECUTION_INSTRUCTION_PATTERNS = [
|
|
85
|
+
{
|
|
86
|
+
code: 'AGENT_EDIT_INSTRUCTION',
|
|
87
|
+
pattern: /\bagent\s+(should|must|will|can)\s+(edit|modify|change|update|touch)\b/i,
|
|
88
|
+
message: 'Start Intent Handoff must not instruct the coding agent to edit files.',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
code: 'CHANGE_THESE_FILES',
|
|
92
|
+
pattern: /\b(edit|modify|change|update|touch)\s+these\s+files\b/i,
|
|
93
|
+
message: 'Start Intent Handoff must not define files for implementation.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
code: 'FILES_TO_MODIFY',
|
|
97
|
+
pattern: /\bfiles\s+to\s+(modify|edit|change|update)\b/i,
|
|
98
|
+
message: 'Start Intent Handoff must not include a files-to-modify list.',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
code: 'DIRECT_FILE_EDIT_TARGET',
|
|
102
|
+
pattern: /\b(edit|modify|change|update|touch)\s+`?([A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+`?/i,
|
|
103
|
+
message: 'Start Intent Handoff may mention files only as non-authoritative hints, not edit targets.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
code: 'DIRECT_FILE_EDIT_TARGET',
|
|
107
|
+
pattern: /`?([A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+`?\s+(must|should|will)\s+be\s+(edited|modified|changed|updated|touched)/i,
|
|
108
|
+
message: 'Start Intent Handoff may mention files only as non-authoritative hints, not edit targets.',
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const EXECUTION_LABEL_PATTERN = /^\s*(Direct Writable|Final File Scope|Implementation Plan|Validation Plan|Files To Modify|Files To Edit|Files To Change|Edit Targets)\s*:/i;
|
|
113
|
+
const FILE_PATH_PATTERN = /(?:^|[\s(`])(?:\.?\.?\/)?(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\.[A-Za-z0-9]+(?:$|[\s`).,;:])/;
|
|
114
|
+
const HINT_LANGUAGE_PATTERN = /\b(may|might|appears?|observed|verify|possible|likely|seems?|non-authoritative|hint|hints|could)\b/i;
|
|
115
|
+
const CODEBASE_UNKNOWN_PATTERN = /\b(codebase|repo|repository|project|existing|current|where|which|file|path|module|component|provider|proxy|controller|service|dto|config|configuration|test|registration|registry|abstraction|pattern|endpoint|route|schema|database|table|api|surface|integration|class|function|hook|entrypoint|workflow|model)\b/i;
|
|
116
|
+
const PRODUCT_DECISION_PATTERN = /\b(should|whether|decide|choose|allow users|users can|user can|paid|free|pricing|tier|plan|rollout|brand|policy|v1|first version|support multiple|enable for)\b/i;
|
|
117
|
+
|
|
118
|
+
function normalizeHeading(value) {
|
|
119
|
+
return String(value || '')
|
|
120
|
+
.toLowerCase()
|
|
121
|
+
.replace(/[’']/g, '')
|
|
122
|
+
.replace(/[^a-z0-9]+/g, ' ')
|
|
123
|
+
.trim();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isNone(value) {
|
|
127
|
+
return /^(none|none\.|n\/a|no blocking questions\.?)$/i.test(String(value || '').trim());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function cleanHeadingText(value) {
|
|
131
|
+
return String(value || '').replace(/\s+#+\s*$/g, '').trim();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseFrontmatter(markdown) {
|
|
135
|
+
const normalized = String(markdown || '').replace(/^\uFEFF/, '');
|
|
136
|
+
const lines = normalized.split(/\r?\n/);
|
|
137
|
+
if (lines[0].trim() !== '---') {
|
|
138
|
+
return {
|
|
139
|
+
frontmatter: {},
|
|
140
|
+
body: normalized,
|
|
141
|
+
errors: [{
|
|
142
|
+
code: 'MISSING_FRONTMATTER',
|
|
143
|
+
message: 'Start Intent Handoff must begin with YAML frontmatter.',
|
|
144
|
+
}],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let end = -1;
|
|
149
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
150
|
+
if (lines[i].trim() === '---') {
|
|
151
|
+
end = i;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (end === -1) {
|
|
157
|
+
return {
|
|
158
|
+
frontmatter: {},
|
|
159
|
+
body: normalized,
|
|
160
|
+
errors: [{
|
|
161
|
+
code: 'UNCLOSED_FRONTMATTER',
|
|
162
|
+
message: 'Start Intent Handoff frontmatter must be closed with ---.',
|
|
163
|
+
}],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
return {
|
|
169
|
+
frontmatter: parseYaml(lines.slice(1, end).join('\n')),
|
|
170
|
+
body: lines.slice(end + 1).join('\n'),
|
|
171
|
+
errors: [],
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return {
|
|
175
|
+
frontmatter: {},
|
|
176
|
+
body: lines.slice(end + 1).join('\n'),
|
|
177
|
+
errors: [{
|
|
178
|
+
code: 'INVALID_FRONTMATTER',
|
|
179
|
+
message: `Start Intent Handoff frontmatter is invalid: ${error.message}`,
|
|
180
|
+
}],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseMarkdownSections(body) {
|
|
186
|
+
const sections = {};
|
|
187
|
+
const allHeadings = [];
|
|
188
|
+
let title = '';
|
|
189
|
+
let active = null;
|
|
190
|
+
|
|
191
|
+
for (const line of String(body || '').split(/\r?\n/)) {
|
|
192
|
+
const h1 = line.match(/^#\s+(.+)$/);
|
|
193
|
+
if (h1 && !title) {
|
|
194
|
+
title = cleanHeadingText(h1[1]);
|
|
195
|
+
allHeadings.push({ level: 1, text: title, normalized: normalizeHeading(title) });
|
|
196
|
+
active = null;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const h2 = line.match(/^##\s+(.+)$/);
|
|
201
|
+
if (h2) {
|
|
202
|
+
const text = cleanHeadingText(h2[1]);
|
|
203
|
+
const normalized = normalizeHeading(text);
|
|
204
|
+
allHeadings.push({ level: 2, text, normalized });
|
|
205
|
+
const known = SECTION_BY_NORMALIZED.get(normalized);
|
|
206
|
+
active = known ? known.key : `__unknown_${allHeadings.length}`;
|
|
207
|
+
if (!sections[active]) {
|
|
208
|
+
sections[active] = {
|
|
209
|
+
title: known ? known.title : text,
|
|
210
|
+
rawTitle: text,
|
|
211
|
+
content: '',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (active) sections[active].content += `${line}\n`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const section of Object.values(sections)) {
|
|
221
|
+
section.content = section.content.trim();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { title, sections, allHeadings };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function addError(errors, code, message, extra = {}) {
|
|
228
|
+
errors.push({ code, message, ...extra });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function addWarning(warnings, code, message, extra = {}) {
|
|
232
|
+
warnings.push({ code, message, ...extra });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function contentLines(section) {
|
|
236
|
+
return String(section && section.content ? section.content : '')
|
|
237
|
+
.split(/\r?\n/)
|
|
238
|
+
.map((line) => line.trim())
|
|
239
|
+
.filter(Boolean);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function contentItems(section) {
|
|
243
|
+
return contentLines(section)
|
|
244
|
+
.map((line) => line.replace(/^[-*]\s+/, '').trim())
|
|
245
|
+
.filter((line) => !isNone(line));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function validateFrontmatter(frontmatter, errors) {
|
|
249
|
+
if (frontmatter.type !== 'hive-lite.start-intent') {
|
|
250
|
+
addError(errors, 'INVALID_TYPE', 'frontmatter.type must be hive-lite.start-intent.', { field: 'type' });
|
|
251
|
+
}
|
|
252
|
+
if (Number(frontmatter.version) !== 1) {
|
|
253
|
+
addError(errors, 'INVALID_VERSION', 'frontmatter.version must be 1.', { field: 'version' });
|
|
254
|
+
}
|
|
255
|
+
if (frontmatter.status !== 'ready_for_hive') {
|
|
256
|
+
addError(errors, 'INVALID_STATUS', 'frontmatter.status must be ready_for_hive.', { field: 'status' });
|
|
257
|
+
}
|
|
258
|
+
if (!frontmatter.source) {
|
|
259
|
+
addError(errors, 'MISSING_SOURCE', 'frontmatter.source is required.', { field: 'source' });
|
|
260
|
+
}
|
|
261
|
+
if (!frontmatter.intent_id) {
|
|
262
|
+
addError(errors, 'MISSING_INTENT_ID', 'frontmatter.intent_id is required.', { field: 'intent_id' });
|
|
263
|
+
} else if (!INTENT_ID_PATTERN.test(String(frontmatter.intent_id))) {
|
|
264
|
+
addError(errors, 'INVALID_INTENT_ID', 'frontmatter.intent_id must match int_[a-z0-9][a-z0-9_-]*.', { field: 'intent_id' });
|
|
265
|
+
}
|
|
266
|
+
if (!frontmatter.created_at) {
|
|
267
|
+
addError(errors, 'MISSING_CREATED_AT', 'frontmatter.created_at is required.', { field: 'created_at' });
|
|
268
|
+
} else if (Number.isNaN(Date.parse(String(frontmatter.created_at)))) {
|
|
269
|
+
addError(errors, 'INVALID_CREATED_AT', 'frontmatter.created_at must be an ISO-like timestamp.', { field: 'created_at' });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function validateSections(parsed, frontmatter, errors, warnings) {
|
|
274
|
+
if (!parsed.title) {
|
|
275
|
+
addError(errors, 'MISSING_TITLE', 'Start Intent Handoff must include an H1 title.');
|
|
276
|
+
} else if (!/^Start Intent Handoff:/i.test(parsed.title)) {
|
|
277
|
+
addWarning(warnings, 'TITLE_FORMAT', 'H1 should start with "Start Intent Handoff:".');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const heading of parsed.allHeadings.filter((item) => item.level === 2)) {
|
|
281
|
+
const forbidden = FORBIDDEN_SECTION_HEADINGS.get(heading.normalized);
|
|
282
|
+
if (forbidden) {
|
|
283
|
+
addError(errors, 'FORBIDDEN_SECTION', `Start Intent Handoff must not include execution-only section "${forbidden}".`, {
|
|
284
|
+
section: heading.text,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const required of REQUIRED_SECTIONS) {
|
|
290
|
+
const section = parsed.sections[required.key];
|
|
291
|
+
if (!section) {
|
|
292
|
+
addError(errors, 'MISSING_SECTION', `Missing required section: ${required.title}.`, { section: required.title });
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (!section.content || (!required.allowNone && isNone(section.content))) {
|
|
296
|
+
addError(errors, 'EMPTY_SECTION', `${required.title} must not be empty.`, { section: required.title });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const blocking = parsed.sections.blockingQuestions;
|
|
301
|
+
if (frontmatter.status === 'ready_for_hive' && blocking && !isNone(blocking.content)) {
|
|
302
|
+
addError(errors, 'BLOCKING_QUESTIONS_PRESENT', 'ready_for_hive handoff must have Blocking Questions set to None.', {
|
|
303
|
+
section: 'Blocking Questions',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const unknowns = parsed.sections.unknownsForHiveLiteToResolve;
|
|
308
|
+
if (unknowns) {
|
|
309
|
+
const items = contentItems(unknowns);
|
|
310
|
+
if (items.length === 0) {
|
|
311
|
+
addError(errors, 'EMPTY_CODEBASE_UNKNOWNS', 'Unknowns For Hive Lite To Resolve must list codebase/context unknowns.', {
|
|
312
|
+
section: 'Unknowns For Hive Lite To Resolve',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
for (const item of items) {
|
|
316
|
+
if (PRODUCT_DECISION_PATTERN.test(item)) {
|
|
317
|
+
addError(errors, 'PRODUCT_DECISION_IN_UNKNOWNS', 'Product decisions belong in User-Confirmed Decisions or Blocking Questions, not Unknowns For Hive Lite To Resolve.', {
|
|
318
|
+
section: 'Unknowns For Hive Lite To Resolve',
|
|
319
|
+
text: item,
|
|
320
|
+
});
|
|
321
|
+
} else if (!CODEBASE_UNKNOWN_PATTERN.test(item)) {
|
|
322
|
+
addWarning(warnings, 'UNKNOWN_MAY_NOT_BE_CODEBASE_CONTEXT', 'Unknowns For Hive Lite To Resolve should be codebase/context questions only.', {
|
|
323
|
+
section: 'Unknowns For Hive Lite To Resolve',
|
|
324
|
+
text: item,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function validateExecutionContent(markdown, parsed, errors, warnings) {
|
|
332
|
+
for (const rule of EXECUTION_INSTRUCTION_PATTERNS) {
|
|
333
|
+
const match = String(markdown || '').match(rule.pattern);
|
|
334
|
+
if (match) {
|
|
335
|
+
addError(errors, rule.code, rule.message, { text: match[0] });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (const [key, section] of Object.entries(parsed.sections)) {
|
|
340
|
+
for (const line of contentLines(section)) {
|
|
341
|
+
const labelMatch = line.match(EXECUTION_LABEL_PATTERN);
|
|
342
|
+
if (labelMatch) {
|
|
343
|
+
addError(errors, 'FORBIDDEN_EXECUTION_LABEL', `Start Intent Handoff must not include execution-only label "${labelMatch[1]}".`, {
|
|
344
|
+
section: section.title,
|
|
345
|
+
text: line,
|
|
346
|
+
});
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!FILE_PATH_PATTERN.test(line)) continue;
|
|
350
|
+
if (key === 'preHiveObservationsHints') {
|
|
351
|
+
if (!HINT_LANGUAGE_PATTERN.test(line)) {
|
|
352
|
+
addWarning(warnings, 'AUTHORITATIVE_FILE_HINT', 'File paths in Pre-Hive Observations / Hints should be explicitly non-authoritative.', {
|
|
353
|
+
section: section.title,
|
|
354
|
+
text: line,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
addWarning(warnings, 'FILE_PATH_OUTSIDE_HINTS', 'File paths should usually appear only as non-authoritative Pre-Hive Observations / Hints.', {
|
|
359
|
+
section: section.title,
|
|
360
|
+
text: line,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function deriveFindIntent(fields) {
|
|
368
|
+
const parts = [
|
|
369
|
+
['Agreed Direction', fields.agreedDirection],
|
|
370
|
+
['User-Confirmed Decisions', fields.userConfirmedDecisions],
|
|
371
|
+
['Non-Goals', fields.nonGoals],
|
|
372
|
+
['Acceptance Signals', fields.acceptanceSignals],
|
|
373
|
+
['Codebase Unknowns For Hive Lite To Resolve', fields.unknownsForHiveLiteToResolve],
|
|
374
|
+
].filter(([, value]) => value && !isNone(value));
|
|
375
|
+
|
|
376
|
+
return parts.map(([label, value]) => `${label}:\n${value}`).join('\n\n');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function parseStartIntentMarkdown(markdown) {
|
|
380
|
+
const front = parseFrontmatter(markdown);
|
|
381
|
+
const parsed = parseMarkdownSections(front.body);
|
|
382
|
+
const fields = {};
|
|
383
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
384
|
+
fields[section.key] = parsed.sections[section.key] ? parsed.sections[section.key].content : '';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
schema_version: SCHEMA_VERSION,
|
|
389
|
+
title: parsed.title,
|
|
390
|
+
frontmatter: front.frontmatter,
|
|
391
|
+
fields,
|
|
392
|
+
findIntent: deriveFindIntent(fields),
|
|
393
|
+
_parse: {
|
|
394
|
+
title: parsed.title,
|
|
395
|
+
frontmatterErrors: front.errors,
|
|
396
|
+
sections: parsed.sections,
|
|
397
|
+
allHeadings: parsed.allHeadings,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function validateStartIntentMarkdown(markdown) {
|
|
403
|
+
const intent = parseStartIntentMarkdown(markdown);
|
|
404
|
+
const errors = [...intent._parse.frontmatterErrors];
|
|
405
|
+
const warnings = [];
|
|
406
|
+
|
|
407
|
+
validateFrontmatter(intent.frontmatter, errors);
|
|
408
|
+
validateSections(intent._parse, intent.frontmatter, errors, warnings);
|
|
409
|
+
validateExecutionContent(markdown, intent._parse, errors, warnings);
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
schema_version: SCHEMA_VERSION,
|
|
413
|
+
valid: errors.length === 0,
|
|
414
|
+
intent_id: intent.frontmatter.intent_id || null,
|
|
415
|
+
status: intent.frontmatter.status || null,
|
|
416
|
+
errors,
|
|
417
|
+
warnings,
|
|
418
|
+
intent: publicIntent(intent),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function publicIntent(intent) {
|
|
423
|
+
return {
|
|
424
|
+
schema_version: SCHEMA_VERSION,
|
|
425
|
+
intent_id: intent.frontmatter.intent_id || null,
|
|
426
|
+
type: intent.frontmatter.type || null,
|
|
427
|
+
version: intent.frontmatter.version || null,
|
|
428
|
+
status: intent.frontmatter.status || null,
|
|
429
|
+
source: intent.frontmatter.source || null,
|
|
430
|
+
created_at: intent.frontmatter.created_at || null,
|
|
431
|
+
title: intent.title || null,
|
|
432
|
+
fields: intent.fields,
|
|
433
|
+
findIntent: intent.findIntent,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function relPath(root, file) {
|
|
438
|
+
const realRoot = fs.realpathSync(root);
|
|
439
|
+
let realFile = path.resolve(file);
|
|
440
|
+
try {
|
|
441
|
+
realFile = fs.realpathSync(file);
|
|
442
|
+
} catch {
|
|
443
|
+
realFile = path.resolve(file);
|
|
444
|
+
}
|
|
445
|
+
const relative = path.relative(realRoot, realFile).replace(/\\/g, '/');
|
|
446
|
+
if (!relative || relative.startsWith('..')) return file;
|
|
447
|
+
return relative;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function intentDir(root, intentId) {
|
|
451
|
+
return path.join(root, '.hive', 'state', 'intents', intentId);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function savedIntentPaths(root, intentId) {
|
|
455
|
+
const dir = intentDir(root, intentId);
|
|
456
|
+
return {
|
|
457
|
+
dir,
|
|
458
|
+
markdownPath: path.join(dir, 'start-intent.md'),
|
|
459
|
+
jsonPath: path.join(dir, 'start-intent.json'),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function resolveIntentTarget(root, cwd, idOrPath) {
|
|
464
|
+
if (!idOrPath) throw new Error('intent id or path is required');
|
|
465
|
+
|
|
466
|
+
const text = String(idOrPath);
|
|
467
|
+
const candidates = path.isAbsolute(text)
|
|
468
|
+
? [text]
|
|
469
|
+
: [path.resolve(cwd || root, text), path.resolve(root, text)];
|
|
470
|
+
const candidate = candidates.find((item, index) => (
|
|
471
|
+
exists(item) && candidates.indexOf(item) === index
|
|
472
|
+
));
|
|
473
|
+
if (candidate) {
|
|
474
|
+
const stat = fs.statSync(candidate);
|
|
475
|
+
if (stat.isDirectory()) {
|
|
476
|
+
return {
|
|
477
|
+
kind: 'path',
|
|
478
|
+
markdownPath: path.join(candidate, 'start-intent.md'),
|
|
479
|
+
jsonPath: path.join(candidate, 'start-intent.json'),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
if (path.basename(candidate) === 'start-intent.json') {
|
|
483
|
+
return {
|
|
484
|
+
kind: 'path',
|
|
485
|
+
markdownPath: path.join(path.dirname(candidate), 'start-intent.md'),
|
|
486
|
+
jsonPath: candidate,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
kind: 'path',
|
|
491
|
+
markdownPath: candidate,
|
|
492
|
+
jsonPath: path.join(path.dirname(candidate), 'start-intent.json'),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (text.includes('/') || text.includes('\\') || text.endsWith('.md') || text.endsWith('.json')) {
|
|
497
|
+
throw new Error(`intent path does not exist: ${text}`);
|
|
498
|
+
}
|
|
499
|
+
if (!INTENT_ID_PATTERN.test(text)) {
|
|
500
|
+
throw new Error(`invalid intent id: ${text}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
kind: 'id',
|
|
505
|
+
intentId: text,
|
|
506
|
+
...savedIntentPaths(root, text),
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function readIntentTarget(root, cwd, idOrPath) {
|
|
511
|
+
const target = resolveIntentTarget(root, cwd, idOrPath);
|
|
512
|
+
if (!exists(target.markdownPath)) {
|
|
513
|
+
throw new Error(`Start Intent Handoff markdown not found: ${target.markdownPath}`);
|
|
514
|
+
}
|
|
515
|
+
const markdown = readText(target.markdownPath);
|
|
516
|
+
const validation = validateStartIntentMarkdown(markdown);
|
|
517
|
+
return {
|
|
518
|
+
...validation.intent,
|
|
519
|
+
markdown,
|
|
520
|
+
paths: {
|
|
521
|
+
markdown: relPath(root, target.markdownPath),
|
|
522
|
+
json: exists(target.jsonPath) ? relPath(root, target.jsonPath) : null,
|
|
523
|
+
},
|
|
524
|
+
validation: {
|
|
525
|
+
valid: validation.valid,
|
|
526
|
+
errors: validation.errors,
|
|
527
|
+
warnings: validation.warnings,
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function validateIntentTarget(root, cwd, idOrPath) {
|
|
533
|
+
const target = resolveIntentTarget(root, cwd, idOrPath);
|
|
534
|
+
if (!exists(target.markdownPath)) {
|
|
535
|
+
throw new Error(`Start Intent Handoff markdown not found: ${target.markdownPath}`);
|
|
536
|
+
}
|
|
537
|
+
const markdown = readText(target.markdownPath);
|
|
538
|
+
const validation = validateStartIntentMarkdown(markdown);
|
|
539
|
+
return {
|
|
540
|
+
schema_version: SCHEMA_VERSION,
|
|
541
|
+
command: 'intent validate',
|
|
542
|
+
target: idOrPath,
|
|
543
|
+
paths: {
|
|
544
|
+
markdown: relPath(root, target.markdownPath),
|
|
545
|
+
json: exists(target.jsonPath) ? relPath(root, target.jsonPath) : null,
|
|
546
|
+
},
|
|
547
|
+
valid: validation.valid,
|
|
548
|
+
intent_id: validation.intent_id,
|
|
549
|
+
status: validation.status,
|
|
550
|
+
errors: validation.errors,
|
|
551
|
+
warnings: validation.warnings,
|
|
552
|
+
intent: validation.intent,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function saveStartIntent(root, markdown, options = {}) {
|
|
557
|
+
const validation = validateStartIntentMarkdown(markdown);
|
|
558
|
+
if (!validation.valid) {
|
|
559
|
+
return {
|
|
560
|
+
schema_version: SCHEMA_VERSION,
|
|
561
|
+
command: 'intent save',
|
|
562
|
+
saved: false,
|
|
563
|
+
source: options.source || null,
|
|
564
|
+
valid: false,
|
|
565
|
+
intent_id: validation.intent_id,
|
|
566
|
+
status: validation.status,
|
|
567
|
+
errors: validation.errors,
|
|
568
|
+
warnings: validation.warnings,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const paths = savedIntentPaths(root, validation.intent_id);
|
|
573
|
+
ensureDir(paths.dir);
|
|
574
|
+
const markdownValue = String(markdown || '').endsWith('\n') ? String(markdown || '') : `${markdown}\n`;
|
|
575
|
+
writeText(paths.markdownPath, markdownValue);
|
|
576
|
+
|
|
577
|
+
const json = {
|
|
578
|
+
...validation.intent,
|
|
579
|
+
paths: {
|
|
580
|
+
markdown: relPath(root, paths.markdownPath),
|
|
581
|
+
json: relPath(root, paths.jsonPath),
|
|
582
|
+
},
|
|
583
|
+
validation: {
|
|
584
|
+
valid: true,
|
|
585
|
+
errors: [],
|
|
586
|
+
warnings: validation.warnings,
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
writeJson(paths.jsonPath, json);
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
schema_version: SCHEMA_VERSION,
|
|
593
|
+
command: 'intent save',
|
|
594
|
+
saved: true,
|
|
595
|
+
source: options.source || null,
|
|
596
|
+
intent_id: validation.intent_id,
|
|
597
|
+
status: validation.status,
|
|
598
|
+
paths: json.paths,
|
|
599
|
+
warnings: validation.warnings,
|
|
600
|
+
intent: json,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function loadSavedIntentJson(root, cwd, idOrPath) {
|
|
605
|
+
const target = resolveIntentTarget(root, cwd, idOrPath);
|
|
606
|
+
if (!exists(target.jsonPath)) return null;
|
|
607
|
+
return readJson(target.jsonPath);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
module.exports = {
|
|
611
|
+
INTENT_ID_PATTERN,
|
|
612
|
+
SCHEMA_VERSION,
|
|
613
|
+
deriveFindIntent,
|
|
614
|
+
loadSavedIntentJson,
|
|
615
|
+
parseStartIntentMarkdown,
|
|
616
|
+
readIntentTarget,
|
|
617
|
+
saveStartIntent,
|
|
618
|
+
validateIntentTarget,
|
|
619
|
+
validateStartIntentMarkdown,
|
|
620
|
+
};
|