heyspex 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 +157 -0
- package/bin/hs.mjs +1028 -0
- package/bin/postinstall.mjs +88 -0
- package/dev-factory/lib/fleet-readiness.mjs +274 -0
- package/dev-factory/lib/fleet-state.mjs +250 -0
- package/dev-factory/lib/sufficiency-score.mjs +441 -0
- package/dist/cli/hs-check.mjs +747 -0
- package/dist/cli/hs-claim.mjs +541 -0
- package/dist/cli/hs-connect.mjs +3436 -0
- package/dist/cli/hs-defer.mjs +8455 -0
- package/dist/cli/hs-env.mjs +36845 -0
- package/dist/cli/hs-init.mjs +1145 -0
- package/dist/cli/hs-install.mjs +1390 -0
- package/dist/cli/hs-join.mjs +38553 -0
- package/dist/cli/hs-vault-pull.mjs +7782 -0
- package/dist/cli/hs-vault.mjs +4166 -0
- package/dist/cli/hs-workspace.mjs +2898 -0
- package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
- package/docs/mcp-client-config.md +200 -0
- package/package.json +124 -0
- package/scripts/check-mcp-freshness.mjs +379 -0
- package/scripts/dev-server-status.mjs +722 -0
- package/scripts/gateway-skills-lib.mjs +421 -0
- package/scripts/lib/console-url-resolver.mjs +152 -0
- package/scripts/lib/dev-mode.mjs +369 -0
- package/scripts/lib/hs-link.mjs +154 -0
- package/scripts/lib/hs-source-commit.mjs +207 -0
- package/scripts/lib/hs-source-log.mjs +366 -0
- package/scripts/lib/hs-source-preflight.mjs +402 -0
- package/scripts/lib/hs-source-push.mjs +307 -0
- package/scripts/lib/hs-source-secret-scan.mjs +263 -0
- package/scripts/lib/standalone-doctor.mjs +89 -0
- package/scripts/vault-id-lib.mjs +173 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
- package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
- package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
- package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
- package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
- package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
- package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
- package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
- package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
- package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
- package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
- package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
- package/vendor/heyspex-mcp/cli.mjs +15754 -0
- package/vendor/heyspex-mcp/config-settings.mjs +419 -0
- package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
- package/vendor/heyspex-mcp/design-tools.mjs +449 -0
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
- package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
- package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
- package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
- package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
- package/vendor/heyspex-mcp/next-review.mjs +548 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
- package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
- package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
- package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
- package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
- package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
- package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
- package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
- package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
- package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
- package/vendor/heyspex-mcp/read-path.mjs +138 -0
- package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
- package/vendor/heyspex-mcp/register-spec.mjs +417 -0
- package/vendor/heyspex-mcp/research-focus.mjs +476 -0
- package/vendor/heyspex-mcp/research-tools.mjs +109 -0
- package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
- package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
- package/vendor/heyspex-mcp/review-focus.mjs +55 -0
- package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
- package/vendor/heyspex-mcp/run-front.mjs +60 -0
- package/vendor/heyspex-mcp/scheduler.mjs +813 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
- package/vendor/heyspex-mcp/split-research.mjs +832 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
- package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
- package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
- package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
- package/vendor/heyspex-mcp/why-now.mjs +64 -0
- package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest external / proprietary plan documents into HeySpex vault specs.
|
|
3
|
+
* Cursor plans live under ~/.cursor/plans/ (or repo .cursor/plans/) and are not
|
|
4
|
+
* portable to Claude Code — vault specs must own canonical plan content.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join, basename, isAbsolute, normalize } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const CANONICAL_SECTION_HEADING = '## Canonical implementation plan (HeySpex)';
|
|
12
|
+
const CANONICAL_SECTION_RE =
|
|
13
|
+
/## Canonical implementation plan \(HeySpex\)[\s\S]*?(?=\n## Change log|\n## NEXT STEP:|\n## NEXT STEP\s|$)/i;
|
|
14
|
+
|
|
15
|
+
/** @typedef {{ ref: string, resolvedPath: string | null, label: string }} PlanRef */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect external plan path references in markdown.
|
|
19
|
+
* @param {string} markdown
|
|
20
|
+
* @returns {PlanRef[]}
|
|
21
|
+
*/
|
|
22
|
+
export function extractExternalPlanRefs(markdown) {
|
|
23
|
+
if (!markdown?.trim()) return [];
|
|
24
|
+
|
|
25
|
+
const refs = [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
|
|
28
|
+
const add = (ref, label = basename(ref)) => {
|
|
29
|
+
const normalized = ref.replace(/\\/g, '/').trim();
|
|
30
|
+
if (!normalized || seen.has(normalized)) return;
|
|
31
|
+
if (normalized.includes('*')) return; // prose globs like .cursor/plans/*.plan.md
|
|
32
|
+
if (!/\.plan\.md$/i.test(normalized) && !/\.cursor\/plans\//i.test(normalized)) return;
|
|
33
|
+
seen.add(normalized);
|
|
34
|
+
refs.push({ ref: normalized, resolvedPath: null, label });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Backtick paths
|
|
38
|
+
for (const m of markdown.matchAll(/`([^`\n]+\.plan\.md)`/gi)) {
|
|
39
|
+
add(m[1]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Bold labels: **Plan:** `path`
|
|
43
|
+
for (const m of markdown.matchAll(
|
|
44
|
+
/\*\*(?:Full plan|Plan|Layout plan|Implementation plan[^*]*)\*\*:?\s*`([^`]+)`/gi,
|
|
45
|
+
)) {
|
|
46
|
+
add(m[1]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Implementation plan line with middle dot separators
|
|
50
|
+
for (const m of markdown.matchAll(
|
|
51
|
+
/\*\*Implementation plan \(Cursor\):\*\*\s*`([^`]+)`/gi,
|
|
52
|
+
)) {
|
|
53
|
+
add(m[1]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return refs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolve a plan reference to an absolute file path.
|
|
61
|
+
* @param {string} vaultRoot
|
|
62
|
+
* @param {string} ref
|
|
63
|
+
* @returns {string | null}
|
|
64
|
+
*/
|
|
65
|
+
export function resolvePlanPath(vaultRoot, ref) {
|
|
66
|
+
const cleaned = ref.replace(/\\/g, '/').trim();
|
|
67
|
+
const candidates = [];
|
|
68
|
+
|
|
69
|
+
if (isAbsolute(cleaned)) {
|
|
70
|
+
candidates.push(normalize(cleaned));
|
|
71
|
+
} else {
|
|
72
|
+
candidates.push(normalize(join(vaultRoot, cleaned)));
|
|
73
|
+
|
|
74
|
+
const planBasename = basename(cleaned);
|
|
75
|
+
if (cleaned.includes('.cursor/plans/') || cleaned.startsWith('.cursor/plans/')) {
|
|
76
|
+
candidates.push(normalize(join(homedir(), '.cursor', 'plans', planBasename)));
|
|
77
|
+
candidates.push(normalize(join(vaultRoot, '.cursor', 'plans', planBasename)));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const p of candidates) {
|
|
82
|
+
if (existsSync(p)) return p;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Strip Cursor plan YAML frontmatter (--- name/overview/todos ---).
|
|
89
|
+
* @param {string} raw
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
export function stripPlanFrontmatter(raw) {
|
|
93
|
+
if (!raw.startsWith('---')) return raw.trim();
|
|
94
|
+
const end = raw.indexOf('\n---', 3);
|
|
95
|
+
if (end === -1) return raw.trim();
|
|
96
|
+
return raw.slice(end + 4).trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} resolvedPath
|
|
101
|
+
* @param {string} ref
|
|
102
|
+
* @returns {{ content: string, sourceLabel: string }}
|
|
103
|
+
*/
|
|
104
|
+
function readPlanFile(resolvedPath, ref) {
|
|
105
|
+
const raw = readFileSync(resolvedPath, 'utf8');
|
|
106
|
+
return {
|
|
107
|
+
content: stripPlanFrontmatter(raw),
|
|
108
|
+
sourceLabel: basename(ref) || basename(resolvedPath),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build the canonical ingested section for vault specs.
|
|
114
|
+
* @param {Array<{ sourceLabel: string, content: string, resolvedPath: string }>} plans
|
|
115
|
+
* @param {string} [ingestedAt]
|
|
116
|
+
*/
|
|
117
|
+
export function buildCanonicalPlanSection(plans, ingestedAt = new Date().toISOString().slice(0, 10)) {
|
|
118
|
+
if (!plans.length) return '';
|
|
119
|
+
|
|
120
|
+
const lines = [
|
|
121
|
+
CANONICAL_SECTION_HEADING,
|
|
122
|
+
'',
|
|
123
|
+
`> **Ingested:** ${ingestedAt}. Treat **this section** as the portable source of truth.`,
|
|
124
|
+
`> External paths (e.g. \`.cursor/plans/*.plan.md\`) are provenance only — Claude Code and other agents may not read them.`,
|
|
125
|
+
'',
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
for (const plan of plans) {
|
|
129
|
+
lines.push(`### Source: ${plan.sourceLabel}`, '');
|
|
130
|
+
if (plan.resolvedPath) {
|
|
131
|
+
lines.push(`_Provenance: \`${plan.resolvedPath.replace(/\\/g, '/')}\`_`, '');
|
|
132
|
+
}
|
|
133
|
+
lines.push(plan.content.trim(), '', '---', '');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return lines.join('\n').trimEnd();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Remove dangling external-only plan pointer lines (replaced by canonical section).
|
|
141
|
+
* @param {string} markdown
|
|
142
|
+
*/
|
|
143
|
+
export function stripExternalPlanPointers(markdown) {
|
|
144
|
+
let out = markdown;
|
|
145
|
+
const patterns = [
|
|
146
|
+
/^\*\*(?:Full plan|Plan|Layout plan)\*\*:?\s*`[^`\n]+\.plan\.md`.*\n/gm,
|
|
147
|
+
/^\*\*Implementation plan \(Cursor\):\*\*[^\n]*\n/gm,
|
|
148
|
+
/^> \*\*Layout plan:\*\*[^\n]*\n/gm,
|
|
149
|
+
];
|
|
150
|
+
for (const re of patterns) {
|
|
151
|
+
out = out.replace(re, '');
|
|
152
|
+
}
|
|
153
|
+
return out.replace(/\n{3,}/g, '\n\n').trim();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Replace external plan refs in text with vault-local pointer.
|
|
158
|
+
* @param {string} markdown
|
|
159
|
+
*/
|
|
160
|
+
export function rewritePlanRefsToCanonical(markdown) {
|
|
161
|
+
let out = markdown;
|
|
162
|
+
out = out.replace(
|
|
163
|
+
/`([^`\n]*\.cursor\/plans\/[^`]+\.plan\.md)`/gi,
|
|
164
|
+
'[[#canonical-implementation-plan-heyspex]] (in this spec)',
|
|
165
|
+
);
|
|
166
|
+
out = out.replace(
|
|
167
|
+
/\*\*(?:Full plan|Plan)\*\*:?\s*`[^`]+`/gi,
|
|
168
|
+
'**Canonical plan:** see ## Canonical implementation plan (HeySpex) in this spec',
|
|
169
|
+
);
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} vaultRoot
|
|
175
|
+
* @param {string} markdown
|
|
176
|
+
* @param {{ extraPaths?: string[] }} [options]
|
|
177
|
+
*/
|
|
178
|
+
export function resolveAndReadPlans(vaultRoot, markdown, options = {}) {
|
|
179
|
+
const refs = extractExternalPlanRefs(markdown);
|
|
180
|
+
const extraPaths = options.extraPaths || [];
|
|
181
|
+
const byRef = new Map();
|
|
182
|
+
|
|
183
|
+
for (const r of refs) {
|
|
184
|
+
const resolved = resolvePlanPath(vaultRoot, r.ref);
|
|
185
|
+
byRef.set(r.ref, { ...r, resolvedPath: resolved });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const p of extraPaths) {
|
|
189
|
+
const ref = p.replace(/\\/g, '/');
|
|
190
|
+
if (!byRef.has(ref)) {
|
|
191
|
+
byRef.set(ref, {
|
|
192
|
+
ref,
|
|
193
|
+
resolvedPath: resolvePlanPath(vaultRoot, ref),
|
|
194
|
+
label: basename(ref),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @type {Array<{ sourceLabel: string, content: string, resolvedPath: string, ref: string }>} */
|
|
200
|
+
const ingested = [];
|
|
201
|
+
/** @type {Array<{ ref: string, resolvedPath: string | null }>} */
|
|
202
|
+
const missing = [];
|
|
203
|
+
|
|
204
|
+
for (const entry of byRef.values()) {
|
|
205
|
+
if (!entry.resolvedPath) {
|
|
206
|
+
missing.push({ ref: entry.ref, resolvedPath: null });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const { content, sourceLabel } = readPlanFile(entry.resolvedPath, entry.ref);
|
|
211
|
+
ingested.push({
|
|
212
|
+
sourceLabel,
|
|
213
|
+
content,
|
|
214
|
+
resolvedPath: entry.resolvedPath,
|
|
215
|
+
ref: entry.ref,
|
|
216
|
+
});
|
|
217
|
+
} catch {
|
|
218
|
+
missing.push({ ref: entry.ref, resolvedPath: entry.resolvedPath });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { ingested, missing, refs: [...byRef.values()] };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Insert canonical plan before change log / NEXT STEP tail sections.
|
|
227
|
+
* @param {string} body
|
|
228
|
+
* @param {string} section
|
|
229
|
+
*/
|
|
230
|
+
function insertCanonicalBeforeTailSections(body, section) {
|
|
231
|
+
const markers = ['\n## Change log', '\n## NEXT STEP'];
|
|
232
|
+
for (const marker of markers) {
|
|
233
|
+
const idx = body.indexOf(marker);
|
|
234
|
+
if (idx !== -1) {
|
|
235
|
+
return `${body.slice(0, idx).trim()}\n\n${section}\n${body.slice(idx)}`;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return `${body.trim()}\n\n${section}\n`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Merge external plans into vault markdown (epic body, task body, or prompt).
|
|
243
|
+
* @param {string} vaultRoot
|
|
244
|
+
* @param {string} markdown
|
|
245
|
+
* @param {{ extraPaths?: string[], force?: boolean }} [options]
|
|
246
|
+
*/
|
|
247
|
+
export function ingestPlansIntoMarkdown(vaultRoot, markdown, options = {}) {
|
|
248
|
+
const hasCanonical = CANONICAL_SECTION_RE.test(markdown);
|
|
249
|
+
if (hasCanonical && !options.force) {
|
|
250
|
+
return {
|
|
251
|
+
markdown,
|
|
252
|
+
ingested: [],
|
|
253
|
+
missing: [],
|
|
254
|
+
already_canonical: true,
|
|
255
|
+
changed: false,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const { ingested, missing } = resolveAndReadPlans(vaultRoot, markdown, {
|
|
260
|
+
extraPaths: options.extraPaths,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (ingested.length === 0) {
|
|
264
|
+
return { markdown, ingested, missing, already_canonical: false, changed: false };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let body = stripExternalPlanPointers(markdown);
|
|
268
|
+
body = rewritePlanRefsToCanonical(body);
|
|
269
|
+
|
|
270
|
+
if (hasCanonical && options.force) {
|
|
271
|
+
body = body.replace(CANONICAL_SECTION_RE, '').trim();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const section = buildCanonicalPlanSection(ingested);
|
|
275
|
+
const markdownOut = insertCanonicalBeforeTailSections(body, section);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
markdown: markdownOut,
|
|
279
|
+
ingested,
|
|
280
|
+
missing,
|
|
281
|
+
already_canonical: false,
|
|
282
|
+
changed: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Prepare epic + implementation prompt for vault write (hs-push / hs-push-next).
|
|
288
|
+
* @param {string} vaultRoot
|
|
289
|
+
* @param {{ plan_markdown?: string, implementation_prompt?: string, source_plan_paths?: string[] }} args
|
|
290
|
+
*/
|
|
291
|
+
export function prepareVaultPlanContent(vaultRoot, args) {
|
|
292
|
+
const extraPaths = args.source_plan_paths || [];
|
|
293
|
+
|
|
294
|
+
const planResult = ingestPlansIntoMarkdown(vaultRoot, args.plan_markdown || '', { extraPaths });
|
|
295
|
+
const promptSource = args.implementation_prompt || '';
|
|
296
|
+
const promptIngest = ingestPlansIntoMarkdown(vaultRoot, promptSource, { extraPaths });
|
|
297
|
+
|
|
298
|
+
/** @type {typeof planResult.ingested} */
|
|
299
|
+
const allIngested = [];
|
|
300
|
+
const seenPaths = new Set();
|
|
301
|
+
for (const item of [...planResult.ingested, ...promptIngest.ingested]) {
|
|
302
|
+
if (seenPaths.has(item.resolvedPath)) continue;
|
|
303
|
+
seenPaths.add(item.resolvedPath);
|
|
304
|
+
allIngested.push(item);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const missing = [...planResult.missing, ...promptIngest.missing].filter(
|
|
308
|
+
(m, i, arr) => arr.findIndex((x) => x.ref === m.ref) === i,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
let planMarkdown = planResult.markdown;
|
|
312
|
+
let implementationPrompt = promptIngest.markdown;
|
|
313
|
+
|
|
314
|
+
// If plans only appeared in prompt, append canonical section to epic body
|
|
315
|
+
if (allIngested.length > 0 && !CANONICAL_SECTION_RE.test(planMarkdown)) {
|
|
316
|
+
const section = buildCanonicalPlanSection(allIngested);
|
|
317
|
+
planMarkdown = insertCanonicalBeforeTailSections(planMarkdown, section);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (allIngested.length > 0) {
|
|
321
|
+
implementationPrompt = rewritePlanRefsToCanonical(
|
|
322
|
+
stripExternalPlanPointers(implementationPrompt),
|
|
323
|
+
);
|
|
324
|
+
if (!/canonical implementation plan/i.test(implementationPrompt)) {
|
|
325
|
+
implementationPrompt = `${implementationPrompt.trim()}\n\n**Canonical plan:** Full detail is in the epic spec under ## Canonical implementation plan (HeySpex).\n`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
plan_markdown: planMarkdown,
|
|
331
|
+
implementation_prompt: implementationPrompt,
|
|
332
|
+
ingested_plans: allIngested.map((p) => ({
|
|
333
|
+
source: p.sourceLabel,
|
|
334
|
+
path: p.resolvedPath.replace(/\\/g, '/'),
|
|
335
|
+
})),
|
|
336
|
+
missing_plans: missing,
|
|
337
|
+
plans_ingested: allIngested.length > 0,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Extract canonical plan section from spec body for hs-next prompts.
|
|
343
|
+
* @param {string} body
|
|
344
|
+
* @returns {string | null}
|
|
345
|
+
*/
|
|
346
|
+
export function extractCanonicalPlanSection(body) {
|
|
347
|
+
const m = body.match(CANONICAL_SECTION_RE);
|
|
348
|
+
return m ? m[0].trim() : null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Enrich agent prompt with canonical + resolvable external plan content.
|
|
353
|
+
* @param {string} vaultRoot
|
|
354
|
+
* @param {string} specRelPath
|
|
355
|
+
* @param {string} specBody
|
|
356
|
+
* @param {string} promptMarkdown
|
|
357
|
+
* @param {{ persist?: boolean }} [options]
|
|
358
|
+
*/
|
|
359
|
+
export function hydratePromptWithPlans(vaultRoot, specRelPath, specBody, promptMarkdown, options = {}) {
|
|
360
|
+
const canonical = extractCanonicalPlanSection(specBody);
|
|
361
|
+
const { ingested, missing } = resolveAndReadPlans(
|
|
362
|
+
vaultRoot,
|
|
363
|
+
`${specBody}\n\n${promptMarkdown}`,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
const parts = [promptMarkdown.trim()];
|
|
367
|
+
|
|
368
|
+
if (canonical) {
|
|
369
|
+
parts.push('', '---', '', canonical);
|
|
370
|
+
} else if (ingested.length > 0) {
|
|
371
|
+
parts.push(
|
|
372
|
+
'',
|
|
373
|
+
'---',
|
|
374
|
+
'',
|
|
375
|
+
'> **Plan ingest (runtime):** External plan file(s) were resolved for this session. Re-run **hs-push-next** or persist with `persist_plan_ingest: true` on `heyspex_next_on_deck` to write into the vault spec.',
|
|
376
|
+
'',
|
|
377
|
+
buildCanonicalPlanSection(ingested),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (missing.length > 0) {
|
|
382
|
+
parts.push(
|
|
383
|
+
'',
|
|
384
|
+
'---',
|
|
385
|
+
'',
|
|
386
|
+
'### ⚠️ Unresolved plan sources',
|
|
387
|
+
'',
|
|
388
|
+
...missing.map(
|
|
389
|
+
(m) => `- \`${m.ref}\` — not found (checked vault root and ~/.cursor/plans/)`,
|
|
390
|
+
),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let persistResult = null;
|
|
395
|
+
if (options.persist && ingested.length > 0 && !canonical) {
|
|
396
|
+
const merged = ingestPlansIntoMarkdown(vaultRoot, specBody, { force: false });
|
|
397
|
+
if (merged.changed) {
|
|
398
|
+
const fullPath = join(vaultRoot, specRelPath);
|
|
399
|
+
writeFileSync(fullPath, patchSpecBody(fullPath, merged.markdown), 'utf8');
|
|
400
|
+
persistResult = { spec_path: specRelPath, written: true };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
prompt_markdown: parts.filter(Boolean).join('\n'),
|
|
406
|
+
plans_in_prompt: ingested.map((p) => p.sourceLabel),
|
|
407
|
+
missing_plans: missing,
|
|
408
|
+
had_canonical_in_spec: Boolean(canonical),
|
|
409
|
+
persist_result: persistResult,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Replace spec body while preserving frontmatter.
|
|
415
|
+
* @param {string} filePath
|
|
416
|
+
* @param {string} newBody
|
|
417
|
+
*/
|
|
418
|
+
function patchSpecBody(filePath, newBody) {
|
|
419
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
420
|
+
const fmEnd = raw.indexOf('\n---', 3);
|
|
421
|
+
if (!raw.startsWith('---') || fmEnd === -1) {
|
|
422
|
+
return newBody;
|
|
423
|
+
}
|
|
424
|
+
const frontmatter = raw.slice(0, fmEnd + 4);
|
|
425
|
+
return `${frontmatter}\n\n${newBody.trim()}\n`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Ingest plans into an on-disk spec file (repair / hs-next persist).
|
|
430
|
+
* @param {string} vaultRoot
|
|
431
|
+
* @param {string} specRelPath
|
|
432
|
+
*/
|
|
433
|
+
export function ingestPlansIntoSpecFile(vaultRoot, specRelPath) {
|
|
434
|
+
const fullPath = join(vaultRoot, specRelPath);
|
|
435
|
+
if (!existsSync(fullPath)) {
|
|
436
|
+
return { error: 'SPEC_NOT_FOUND', message: specRelPath };
|
|
437
|
+
}
|
|
438
|
+
const raw = readFileSync(fullPath, 'utf8');
|
|
439
|
+
const fmEnd = raw.indexOf('\n---', 3);
|
|
440
|
+
const body = fmEnd === -1 ? raw : raw.slice(fmEnd + 4);
|
|
441
|
+
const result = ingestPlansIntoMarkdown(vaultRoot, body, { force: false });
|
|
442
|
+
if (!result.changed) {
|
|
443
|
+
return {
|
|
444
|
+
spec_path: specRelPath,
|
|
445
|
+
changed: false,
|
|
446
|
+
ingested_plans: result.ingested,
|
|
447
|
+
missing_plans: result.missing,
|
|
448
|
+
already_canonical: result.already_canonical,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
writeFileSync(fullPath, patchSpecBody(fullPath, result.markdown), 'utf8');
|
|
452
|
+
return {
|
|
453
|
+
spec_path: specRelPath,
|
|
454
|
+
changed: true,
|
|
455
|
+
ingested_plans: result.ingested.map((p) => ({
|
|
456
|
+
source: p.sourceLabel,
|
|
457
|
+
path: p.resolvedPath.replace(/\\/g, '/'),
|
|
458
|
+
})),
|
|
459
|
+
missing_plans: result.missing,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ── Minimal self-test: node vendor/heyspex-mcp/plan-ingest.mjs
|
|
464
|
+
function runSelfTest() {
|
|
465
|
+
const refs = extractExternalPlanRefs(
|
|
466
|
+
'**Plan:** `.cursor/plans/foo.plan.md` and `C:\\Users\\x\\.cursor\\plans\\bar.plan.md`',
|
|
467
|
+
);
|
|
468
|
+
console.assert(refs.length >= 1, 'extract refs');
|
|
469
|
+
const stripped = stripPlanFrontmatter('---\nname: x\n---\n\n# Body\n');
|
|
470
|
+
console.assert(stripped.startsWith('# Body'), 'strip frontmatter');
|
|
471
|
+
console.log('plan-ingest.mjs: ok');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (process.argv[1]?.endsWith('plan-ingest.mjs')) {
|
|
475
|
+
runSelfTest();
|
|
476
|
+
}
|