heyspex 0.2.0 → 0.2.2

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 (146) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/bin/hs.mjs +181 -15
  3. package/dist/cli/hs-check.mjs +88 -46
  4. package/dist/cli/hs-claim.mjs +84 -39
  5. package/dist/cli/hs-connect.mjs +583 -129
  6. package/dist/cli/hs-defer.mjs +251 -30
  7. package/dist/cli/hs-dev.mjs +20 -8
  8. package/dist/cli/hs-env.mjs +62 -34
  9. package/dist/cli/hs-index-rebuild.mjs +288 -83
  10. package/dist/cli/hs-index-reconcile.mjs +397 -111
  11. package/dist/cli/hs-init.mjs +2143 -91
  12. package/dist/cli/hs-install.mjs +628 -106
  13. package/dist/cli/hs-join.mjs +686 -236
  14. package/dist/cli/hs-vault-pull.mjs +3189 -179
  15. package/dist/cli/hs-vault-push.mjs +6433 -0
  16. package/dist/cli/hs-vault.mjs +396 -178
  17. package/dist/cli/hs-workspace.mjs +289 -151
  18. package/dist/cli-templates/harness-commands/claude/hs-backfill.md +28 -28
  19. package/dist/cli-templates/harness-commands/claude/hs-capture.md +62 -62
  20. package/dist/cli-templates/harness-commands/claude/hs-defer.md +29 -29
  21. package/dist/cli-templates/harness-commands/claude/hs-ground.md +30 -28
  22. package/dist/cli-templates/harness-commands/claude/hs-next.md +29 -29
  23. package/dist/cli-templates/harness-commands/claude/hs-promote.md +70 -70
  24. package/dist/cli-templates/harness-commands/claude/hs-push.md +35 -35
  25. package/dist/cli-templates/harness-commands/claude/hs-qc.md +27 -27
  26. package/dist/cli-templates/harness-commands/claude/hs-resolve.md +23 -23
  27. package/dist/cli-templates/harness-commands/claude/hs-review.md +23 -23
  28. package/dist/cli-templates/harness-commands/claude/hs-view.md +28 -26
  29. package/dist/cli-templates/harness-commands/claude/kill-ports.md +32 -32
  30. package/dist/cli-templates/harness-commands/claude/prototype.md +91 -91
  31. package/dist/cli-templates/harness-commands/cursor/hs-backfill.md +22 -22
  32. package/dist/cli-templates/harness-commands/cursor/hs-capture.md +56 -56
  33. package/dist/cli-templates/harness-commands/cursor/hs-defer.md +23 -23
  34. package/dist/cli-templates/harness-commands/cursor/hs-ground.md +24 -22
  35. package/dist/cli-templates/harness-commands/cursor/hs-next.md +23 -23
  36. package/dist/cli-templates/harness-commands/cursor/hs-promote.md +64 -64
  37. package/dist/cli-templates/harness-commands/cursor/hs-push.md +29 -29
  38. package/dist/cli-templates/harness-commands/cursor/hs-qc.md +21 -21
  39. package/dist/cli-templates/harness-commands/cursor/hs-resolve.md +17 -17
  40. package/dist/cli-templates/harness-commands/cursor/hs-review.md +17 -17
  41. package/dist/cli-templates/harness-commands/cursor/hs-view.md +24 -22
  42. package/dist/cli-templates/harness-commands/cursor/kill-ports.md +26 -26
  43. package/dist/cli-templates/harness-commands/cursor/prototype.md +86 -86
  44. package/docs/copilot-plugin-install.md +177 -0
  45. package/docs/driving-heyspex-from-copilot.md +165 -0
  46. package/package.json +21 -2
  47. package/plugins/heyspex/.claude-plugin/plugin.json +12 -12
  48. package/plugins/heyspex/.cursor-plugin/plugin.json +15 -15
  49. package/plugins/heyspex/README.md +312 -0
  50. package/plugins/heyspex/bin/run-hook.mjs +20 -2
  51. package/plugins/heyspex/hooks/cursor-hooks.json +81 -0
  52. package/plugins/heyspex/hooks/hooks.json +94 -37
  53. package/scripts/claude-hooks/_intent-classifier-router.mjs +456 -410
  54. package/scripts/claude-hooks/_intent-classifier-semantic.mjs +5 -0
  55. package/scripts/claude-hooks/_run.mjs +316 -23
  56. package/scripts/claude-hooks/capture-execution-gate.mjs +51 -0
  57. package/scripts/claude-hooks/claim-liveness-shared.mjs +2 -0
  58. package/scripts/claude-hooks/diagnose-capture-reminder.mjs +11 -0
  59. package/scripts/claude-hooks/directive-obligation-gate.mjs +11 -0
  60. package/scripts/claude-hooks/inject-communication-style.mjs +20 -0
  61. package/scripts/claude-hooks/intent-classifier-trigger.mjs +279 -247
  62. package/scripts/claude-hooks/session-end.mjs +2 -0
  63. package/scripts/claude-hooks/vault-mirror-catchup.mjs +11 -0
  64. package/scripts/claude-hooks/vault-write-tier-guard.mjs +11 -0
  65. package/scripts/claude-hooks/wip-checkpoint.mjs +5 -1
  66. package/scripts/cursor-hooks/manifest.mjs +46 -0
  67. package/scripts/heyspex-mode.mjs +20 -0
  68. package/scripts/hs-mode.mjs +1 -1
  69. package/scripts/hs-view.mjs +180 -17
  70. package/scripts/install-claude-code-hooks.mjs +3 -1
  71. package/scripts/install-copilot-hooks.mjs +288 -0
  72. package/scripts/lib/console-url-resolver.mjs +3 -2
  73. package/scripts/lib/customer-kit.mjs +327 -10
  74. package/scripts/lib/dev-mode.mjs +7 -6
  75. package/scripts/lib/directive-obligations.mjs +349 -0
  76. package/scripts/lib/hook-dispositions.mjs +210 -0
  77. package/scripts/lib/hook-vault-header.mjs +30 -0
  78. package/scripts/lib/hs-source-branch.mjs +196 -0
  79. package/scripts/lib/hs-source-log.mjs +2 -1
  80. package/scripts/lib/hs-source-preflight.mjs +18 -4
  81. package/scripts/lib/hs-source-push.mjs +386 -356
  82. package/scripts/lib/window-placement.mjs +109 -0
  83. package/spexharness/src/adapters/claude-code.mjs +11 -3
  84. package/spexharness/src/adapters/copilot.mjs +236 -0
  85. package/spexharness/src/adapters/detect-harness.mjs +34 -0
  86. package/spexharness/src/gates/_shell-write-targets.mjs +77 -0
  87. package/spexharness/src/gates/deep-research-cost-gate.mjs +4 -3
  88. package/spexharness/src/gates/diagnose-capture-reminder.mjs +112 -0
  89. package/spexharness/src/gates/directive-obligation-gate.decide.mjs +132 -0
  90. package/spexharness/src/gates/directive-obligation-gate.mjs +183 -0
  91. package/spexharness/src/gates/vault-mirror-catchup.mjs +124 -0
  92. package/spexharness/src/gates/vault-write-tier-guard.decide.mjs +125 -0
  93. package/spexharness/src/gates/vault-write-tier-guard.mjs +42 -0
  94. package/spexharness/src/intent-classifier.mjs +4 -2
  95. package/spexharness/src/intent-directive.mjs +626 -541
  96. package/spexharness/src/rule-table.mjs +31 -1
  97. package/vendor/heyspex-mcp/account-switch.mjs +6 -5
  98. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +31 -3
  99. package/vendor/heyspex-mcp/capture-console-client.mjs +37 -2
  100. package/vendor/heyspex-mcp/capture-focus.mjs +26 -2
  101. package/vendor/heyspex-mcp/capture-outbox.mjs +10 -4
  102. package/vendor/heyspex-mcp/capture-sources.mjs +195 -0
  103. package/vendor/heyspex-mcp/capture-tools.mjs +56 -11
  104. package/vendor/heyspex-mcp/claim-tools.mjs +25 -2
  105. package/vendor/heyspex-mcp/cli-verbs.mjs +19 -21
  106. package/vendor/heyspex-mcp/cli.mjs +7 -1
  107. package/vendor/heyspex-mcp/close-branch-report.mjs +102 -0
  108. package/vendor/heyspex-mcp/cloud-vault-writes.mjs +197 -7
  109. package/vendor/heyspex-mcp/console-fetch.mjs +192 -28
  110. package/vendor/heyspex-mcp/console-transport.mjs +7 -1
  111. package/vendor/heyspex-mcp/credentials.mjs +9 -8
  112. package/vendor/heyspex-mcp/dev-tools.mjs +7 -0
  113. package/vendor/heyspex-mcp/filing-tools.mjs +35 -0
  114. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +2 -1
  115. package/vendor/heyspex-mcp/folder-trust-store.mjs +2 -1
  116. package/vendor/heyspex-mcp/http-transport.mjs +2 -0
  117. package/vendor/heyspex-mcp/list-tools.mjs +1 -1
  118. package/vendor/heyspex-mcp/mcp-config.mjs +14 -1
  119. package/vendor/heyspex-mcp/memory-tools.mjs +523 -523
  120. package/vendor/heyspex-mcp/obligation-backstop.mjs +103 -0
  121. package/vendor/heyspex-mcp/occupied-keys.mjs +1 -0
  122. package/vendor/heyspex-mcp/on-deck-brief.mjs +19 -2
  123. package/vendor/heyspex-mcp/on-deck-scheduler.mjs +1708 -1708
  124. package/vendor/heyspex-mcp/plan-push.mjs +3 -2
  125. package/vendor/heyspex-mcp/preflight-gate.mjs +1 -0
  126. package/vendor/heyspex-mcp/procedure-handlers.mjs +327 -325
  127. package/vendor/heyspex-mcp/procedure-tools.mjs +1181 -1159
  128. package/vendor/heyspex-mcp/promote-capture.mjs +2 -0
  129. package/vendor/heyspex-mcp/promote-research.mjs +6 -0
  130. package/vendor/heyspex-mcp/read-path.mjs +162 -6
  131. package/vendor/heyspex-mcp/request-context.mjs +25 -1
  132. package/vendor/heyspex-mcp/research-handlers.mjs +6 -4
  133. package/vendor/heyspex-mcp/roadmap-default.mjs +70 -0
  134. package/vendor/heyspex-mcp/session-owner.mjs +3 -2
  135. package/vendor/heyspex-mcp/session-tools.mjs +10 -0
  136. package/vendor/heyspex-mcp/spec-edit-tools.mjs +8 -1
  137. package/vendor/heyspex-mcp/split-research.mjs +3 -0
  138. package/vendor/heyspex-mcp/submit-for-review.mjs +18 -1
  139. package/vendor/heyspex-mcp/summary-write-tools.mjs +5 -1
  140. package/vendor/heyspex-mcp/test-helpers.mjs +18 -3
  141. package/vendor/heyspex-mcp/tool-catalog.mjs +7 -1
  142. package/vendor/heyspex-mcp/tool-dispatch.mjs +18 -0
  143. package/vendor/heyspex-mcp/vault-io.mjs +720 -724
  144. package/vendor/heyspex-mcp/vault-resolve.mjs +197 -9
  145. package/vendor/heyspex-mcp/whoami.mjs +2 -1
  146. package/vendor/heyspex-mcp/write-outbox.mjs +512 -0
@@ -1,325 +1,327 @@
1
- // heyspex_list_procedures, get_procedure, preflight handlers (disk and tier-aware
2
- // variants; the procedure store itself is procedure-tools.mjs). Extracted from cli.mjs under
3
- // [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
4
-
5
- import { executeGetProcedure, executeGetProcedureViaTier, executeListProcedures, executeListProceduresViaTier, executeRunPreflight, executeRunPreflightViaTier } from './procedure-tools.mjs';
6
- import { detectAgentIdentity, effectiveConsoleUrl } from './mcp-config.mjs';
7
- import { preflightMarkerNotice, writePreflightMarker } from './preflight-gate.mjs';
8
-
9
- /**
10
- * [[HS-K0ZP]]: reading research-capture-protocol directly is the same proof that
11
- * running heyspex_preflight({ intent: "research" }) gives, so a successful
12
- * heyspex_get_procedure on it writes the `research` marker too. Only a real body
13
- * counts -- a NOT_FOUND response taught the agent nothing. Never throws: the
14
- * marker write is fail-open by contract, and a read must never break on it.
15
- */
16
- export const RESEARCH_PROTOCOL_NAME = 'research-capture-protocol';
17
- export function recordResearchProtocolRead(vaultRoot, args, result) {
18
- try {
19
- if (String(args?.name ?? '').trim() !== RESEARCH_PROTOCOL_NAME) return false;
20
- if (!result || result.error || !result.content) return false;
21
- const identity = detectAgentIdentity(args.agent, args.session_id);
22
- return writePreflightMarker(vaultRoot, 'research', {
23
- sessionId: identity.sessionId,
24
- agent: identity.agent,
25
- procedures: [RESEARCH_PROTOCOL_NAME],
26
- });
27
- } catch {
28
- return false;
29
- }
30
- }
31
- import { matchingRecipes } from './recipe-tools.mjs';
32
- import { wrapDesignResult } from './tool-helpers.mjs';
33
- import { MEMORY_BASELINE_TAGS, MEMORY_DESCRIPTION_TRUNCATE, MEMORY_INJECTION_LIMIT, detectMemoryAmendment, scoreRelevantMemories } from './on-deck-brief.mjs';
34
- import { readViaSelector, resolveReadTier } from './read-path.mjs';
35
- import { consoleFetch } from './console-transport.mjs';
36
- import { extractSummary } from './memory-tools.mjs';
37
-
38
- export function handleListProcedures(vaultRoot, args) {
39
- return wrapDesignResult(executeListProcedures(vaultRoot, args));
40
- }
41
-
42
- export function handleGetProcedure(vaultRoot, args) {
43
- const result = executeGetProcedure(vaultRoot, args);
44
- recordResearchProtocolRead(vaultRoot, args, result);
45
- return wrapDesignResult(result);
46
- }
47
-
48
- export function handleRunPreflight(vaultRoot, args) {
49
- const result = executeRunPreflight(vaultRoot, args);
50
-
51
- // Proof-of-use ([[preflight-gate]]): record that the questions were actually
52
- // asked, so the tools those questions govern can refuse when they were not.
53
- // Only a real answer counts — an UNKNOWN_INTENT / CATALOG_NOT_FOUND response
54
- // taught the agent nothing and must not clear a gate.
55
- if (result && !result.error) {
56
- const identity = detectAgentIdentity(args.agent, args.session_id);
57
- const wrote = writePreflightMarker(vaultRoot, String(args.intent ?? '').trim(), {
58
- sessionId: identity.sessionId,
59
- agent: identity.agent,
60
- threadType: args.thread_type ?? null,
61
- procedures: Array.isArray(result.procedures)
62
- ? result.procedures.map((p) => p?.procedure).filter(Boolean)
63
- : [],
64
- });
65
- // A marker we could not write used to be swallowed silently, so preflight
66
- // reported success while the gated tools refused forever and told the caller
67
- // to "call preflight, then retry" — a loop no retry escaped
68
- // ([[HS-N71O-P3XQ-DIAG-six-new-qc-gates-fail-cold-verification]] item 3).
69
- // Preflight still succeeds (fail-open); it just stops claiming it recorded
70
- // something it did not.
71
- const notice = preflightMarkerNotice(wrote);
72
- if (notice) Object.assign(result, notice);
73
- }
74
- // Match workflow-recipes against the intent so recipes are discoverable the
75
- // same way procedures are ([[HS-RQ9K-TASK]] / [[HS-5G0P-EPIC]]). Never throws.
76
- let recipes = [];
77
- try {
78
- recipes = matchingRecipes(vaultRoot, args.intent);
79
- } catch {
80
- recipes = [];
81
- }
82
-
83
- // Recipe-only intent (e.g. "bug_fix"): a valid recipe signature that is NOT a
84
- // procedure intent returns UNKNOWN_INTENT from the procedure map. Rather than
85
- // hard-error, surface the matching recipes so the caller still gets a useful
86
- // answer — recipe intents live in a different namespace than procedure intents.
87
- if (result && result.error === 'UNKNOWN_INTENT' && recipes.length > 0) {
88
- return wrapDesignResult({
89
- intent: String(args.intent ?? '').trim(),
90
- procedures: [],
91
- recipes,
92
- note:
93
- 'No procedure checklist for this intent, but it matches one or more workflow-recipes. Start one with heyspex_start_recipe, or call heyspex_preflight with a procedure intent (e.g. file_task, execute_spec).',
94
- });
95
- }
96
-
97
- // Attach recalled_memories so high-stakes decision points carry the same
98
- // memory load /hs-next does. Skipped on error responses ([[HS-TASK-IMMF]]).
99
- if (result && !result.error) {
100
- try {
101
- const recalled = scoreRelevantMemories(vaultRoot, {
102
- // Use the intent name as a primary signal so an intent like
103
- // recommend_location surfaces template/saas/architecture memories
104
- // strongly. The baseline pool still applies on top.
105
- primaryTags: typeof args.intent === 'string' ? [args.intent] : [],
106
- });
107
- if (recalled.items.length > 0) {
108
- result.recalled_memories = recalled;
109
- }
110
- } catch {
111
- // Recall failure must never break preflight — surface the procedures
112
- // without memories rather than 500ing the response.
113
- }
114
- if (recipes.length > 0) {
115
- result.recipes = recipes;
116
- }
117
- }
118
- return wrapDesignResult(result);
119
- }
120
-
121
- /**
122
- * Cloud-reliant sibling of `handleListProcedures` ([[HS-DJWP-MCPC-TASK]]
123
- * "Procedures" family, backing `heyspex_list_procedures`). `handleListProcedures`
124
- * stays untouched (its `diskRead` branch).
125
- */
126
- export async function handleListProceduresViaTier(vaultRoot, args) {
127
- const tier = await resolveReadTier();
128
- const result = await executeListProceduresViaTier({
129
- vaultRoot,
130
- args,
131
- tier,
132
- consoleFetch,
133
- consoleUrl: effectiveConsoleUrl(),
134
- });
135
- return wrapDesignResult(result);
136
- }
137
-
138
- /**
139
- * Cloud-reliant sibling of `handleGetProcedure` ([[HS-DJWP-MCPC-TASK]]
140
- * "Procedures" family, backing `heyspex_get_procedure`). `handleGetProcedure`
141
- * stays untouched (its `diskRead` branch).
142
- */
143
- export async function handleGetProcedureViaTier(vaultRoot, args) {
144
- const tier = await resolveReadTier();
145
- const result = await executeGetProcedureViaTier({
146
- vaultRoot,
147
- args,
148
- tier,
149
- consoleFetch,
150
- consoleUrl: effectiveConsoleUrl(),
151
- });
152
- recordResearchProtocolRead(vaultRoot, args, result);
153
- return wrapDesignResult(result);
154
- }
155
-
156
- /**
157
- * Cloud-reliant sibling of `handleRunPreflight` ([[HS-DJWP-MCPC-TASK]]
158
- * "Procedures" + "Memories" families, backing `heyspex_preflight`).
159
- * `handleRunPreflight` is left completely untouched — it is imported and
160
- * called directly (synchronously) by `preflight-gate.test.mjs`, so it must
161
- * keep returning a plain object, never a Promise. This is a SEPARATE function,
162
- * used only by the live tool dispatch (`case 'heyspex_preflight'` below),
163
- * which duplicates the marker-write / recipe-match / memory-recall tail
164
- * (`writePreflightMarker`, `preflightMarkerNotice`, `matchingRecipes` are
165
- * reused unchanged — only the tier-aware catalog + memory fetch differ).
166
- */
167
- export async function handleRunPreflightViaTier(vaultRoot, args) {
168
- const tier = await resolveReadTier();
169
- const consoleUrl = effectiveConsoleUrl();
170
- const result = await executeRunPreflightViaTier({ vaultRoot, args, tier, consoleFetch, consoleUrl });
171
-
172
- if (result && !result.error) {
173
- const identity = detectAgentIdentity(args.agent, args.session_id);
174
- const wrote = writePreflightMarker(vaultRoot, String(args.intent ?? '').trim(), {
175
- sessionId: identity.sessionId,
176
- agent: identity.agent,
177
- threadType: args.thread_type ?? null,
178
- procedures: Array.isArray(result.procedures)
179
- ? result.procedures.map((p) => p?.procedure).filter(Boolean)
180
- : [],
181
- });
182
- const notice = preflightMarkerNotice(wrote);
183
- if (notice) Object.assign(result, notice);
184
- }
185
-
186
- let recipes = [];
187
- try {
188
- recipes = matchingRecipes(vaultRoot, args.intent);
189
- } catch {
190
- recipes = [];
191
- }
192
-
193
- if (result && result.error === 'UNKNOWN_INTENT' && recipes.length > 0) {
194
- return wrapDesignResult({
195
- intent: String(args.intent ?? '').trim(),
196
- procedures: [],
197
- recipes,
198
- note:
199
- 'No procedure checklist for this intent, but it matches one or more workflow-recipes. Start one with heyspex_start_recipe, or call heyspex_preflight with a procedure intent (e.g. file_task, execute_spec).',
200
- });
201
- }
202
-
203
- if (result && !result.error) {
204
- try {
205
- const recalled = await scoreRelevantMemoriesViaTier(
206
- vaultRoot,
207
- { primaryTags: typeof args.intent === 'string' ? [args.intent] : [] },
208
- { tier, consoleUrl },
209
- );
210
- if (recalled.items.length > 0) {
211
- result.recalled_memories = recalled;
212
- }
213
- } catch {
214
- // Recall failure must never break preflight.
215
- }
216
- if (recipes.length > 0) {
217
- result.recipes = recipes;
218
- }
219
- }
220
- return wrapDesignResult(result);
221
- }
222
-
223
- /**
224
- * Cloud-reliant sibling of `scoreRelevantMemories` ([[HS-DJWP-MCPC-TASK]]
225
- * "Memories" family, backing the preflight memory recall). `scoreRelevantMemories`
226
- * is left completely untouched (its callers — `handleRunPreflight`,
227
- * `enrichOnDeckWithRecalledMemories` — keep the exact disk-only behavior they
228
- * have today); this is a NEW, separate cloud path used only by
229
- * `handleRunPreflightViaTier` below. Scoring logic (tag-overlap, amendment
230
- * detection, truncation, sort) is duplicated rather than shared with
231
- * `scoreRelevantMemories` deliberately — the two operate on differently-shaped
232
- * inputs (disk: `{frontmatter, body}` per file; cloud: the console's flat
233
- * `MemoryDoc` rows) and duplicating ~25 lines is a smaller risk than
234
- * refactoring the already-tested disk function to share a helper.
235
- *
236
- * Unlike `handleRecallMemoriesViaTier`, no fidelity gap here — `MemoryDoc`
237
- * already carries everything this scorer reads (`tags`, `body`, `name`, `id`).
238
- *
239
- * Split into a tag-only ranking pass ({@link rankMemoryCandidatesByTags}) and a
240
- * per-item body-dependent builder ({@link buildScoredMemoryItem}) so the
241
- * caller ({@link scoreRelevantMemoriesViaTier}) can rank against a
242
- * summary-only listing (score is pure tag-overlap, never touches body) and
243
- * only fetch body text for the top `limit` survivors —
244
- * [[HS-LKV0-FUU4-DIAG-cloud-mode-recall-times-out-the]]: the console's
245
- * `GET /api/memories` used to always ship a body per doc, which is what made
246
- * a 896-doc vault blow the 30s fetch timeout on every one of these read paths.
247
- */
248
- export function rankMemoryCandidatesByTags(docs, options = {}) {
249
- const primaryTags = Array.isArray(options.primaryTags) ? options.primaryTags : [];
250
- const baselineTags = Array.isArray(options.baselineTags) ? options.baselineTags : MEMORY_BASELINE_TAGS;
251
- const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : MEMORY_INJECTION_LIMIT;
252
-
253
- const querySet = new Set([...primaryTags, ...baselineTags].map((t) => String(t).toLowerCase()));
254
- if (querySet.size === 0) return [];
255
-
256
- const scored = [];
257
- for (const doc of docs) {
258
- const tags = Array.isArray(doc.tags) ? doc.tags : [];
259
- if (tags.length === 0) continue;
260
- const tagSet = new Set(tags.map((t) => String(t).toLowerCase()));
261
- let score = 0;
262
- for (const q of querySet) if (tagSet.has(q)) score += 1;
263
- if (score === 0) continue;
264
- scored.push({ doc, score });
265
- }
266
-
267
- scored.sort((a, b) => {
268
- if (b.score !== a.score) return b.score - a.score;
269
- const aName = a.doc.name || a.doc.id || '';
270
- const bName = b.doc.name || b.doc.id || '';
271
- return String(aName).localeCompare(String(bName));
272
- });
273
-
274
- return scored.slice(0, limit);
275
- }
276
-
277
- /** Builds one scored-memory item from a ranked candidate + its (separately fetched) body. */
278
- export function buildScoredMemoryItem(doc, score, body) {
279
- const slug = doc.id;
280
- const name = doc.name || doc.id || slug;
281
- let description = extractSummary(body || '');
282
- if (description.length > MEMORY_DESCRIPTION_TRUNCATE) {
283
- description = description.slice(0, MEMORY_DESCRIPTION_TRUNCATE - 1) + '…';
284
- }
285
- const amended = detectMemoryAmendment(body || '', description);
286
- if (amended) {
287
- description = `${description} ⚠ ${amended} — this summary may not reflect the current ruling; READ THE FULL BODY before applying.`;
288
- }
289
- return { source: 'vault', name, link: `[[${slug}]]`, description, amended: amended || undefined, score };
290
- }
291
-
292
- export async function scoreRelevantMemoriesViaTier(vaultRoot, options, { tier, consoleUrl }) {
293
- return readViaSelector({
294
- tier,
295
- diskRead: () => scoreRelevantMemories(vaultRoot, options),
296
- neonRead: async () => {
297
- // Summary-only listing — scoring is pure tag-overlap and never needs body.
298
- const res = await consoleFetch('/api/memories');
299
- if (!res.ok) {
300
- throw new Error(`GET /api/memories returned ${res.status}: ${res.error || JSON.stringify(res.body ?? {})}`);
301
- }
302
- const docs = Array.isArray(res.body?.memories) ? res.body.memories : [];
303
- const ranked = rankMemoryCandidatesByTags(docs, options);
304
- if (ranked.length === 0) return { header: null, items: [] };
305
-
306
- // Fetch bodies ONLY for the survivors that ranking already narrowed to
307
- // (`limit`, default 5) — never one fetch per candidate across the whole set.
308
- const idsQs = new URLSearchParams({ ids: ranked.map(({ doc }) => doc.id).join(','), fields: 'body' });
309
- const bodyPath = `/api/memories?${idsQs}`;
310
- const bodyRes = await consoleFetch(bodyPath);
311
- if (!bodyRes.ok) {
312
- throw new Error(`GET ${bodyPath} returned ${bodyRes.status}: ${bodyRes.error || JSON.stringify(bodyRes.body ?? {})}`);
313
- }
314
- const bodyDocs = Array.isArray(bodyRes.body?.memories) ? bodyRes.body.memories : [];
315
- const bodyById = new Map(bodyDocs.map((d) => [d.id, typeof d.body === 'string' ? d.body : '']));
316
-
317
- return {
318
- header: 'Retrieved from prior sessions. Apply at decision points; do not echo verbatim.',
319
- items: ranked.map(({ doc, score }) => buildScoredMemoryItem(doc, score, bodyById.get(doc.id) || '')),
320
- };
321
- },
322
- routeLabel: 'GET /api/memories',
323
- consoleUrl,
324
- });
325
- }
1
+ // heyspex_list_procedures, get_procedure, preflight handlers (disk and tier-aware
2
+ // variants; the procedure store itself is procedure-tools.mjs). Extracted from cli.mjs under
3
+ // [[HS-BLNF-6W61-DIAG-cli-mjs-has-grown-into-a-13-665-line]] (no behavior change).
4
+
5
+ import { executeGetProcedure, executeGetProcedureViaTier, executeListProcedures, executeListProceduresViaTier, executeRunPreflight, executeRunPreflightViaTier } from './procedure-tools.mjs';
6
+ import { detectAgentIdentity, effectiveConsoleUrl } from './mcp-config.mjs';
7
+ import { preflightMarkerNotice, writePreflightMarker } from './preflight-gate.mjs';
8
+
9
+ /**
10
+ * [[HS-K0ZP]]: reading research-capture-protocol directly is the same proof that
11
+ * running heyspex_preflight({ intent: "research" }) gives, so a successful
12
+ * heyspex_get_procedure on it writes the `research` marker too. Only a real body
13
+ * counts -- a NOT_FOUND response taught the agent nothing. Never throws: the
14
+ * marker write is fail-open by contract, and a read must never break on it.
15
+ */
16
+ export const RESEARCH_PROTOCOL_NAME = 'research-capture-protocol';
17
+ export function recordResearchProtocolRead(vaultRoot, args, result) {
18
+ try {
19
+ if (String(args?.name ?? '').trim() !== RESEARCH_PROTOCOL_NAME) return false;
20
+ if (!result || result.error || !result.content) return false;
21
+ const identity = detectAgentIdentity(args.agent, args.session_id);
22
+ return writePreflightMarker(vaultRoot, 'research', {
23
+ sessionId: identity.sessionId,
24
+ agent: identity.agent,
25
+ procedures: [RESEARCH_PROTOCOL_NAME],
26
+ degradedSource: result.degraded ? 'disk' : undefined,
27
+ });
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+ import { matchingRecipes } from './recipe-tools.mjs';
33
+ import { wrapDesignResult } from './tool-helpers.mjs';
34
+ import { MEMORY_BASELINE_TAGS, MEMORY_DESCRIPTION_TRUNCATE, MEMORY_INJECTION_LIMIT, detectMemoryAmendment, scoreRelevantMemories } from './on-deck-brief.mjs';
35
+ import { consoleReadErrorFrom, readViaSelector, resolveReadTier } from './read-path.mjs';
36
+ import { consoleFetch } from './console-transport.mjs';
37
+ import { extractSummary } from './memory-tools.mjs';
38
+
39
+ export function handleListProcedures(vaultRoot, args) {
40
+ return wrapDesignResult(executeListProcedures(vaultRoot, args));
41
+ }
42
+
43
+ export function handleGetProcedure(vaultRoot, args) {
44
+ const result = executeGetProcedure(vaultRoot, args);
45
+ recordResearchProtocolRead(vaultRoot, args, result);
46
+ return wrapDesignResult(result);
47
+ }
48
+
49
+ export function handleRunPreflight(vaultRoot, args) {
50
+ const result = executeRunPreflight(vaultRoot, args);
51
+
52
+ // Proof-of-use ([[preflight-gate]]): record that the questions were actually
53
+ // asked, so the tools those questions govern can refuse when they were not.
54
+ // Only a real answer counts — an UNKNOWN_INTENT / CATALOG_NOT_FOUND response
55
+ // taught the agent nothing and must not clear a gate.
56
+ if (result && !result.error) {
57
+ const identity = detectAgentIdentity(args.agent, args.session_id);
58
+ const wrote = writePreflightMarker(vaultRoot, String(args.intent ?? '').trim(), {
59
+ sessionId: identity.sessionId,
60
+ agent: identity.agent,
61
+ threadType: args.thread_type ?? null,
62
+ procedures: Array.isArray(result.procedures)
63
+ ? result.procedures.map((p) => p?.procedure).filter(Boolean)
64
+ : [],
65
+ });
66
+ // A marker we could not write used to be swallowed silently, so preflight
67
+ // reported success while the gated tools refused forever and told the caller
68
+ // to "call preflight, then retry" — a loop no retry escaped
69
+ // ([[HS-N71O-P3XQ-DIAG-six-new-qc-gates-fail-cold-verification]] item 3).
70
+ // Preflight still succeeds (fail-open); it just stops claiming it recorded
71
+ // something it did not.
72
+ const notice = preflightMarkerNotice(wrote);
73
+ if (notice) Object.assign(result, notice);
74
+ }
75
+ // Match workflow-recipes against the intent so recipes are discoverable the
76
+ // same way procedures are ([[HS-RQ9K-TASK]] / [[HS-5G0P-EPIC]]). Never throws.
77
+ let recipes = [];
78
+ try {
79
+ recipes = matchingRecipes(vaultRoot, args.intent);
80
+ } catch {
81
+ recipes = [];
82
+ }
83
+
84
+ // Recipe-only intent (e.g. "bug_fix"): a valid recipe signature that is NOT a
85
+ // procedure intent returns UNKNOWN_INTENT from the procedure map. Rather than
86
+ // hard-error, surface the matching recipes so the caller still gets a useful
87
+ // answer — recipe intents live in a different namespace than procedure intents.
88
+ if (result && result.error === 'UNKNOWN_INTENT' && recipes.length > 0) {
89
+ return wrapDesignResult({
90
+ intent: String(args.intent ?? '').trim(),
91
+ procedures: [],
92
+ recipes,
93
+ note:
94
+ 'No procedure checklist for this intent, but it matches one or more workflow-recipes. Start one with heyspex_start_recipe, or call heyspex_preflight with a procedure intent (e.g. file_task, execute_spec).',
95
+ });
96
+ }
97
+
98
+ // Attach recalled_memories so high-stakes decision points carry the same
99
+ // memory load /hs-next does. Skipped on error responses ([[HS-TASK-IMMF]]).
100
+ if (result && !result.error) {
101
+ try {
102
+ const recalled = scoreRelevantMemories(vaultRoot, {
103
+ // Use the intent name as a primary signal so an intent like
104
+ // recommend_location surfaces template/saas/architecture memories
105
+ // strongly. The baseline pool still applies on top.
106
+ primaryTags: typeof args.intent === 'string' ? [args.intent] : [],
107
+ });
108
+ if (recalled.items.length > 0) {
109
+ result.recalled_memories = recalled;
110
+ }
111
+ } catch {
112
+ // Recall failure must never break preflight — surface the procedures
113
+ // without memories rather than 500ing the response.
114
+ }
115
+ if (recipes.length > 0) {
116
+ result.recipes = recipes;
117
+ }
118
+ }
119
+ return wrapDesignResult(result);
120
+ }
121
+
122
+ /**
123
+ * Cloud-reliant sibling of `handleListProcedures` ([[HS-DJWP-MCPC-TASK]]
124
+ * "Procedures" family, backing `heyspex_list_procedures`). `handleListProcedures`
125
+ * stays untouched (its `diskRead` branch).
126
+ */
127
+ export async function handleListProceduresViaTier(vaultRoot, args) {
128
+ const tier = await resolveReadTier();
129
+ const result = await executeListProceduresViaTier({
130
+ vaultRoot,
131
+ args,
132
+ tier,
133
+ consoleFetch,
134
+ consoleUrl: effectiveConsoleUrl(),
135
+ });
136
+ return wrapDesignResult(result);
137
+ }
138
+
139
+ /**
140
+ * Cloud-reliant sibling of `handleGetProcedure` ([[HS-DJWP-MCPC-TASK]]
141
+ * "Procedures" family, backing `heyspex_get_procedure`). `handleGetProcedure`
142
+ * stays untouched (its `diskRead` branch).
143
+ */
144
+ export async function handleGetProcedureViaTier(vaultRoot, args) {
145
+ const tier = await resolveReadTier();
146
+ const result = await executeGetProcedureViaTier({
147
+ vaultRoot,
148
+ args,
149
+ tier,
150
+ consoleFetch,
151
+ consoleUrl: effectiveConsoleUrl(),
152
+ });
153
+ recordResearchProtocolRead(vaultRoot, args, result);
154
+ return wrapDesignResult(result);
155
+ }
156
+
157
+ /**
158
+ * Cloud-reliant sibling of `handleRunPreflight` ([[HS-DJWP-MCPC-TASK]]
159
+ * "Procedures" + "Memories" families, backing `heyspex_preflight`).
160
+ * `handleRunPreflight` is left completely untouched — it is imported and
161
+ * called directly (synchronously) by `preflight-gate.test.mjs`, so it must
162
+ * keep returning a plain object, never a Promise. This is a SEPARATE function,
163
+ * used only by the live tool dispatch (`case 'heyspex_preflight'` below),
164
+ * which duplicates the marker-write / recipe-match / memory-recall tail
165
+ * (`writePreflightMarker`, `preflightMarkerNotice`, `matchingRecipes` are
166
+ * reused unchanged — only the tier-aware catalog + memory fetch differ).
167
+ */
168
+ export async function handleRunPreflightViaTier(vaultRoot, args) {
169
+ const tier = await resolveReadTier();
170
+ const consoleUrl = effectiveConsoleUrl();
171
+ const result = await executeRunPreflightViaTier({ vaultRoot, args, tier, consoleFetch, consoleUrl });
172
+
173
+ if (result && !result.error) {
174
+ const identity = detectAgentIdentity(args.agent, args.session_id);
175
+ const wrote = writePreflightMarker(vaultRoot, String(args.intent ?? '').trim(), {
176
+ sessionId: identity.sessionId,
177
+ agent: identity.agent,
178
+ threadType: args.thread_type ?? null,
179
+ procedures: Array.isArray(result.procedures)
180
+ ? result.procedures.map((p) => p?.procedure).filter(Boolean)
181
+ : [],
182
+ degradedSource: result.degraded ? 'disk' : undefined,
183
+ });
184
+ const notice = preflightMarkerNotice(wrote);
185
+ if (notice) Object.assign(result, notice);
186
+ }
187
+
188
+ let recipes = [];
189
+ try {
190
+ recipes = matchingRecipes(vaultRoot, args.intent);
191
+ } catch {
192
+ recipes = [];
193
+ }
194
+
195
+ if (result && result.error === 'UNKNOWN_INTENT' && recipes.length > 0) {
196
+ return wrapDesignResult({
197
+ intent: String(args.intent ?? '').trim(),
198
+ procedures: [],
199
+ recipes,
200
+ note:
201
+ 'No procedure checklist for this intent, but it matches one or more workflow-recipes. Start one with heyspex_start_recipe, or call heyspex_preflight with a procedure intent (e.g. file_task, execute_spec).',
202
+ });
203
+ }
204
+
205
+ if (result && !result.error) {
206
+ try {
207
+ const recalled = await scoreRelevantMemoriesViaTier(
208
+ vaultRoot,
209
+ { primaryTags: typeof args.intent === 'string' ? [args.intent] : [] },
210
+ { tier, consoleUrl },
211
+ );
212
+ if (recalled.items.length > 0) {
213
+ result.recalled_memories = recalled;
214
+ }
215
+ } catch {
216
+ // Recall failure must never break preflight.
217
+ }
218
+ if (recipes.length > 0) {
219
+ result.recipes = recipes;
220
+ }
221
+ }
222
+ return wrapDesignResult(result);
223
+ }
224
+
225
+ /**
226
+ * Cloud-reliant sibling of `scoreRelevantMemories` ([[HS-DJWP-MCPC-TASK]]
227
+ * "Memories" family, backing the preflight memory recall). `scoreRelevantMemories`
228
+ * is left completely untouched (its callers — `handleRunPreflight`,
229
+ * `enrichOnDeckWithRecalledMemories` — keep the exact disk-only behavior they
230
+ * have today); this is a NEW, separate cloud path used only by
231
+ * `handleRunPreflightViaTier` below. Scoring logic (tag-overlap, amendment
232
+ * detection, truncation, sort) is duplicated rather than shared with
233
+ * `scoreRelevantMemories` deliberately — the two operate on differently-shaped
234
+ * inputs (disk: `{frontmatter, body}` per file; cloud: the console's flat
235
+ * `MemoryDoc` rows) and duplicating ~25 lines is a smaller risk than
236
+ * refactoring the already-tested disk function to share a helper.
237
+ *
238
+ * Unlike `handleRecallMemoriesViaTier`, no fidelity gap here — `MemoryDoc`
239
+ * already carries everything this scorer reads (`tags`, `body`, `name`, `id`).
240
+ *
241
+ * Split into a tag-only ranking pass ({@link rankMemoryCandidatesByTags}) and a
242
+ * per-item body-dependent builder ({@link buildScoredMemoryItem}) so the
243
+ * caller ({@link scoreRelevantMemoriesViaTier}) can rank against a
244
+ * summary-only listing (score is pure tag-overlap, never touches body) and
245
+ * only fetch body text for the top `limit` survivors —
246
+ * [[HS-LKV0-FUU4-DIAG-cloud-mode-recall-times-out-the]]: the console's
247
+ * `GET /api/memories` used to always ship a body per doc, which is what made
248
+ * a 896-doc vault blow the 30s fetch timeout on every one of these read paths.
249
+ */
250
+ export function rankMemoryCandidatesByTags(docs, options = {}) {
251
+ const primaryTags = Array.isArray(options.primaryTags) ? options.primaryTags : [];
252
+ const baselineTags = Array.isArray(options.baselineTags) ? options.baselineTags : MEMORY_BASELINE_TAGS;
253
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : MEMORY_INJECTION_LIMIT;
254
+
255
+ const querySet = new Set([...primaryTags, ...baselineTags].map((t) => String(t).toLowerCase()));
256
+ if (querySet.size === 0) return [];
257
+
258
+ const scored = [];
259
+ for (const doc of docs) {
260
+ const tags = Array.isArray(doc.tags) ? doc.tags : [];
261
+ if (tags.length === 0) continue;
262
+ const tagSet = new Set(tags.map((t) => String(t).toLowerCase()));
263
+ let score = 0;
264
+ for (const q of querySet) if (tagSet.has(q)) score += 1;
265
+ if (score === 0) continue;
266
+ scored.push({ doc, score });
267
+ }
268
+
269
+ scored.sort((a, b) => {
270
+ if (b.score !== a.score) return b.score - a.score;
271
+ const aName = a.doc.name || a.doc.id || '';
272
+ const bName = b.doc.name || b.doc.id || '';
273
+ return String(aName).localeCompare(String(bName));
274
+ });
275
+
276
+ return scored.slice(0, limit);
277
+ }
278
+
279
+ /** Builds one scored-memory item from a ranked candidate + its (separately fetched) body. */
280
+ export function buildScoredMemoryItem(doc, score, body) {
281
+ const slug = doc.id;
282
+ const name = doc.name || doc.id || slug;
283
+ let description = extractSummary(body || '');
284
+ if (description.length > MEMORY_DESCRIPTION_TRUNCATE) {
285
+ description = description.slice(0, MEMORY_DESCRIPTION_TRUNCATE - 1) + '…';
286
+ }
287
+ const amended = detectMemoryAmendment(body || '', description);
288
+ if (amended) {
289
+ description = `${description} ⚠ ${amended} — this summary may not reflect the current ruling; READ THE FULL BODY before applying.`;
290
+ }
291
+ return { source: 'vault', name, link: `[[${slug}]]`, description, amended: amended || undefined, score };
292
+ }
293
+
294
+ export async function scoreRelevantMemoriesViaTier(vaultRoot, options, { tier, consoleUrl }) {
295
+ return readViaSelector({
296
+ tier,
297
+ diskRead: () => scoreRelevantMemories(vaultRoot, options),
298
+ neonRead: async () => {
299
+ // Summary-only listing — scoring is pure tag-overlap and never needs body.
300
+ const res = await consoleFetch('/api/memories');
301
+ if (!res.ok) {
302
+ throw consoleReadErrorFrom(res, `GET /api/memories returned ${res.status}: ${res.error || JSON.stringify(res.body ?? {})}`, null);
303
+ }
304
+ const docs = Array.isArray(res.body?.memories) ? res.body.memories : [];
305
+ const ranked = rankMemoryCandidatesByTags(docs, options);
306
+ if (ranked.length === 0) return { header: null, items: [] };
307
+
308
+ // Fetch bodies ONLY for the survivors that ranking already narrowed to
309
+ // (`limit`, default 5) — never one fetch per candidate across the whole set.
310
+ const idsQs = new URLSearchParams({ ids: ranked.map(({ doc }) => doc.id).join(','), fields: 'body' });
311
+ const bodyPath = `/api/memories?${idsQs}`;
312
+ const bodyRes = await consoleFetch(bodyPath);
313
+ if (!bodyRes.ok) {
314
+ throw consoleReadErrorFrom(bodyRes, `GET ${bodyPath} returned ${bodyRes.status}: ${bodyRes.error || JSON.stringify(bodyRes.body ?? {})}`, null);
315
+ }
316
+ const bodyDocs = Array.isArray(bodyRes.body?.memories) ? bodyRes.body.memories : [];
317
+ const bodyById = new Map(bodyDocs.map((d) => [d.id, typeof d.body === 'string' ? d.body : '']));
318
+
319
+ return {
320
+ header: 'Retrieved from prior sessions. Apply at decision points; do not echo verbatim.',
321
+ items: ranked.map(({ doc, score }) => buildScoredMemoryItem(doc, score, bodyById.get(doc.id) || '')),
322
+ };
323
+ },
324
+ routeLabel: 'GET /api/memories',
325
+ consoleUrl,
326
+ });
327
+ }