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.
Files changed (104) hide show
  1. package/README.md +157 -0
  2. package/bin/hs.mjs +1028 -0
  3. package/bin/postinstall.mjs +88 -0
  4. package/dev-factory/lib/fleet-readiness.mjs +274 -0
  5. package/dev-factory/lib/fleet-state.mjs +250 -0
  6. package/dev-factory/lib/sufficiency-score.mjs +441 -0
  7. package/dist/cli/hs-check.mjs +747 -0
  8. package/dist/cli/hs-claim.mjs +541 -0
  9. package/dist/cli/hs-connect.mjs +3436 -0
  10. package/dist/cli/hs-defer.mjs +8455 -0
  11. package/dist/cli/hs-env.mjs +36845 -0
  12. package/dist/cli/hs-init.mjs +1145 -0
  13. package/dist/cli/hs-install.mjs +1390 -0
  14. package/dist/cli/hs-join.mjs +38553 -0
  15. package/dist/cli/hs-vault-pull.mjs +7782 -0
  16. package/dist/cli/hs-vault.mjs +4166 -0
  17. package/dist/cli/hs-workspace.mjs +2898 -0
  18. package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
  19. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
  20. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
  21. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
  22. package/docs/mcp-client-config.md +200 -0
  23. package/package.json +124 -0
  24. package/scripts/check-mcp-freshness.mjs +379 -0
  25. package/scripts/dev-server-status.mjs +722 -0
  26. package/scripts/gateway-skills-lib.mjs +421 -0
  27. package/scripts/lib/console-url-resolver.mjs +152 -0
  28. package/scripts/lib/dev-mode.mjs +369 -0
  29. package/scripts/lib/hs-link.mjs +154 -0
  30. package/scripts/lib/hs-source-commit.mjs +207 -0
  31. package/scripts/lib/hs-source-log.mjs +366 -0
  32. package/scripts/lib/hs-source-preflight.mjs +402 -0
  33. package/scripts/lib/hs-source-push.mjs +307 -0
  34. package/scripts/lib/hs-source-secret-scan.mjs +263 -0
  35. package/scripts/lib/standalone-doctor.mjs +89 -0
  36. package/scripts/vault-id-lib.mjs +173 -0
  37. package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
  38. package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
  39. package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
  40. package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
  41. package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
  42. package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
  43. package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
  44. package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
  45. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
  46. package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
  47. package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
  48. package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
  49. package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
  50. package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
  51. package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
  52. package/vendor/heyspex-mcp/cli.mjs +15754 -0
  53. package/vendor/heyspex-mcp/config-settings.mjs +419 -0
  54. package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
  55. package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
  56. package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
  57. package/vendor/heyspex-mcp/design-tools.mjs +449 -0
  58. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
  59. package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
  60. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
  61. package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
  62. package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
  63. package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
  64. package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
  65. package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
  66. package/vendor/heyspex-mcp/next-review.mjs +548 -0
  67. package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
  68. package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
  69. package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
  70. package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
  71. package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
  72. package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
  73. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  74. package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
  75. package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
  76. package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
  77. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
  78. package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
  79. package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
  80. package/vendor/heyspex-mcp/read-path.mjs +138 -0
  81. package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
  82. package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
  83. package/vendor/heyspex-mcp/register-spec.mjs +417 -0
  84. package/vendor/heyspex-mcp/research-focus.mjs +476 -0
  85. package/vendor/heyspex-mcp/research-tools.mjs +109 -0
  86. package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
  87. package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
  88. package/vendor/heyspex-mcp/review-focus.mjs +55 -0
  89. package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
  90. package/vendor/heyspex-mcp/run-front.mjs +60 -0
  91. package/vendor/heyspex-mcp/scheduler.mjs +813 -0
  92. package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
  93. package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
  94. package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
  95. package/vendor/heyspex-mcp/split-research.mjs +832 -0
  96. package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
  97. package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
  98. package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
  99. package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
  100. package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
  101. package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
  102. package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
  103. package/vendor/heyspex-mcp/why-now.mjs +64 -0
  104. package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Conflict / drift analysis for hs-ground — same engine as console On Deck
3
+ * ([[HS-EPIC-6RKN-spec-conflict-detection-pre-flight-check]], GET /api/specs/:id/conflicts).
4
+ */
5
+
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { join, basename as pathBasename } from 'node:path';
8
+ import {
9
+ detectSpecConflicts,
10
+ extractBehavioralClaims,
11
+ findConflicts,
12
+ resolveWikilink,
13
+ extractWikilinks,
14
+ } from './plan-push.mjs';
15
+
16
+ const SOURCE_OF_TRUTH_RULES = [
17
+ {
18
+ layer: 'L1',
19
+ spec: 'heyspex-north-star',
20
+ rule: 'North-star wins on vision and product intent conflicts.',
21
+ },
22
+ {
23
+ layer: 'L2',
24
+ spec: 'architecture-pillars',
25
+ rule: 'Pillars win on milestone scope and system-shape conflicts.',
26
+ },
27
+ {
28
+ layer: 'memory-decision',
29
+ rule: 'Memory decisions/lessons are authoritative for documented implementation choices.',
30
+ },
31
+ {
32
+ layer: 'protocol',
33
+ rule: 'Default: **protocol specs** are source of truth for stable product behavior.',
34
+ },
35
+ {
36
+ layer: 'epic',
37
+ rule: 'Epics may supersede protocols only with explicit override ([[hs-resolve]] Option A/C).',
38
+ },
39
+ {
40
+ layer: 'console',
41
+ rule: 'Focused epic conflicts use the same detector as Console **On Deck** and **Specs** (`/api/specs/:id/conflicts`).',
42
+ },
43
+ ];
44
+
45
+ function readSpecContent(vaultRoot, specBasename) {
46
+ const path = resolveWikilink(vaultRoot, specBasename);
47
+ if (!path) return null;
48
+ try {
49
+ return readFileSync(path, 'utf8');
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function parseFrontmatter(content) {
56
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
57
+ if (!match) return { frontmatter: {}, body: content };
58
+ const body = content.slice(match[0].length).trim();
59
+ const frontmatter = {};
60
+ const lines = match[1].split(/\r?\n/);
61
+ let currentArray = null;
62
+ for (const line of lines) {
63
+ if (!line.trim()) continue;
64
+ const arrayItem = line.match(/^\s+-\s*(.*)$/);
65
+ if (arrayItem && currentArray) {
66
+ currentArray.push(arrayItem[1].trim().replace(/^["']|["']$/g, ''));
67
+ continue;
68
+ }
69
+ const keyMatch = line.match(/^(\w[\w_-]*):\s*(.*)$/);
70
+ if (keyMatch) {
71
+ const key = keyMatch[1];
72
+ const value = keyMatch[2].trim();
73
+ currentArray = null;
74
+ if (value === '') {
75
+ frontmatter[key] = [];
76
+ currentArray = frontmatter[key];
77
+ } else {
78
+ frontmatter[key] = value.replace(/^["']|["']$/g, '');
79
+ }
80
+ }
81
+ }
82
+ return { frontmatter, body };
83
+ }
84
+
85
+ function normalizeConflict(raw, meta) {
86
+ return {
87
+ ...raw,
88
+ source: meta.source,
89
+ severity: meta.severity || 'blocker',
90
+ spec_a: meta.spec_a || raw.epic_source || null,
91
+ spec_b: meta.spec_b || raw.spec,
92
+ recommended_source_of_truth: meta.recommended_source_of_truth || 'protocol',
93
+ rationale: meta.rationale || SOURCE_OF_TRUTH_RULES[3].rule,
94
+ resolve_before_context: meta.resolve_before_context !== false,
95
+ console_ui_note:
96
+ meta.source === 'focused_epic'
97
+ ? 'Also visible in Console On Deck / Specs editor for the focused epic.'
98
+ : null,
99
+ };
100
+ }
101
+
102
+ function detectPairwiseDrift(vaultRoot, basenameA, basenameB) {
103
+ const contentA = readSpecContent(vaultRoot, basenameA);
104
+ const contentB = readSpecContent(vaultRoot, basenameB);
105
+ if (!contentA || !contentB) return [];
106
+
107
+ const claimsA = extractBehavioralClaims(contentA, basenameA);
108
+ const claimsB = extractBehavioralClaims(contentB, basenameB);
109
+ if (claimsA.length === 0 || claimsB.length === 0) return [];
110
+
111
+ const aVsB = findConflicts(claimsA, claimsB);
112
+ const bVsA = findConflicts(claimsB, claimsA);
113
+
114
+ const merged = [];
115
+ const seen = new Set();
116
+
117
+ for (const c of [...aVsB, ...bVsA]) {
118
+ const key = [basenameA, basenameB, c.action, c.spec_claim].sort().join('|');
119
+ if (seen.has(key)) continue;
120
+ seen.add(key);
121
+ merged.push(
122
+ normalizeConflict(c, {
123
+ source: 'cross_spec_drift',
124
+ spec_a: basenameA,
125
+ spec_b: basenameB,
126
+ epic_source: basenameA,
127
+ recommended_source_of_truth: 'resolve_interactively',
128
+ rationale: `Drift between [[${basenameA}]] and [[${basenameB}]] — pick one authority (usually protocol for behavior, epic only if superseding).`,
129
+ resolve_before_context: true,
130
+ }),
131
+ );
132
+ }
133
+ return merged;
134
+ }
135
+
136
+ /**
137
+ * @param {string} vaultRoot
138
+ * @param {object} options
139
+ * @param {string} [options.topic]
140
+ * @param {string} [options.summary] - draft plan / chat description
141
+ * @param {string[]} [options.protocolBasenames]
142
+ * @param {Array<{basename: string, relevance: number, type?: string}>} [options.topSpecs]
143
+ * @param {boolean} [options.check_focus_epic]
144
+ * @param {string} [options.focusSpecPath] - Neon layer-2 focus selection spec_path,
145
+ * injected at the async handler boundary ([[HS-TASK-FSEL]]); takes precedence
146
+ * over the dormant `.heyspex/focus.yaml` on a Neon-up machine.
147
+ */
148
+ export function analyzeGroundingConflicts(vaultRoot, options = {}) {
149
+ const {
150
+ topic = '',
151
+ summary = '',
152
+ protocolBasenames = [],
153
+ topSpecs = [],
154
+ check_focus_epic = true,
155
+ focusSpecPath = null,
156
+ } = options;
157
+
158
+ const allConflicts = [];
159
+ const linkedSpecsChecked = new Set();
160
+ let focusedEpicId = null;
161
+ let focusedEpicPath = null;
162
+
163
+ // 1) Focused epic (same as hs-resolve / console On Deck). The Neon layer-2
164
+ // selection is injected as `focusSpecPath` at the async handler boundary
165
+ // ([[HS-TASK-FSEL]]). There is no `.heyspex/focus.yaml` fallback
166
+ // ([[no-fallbacks-by-default]]) — when the console is unreachable / no focus
167
+ // is set, the focused-epic conflict check is simply skipped.
168
+ if (check_focus_epic) {
169
+ const focusPath = focusSpecPath;
170
+ if (focusPath && existsSync(join(vaultRoot, focusPath))) {
171
+ try {
172
+ const epicContent = readFileSync(join(vaultRoot, focusPath), 'utf8');
173
+ const { frontmatter } = parseFrontmatter(epicContent);
174
+ if (frontmatter.type === 'epic') {
175
+ focusedEpicId = frontmatter.id || pathBasename(focusPath, '.md');
176
+ focusedEpicPath = focusPath;
177
+ const related = extractWikilinks(frontmatter.related || []);
178
+ const { conflicts, linked_specs_checked } = detectSpecConflicts(
179
+ vaultRoot,
180
+ epicContent,
181
+ related,
182
+ );
183
+ linked_specs_checked.forEach((s) => linkedSpecsChecked.add(s));
184
+ for (const c of conflicts) {
185
+ allConflicts.push(
186
+ normalizeConflict(c, {
187
+ source: 'focused_epic',
188
+ spec_a: focusedEpicId,
189
+ spec_b: c.spec,
190
+ recommended_source_of_truth: 'protocol',
191
+ rationale:
192
+ 'On-deck epic vs linked protocol — resolve via /hs-resolve before grounding a new plan on stale claims.',
193
+ resolve_before_context: true,
194
+ }),
195
+ );
196
+ }
197
+ }
198
+ } catch {
199
+ /* skip */
200
+ }
201
+ }
202
+ }
203
+
204
+ // 2) Draft plan (topic + summary) vs governing protocols
205
+ const draftBody = [
206
+ `# Draft plan: ${topic}`,
207
+ '',
208
+ summary || topic,
209
+ '',
210
+ '## Proposed behavior',
211
+ '',
212
+ summary,
213
+ ].join('\n');
214
+
215
+ const protocolSet = [
216
+ ...new Set([
217
+ ...protocolBasenames,
218
+ ...topSpecs.filter((s) => s.type === 'protocol' || String(s.basename).includes('protocol')).map((s) => s.basename),
219
+ ]),
220
+ ].slice(0, 12);
221
+
222
+ if (draftBody.trim().length > 20 && protocolSet.length > 0) {
223
+ const { conflicts, linked_specs_checked } = detectSpecConflicts(
224
+ vaultRoot,
225
+ draftBody,
226
+ protocolSet,
227
+ );
228
+ linked_specs_checked.forEach((s) => linkedSpecsChecked.add(s));
229
+ for (const c of conflicts) {
230
+ allConflicts.push(
231
+ normalizeConflict(c, {
232
+ source: 'draft_plan',
233
+ spec_a: '(draft plan from hs-ground)',
234
+ spec_b: c.spec,
235
+ recommended_source_of_truth: 'protocol',
236
+ rationale:
237
+ 'Draft plan contradicts a governing protocol — align draft or update protocol before using as plan context.',
238
+ resolve_before_context: true,
239
+ }),
240
+ );
241
+ }
242
+ }
243
+
244
+ // 3) Cross-spec drift among high-relevance discoveries (pairwise)
245
+ const driftCandidates = topSpecs
246
+ .filter((s) => s.relevance >= 3)
247
+ .slice(0, 6)
248
+ .map((s) => s.basename);
249
+
250
+ for (let i = 0; i < driftCandidates.length; i++) {
251
+ for (let j = i + 1; j < driftCandidates.length; j++) {
252
+ const pairDrift = detectPairwiseDrift(vaultRoot, driftCandidates[i], driftCandidates[j]);
253
+ allConflicts.push(...pairDrift);
254
+ }
255
+ }
256
+
257
+ const conflictedBasenames = new Set();
258
+ for (const c of allConflicts) {
259
+ if (c.spec_b) conflictedBasenames.add(c.spec_b);
260
+ if (c.spec_a && !String(c.spec_a).includes('draft')) conflictedBasenames.add(c.spec_a);
261
+ if (c.spec_a && c.spec_b) {
262
+ conflictedBasenames.add(c.spec_a);
263
+ conflictedBasenames.add(c.spec_b);
264
+ }
265
+ }
266
+
267
+ const hasBlocking = allConflicts.some((c) => c.resolve_before_context);
268
+
269
+ const recommendations = [];
270
+ if (allConflicts.length > 0) {
271
+ recommendations.push({
272
+ priority: 'blocker',
273
+ action: 'Resolve conflicts before using conflicting specs as authoritative plan context.',
274
+ steps: [
275
+ focusedEpicId
276
+ ? `Run \`/hs-resolve ${focusedEpicId}\` or open Console **On Deck** for the focused epic.`
277
+ : 'Run `/hs-resolve` when an epic is on deck, or fix draft/protocol alignment in chat.',
278
+ 'Pick source of truth per row in `conflicts` (default: protocol for behavior; north-star for vision).',
279
+ 'Re-run `/hs-ground` after resolution so `safe_context_specs` is current.',
280
+ ],
281
+ });
282
+ }
283
+
284
+ if (focusedEpicId && allConflicts.some((c) => c.source === 'focused_epic')) {
285
+ recommendations.push({
286
+ priority: 'info',
287
+ action: 'Console parity',
288
+ steps: [
289
+ `Epic ${focusedEpicId} conflicts match GET /api/specs/${focusedEpicId}/conflicts — same as On Deck UI.`,
290
+ ],
291
+ });
292
+ }
293
+
294
+ return {
295
+ has_blocking_conflicts: hasBlocking,
296
+ conflict_count: allConflicts.length,
297
+ conflicts: allConflicts,
298
+ linked_specs_checked: [...linkedSpecsChecked],
299
+ focused_epic: focusedEpicId
300
+ ? { id: focusedEpicId, path: focusedEpicPath, conflicts_in_ui: true }
301
+ : null,
302
+ source_of_truth_rules: SOURCE_OF_TRUTH_RULES,
303
+ conflicted_spec_basenames: [...conflictedBasenames],
304
+ recommendations,
305
+ resolve_commands: [
306
+ focusedEpicId ? `/hs-resolve ${focusedEpicId}` : '/hs-resolve',
307
+ 'Console: On Deck surface → Spec Conflicts section',
308
+ ],
309
+ };
310
+ }
311
+
312
+ /**
313
+ * @param {string[]} suggestedRelated
314
+ * @param {Set<string>|string[]} conflictedBasenames
315
+ */
316
+ export function computeSafeContextSpecs(suggestedRelated, conflictedBasenames) {
317
+ const blocked = conflictedBasenames instanceof Set
318
+ ? conflictedBasenames
319
+ : new Set(conflictedBasenames);
320
+ return suggestedRelated.filter((b) => !blocked.has(b));
321
+ }