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,970 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HeySpex procedure-discovery MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Pure-logic module backing the `heyspex_list_procedures`,
|
|
5
|
+
* `heyspex_get_procedure`, and `heyspex_preflight` MCP tools mounted in
|
|
6
|
+
* cli.mjs. Reads the committed catalog at specs/protocols/.catalog.json
|
|
7
|
+
* (generated by scripts/build-procedure-catalog.mjs) and the underlying
|
|
8
|
+
* markdown bodies under specs/protocols/.
|
|
9
|
+
*
|
|
10
|
+
* Spec: [[HS-EPIC-3QS8-agent-procedure-discovery-enforcement-la]]
|
|
11
|
+
* Pattern source: vendor/heyspex-mcp/design-tools.mjs (the
|
|
12
|
+
* heyspex_design_* family is the canonical template this module mirrors).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { readViaSelector } from './read-path.mjs';
|
|
19
|
+
|
|
20
|
+
const CATALOG_FILE = ['specs', 'protocols', '.catalog.json'];
|
|
21
|
+
const PROTOCOLS_DIR = ['specs', 'protocols'];
|
|
22
|
+
|
|
23
|
+
function normalize(value) {
|
|
24
|
+
return String(value ?? '').toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loadCatalog(vaultRoot) {
|
|
28
|
+
const path = join(vaultRoot, ...CATALOG_FILE);
|
|
29
|
+
if (!existsSync(path)) return null;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function summarize(entry) {
|
|
38
|
+
return {
|
|
39
|
+
name: entry.name,
|
|
40
|
+
id: entry.id,
|
|
41
|
+
title: entry.title,
|
|
42
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
43
|
+
triggers: Array.isArray(entry.triggers) ? entry.triggers : [],
|
|
44
|
+
summary: entry.summary ?? '',
|
|
45
|
+
path: entry.path,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `injectedCatalog` (HS-DJWP-MCPC-TASK read-path wiring): when provided, used
|
|
51
|
+
* verbatim INSTEAD of `loadCatalog(vaultRoot)` — the cloud-reliant selector
|
|
52
|
+
* wrapper below fetches the catalog once over HTTP and injects it here so
|
|
53
|
+
* this function never touches disk in that tier. `undefined` (the default)
|
|
54
|
+
* preserves today's disk-only behavior byte-for-byte.
|
|
55
|
+
*/
|
|
56
|
+
export function executeListProcedures(vaultRoot, args = {}, injectedCatalog) {
|
|
57
|
+
const catalog = injectedCatalog !== undefined ? injectedCatalog : loadCatalog(vaultRoot);
|
|
58
|
+
if (!catalog) {
|
|
59
|
+
return {
|
|
60
|
+
error: 'CATALOG_NOT_FOUND',
|
|
61
|
+
message:
|
|
62
|
+
'specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const procedures = Array.isArray(catalog.procedures) ? catalog.procedures : [];
|
|
66
|
+
const tagFilter = args.tag ? normalize(args.tag) : null;
|
|
67
|
+
const triggerFilter = args.trigger ? normalize(args.trigger) : null;
|
|
68
|
+
const queryFilter = args.query ? normalize(args.query) : null;
|
|
69
|
+
|
|
70
|
+
const matches = [];
|
|
71
|
+
for (const entry of procedures) {
|
|
72
|
+
if (tagFilter) {
|
|
73
|
+
const tags = (entry.tags ?? []).map(normalize);
|
|
74
|
+
if (!tags.includes(tagFilter)) continue;
|
|
75
|
+
}
|
|
76
|
+
if (triggerFilter) {
|
|
77
|
+
const triggers = (entry.triggers ?? []).map(normalize);
|
|
78
|
+
if (!triggers.includes(triggerFilter)) continue;
|
|
79
|
+
}
|
|
80
|
+
if (queryFilter) {
|
|
81
|
+
const haystack = [entry.name, entry.title, entry.summary]
|
|
82
|
+
.map(normalize)
|
|
83
|
+
.join(' ');
|
|
84
|
+
if (!haystack.includes(queryFilter)) continue;
|
|
85
|
+
}
|
|
86
|
+
matches.push(summarize(entry));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
matches.sort((a, b) => a.name.localeCompare(b.name));
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
procedures: matches,
|
|
93
|
+
count: matches.length,
|
|
94
|
+
total: procedures.length,
|
|
95
|
+
filters_applied: {
|
|
96
|
+
tag: args.tag ?? null,
|
|
97
|
+
trigger: args.trigger ?? null,
|
|
98
|
+
query: args.query ?? null,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function findEntry(catalog, name) {
|
|
104
|
+
const procedures = Array.isArray(catalog.procedures) ? catalog.procedures : [];
|
|
105
|
+
return procedures.find((p) => p.name === name) || null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function executeGetProcedure(vaultRoot, args = {}) {
|
|
109
|
+
const name = String(args.name ?? '').trim();
|
|
110
|
+
if (!name) {
|
|
111
|
+
return { error: 'MISSING_NAME', message: 'name is required (e.g. "spec-hierarchy")' };
|
|
112
|
+
}
|
|
113
|
+
const catalog = loadCatalog(vaultRoot);
|
|
114
|
+
if (!catalog) {
|
|
115
|
+
return {
|
|
116
|
+
error: 'CATALOG_NOT_FOUND',
|
|
117
|
+
message:
|
|
118
|
+
'specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const entry = findEntry(catalog, name);
|
|
122
|
+
if (!entry) {
|
|
123
|
+
return {
|
|
124
|
+
error: 'PROCEDURE_NOT_FOUND',
|
|
125
|
+
message: `No procedure named "${name}" in catalog. Call heyspex_list_procedures to browse.`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const bodyPath = join(vaultRoot, ...PROTOCOLS_DIR, `${name}.md`);
|
|
129
|
+
if (!existsSync(bodyPath)) {
|
|
130
|
+
return {
|
|
131
|
+
error: 'BODY_FILE_MISSING',
|
|
132
|
+
message: `Catalog entry exists but ${entry.path} not found on disk. Regenerate the catalog.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const content = readFileSync(bodyPath, 'utf8');
|
|
136
|
+
return {
|
|
137
|
+
procedure: {
|
|
138
|
+
name: entry.name,
|
|
139
|
+
id: entry.id,
|
|
140
|
+
title: entry.title,
|
|
141
|
+
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
|
142
|
+
triggers: Array.isArray(entry.triggers) ? entry.triggers : [],
|
|
143
|
+
summary: entry.summary ?? '',
|
|
144
|
+
path: entry.path,
|
|
145
|
+
status: entry.status ?? null,
|
|
146
|
+
created: entry.created ?? null,
|
|
147
|
+
lastModified: entry.lastModified ?? null,
|
|
148
|
+
byteSize: entry.byteSize ?? null,
|
|
149
|
+
},
|
|
150
|
+
content,
|
|
151
|
+
source: entry.path,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Bootstrap intent → procedure map (eight intents per HS-EPIC-3QS8 §Open
|
|
156
|
+
// questions "Trigger taxonomy"). Each row names the procedure slug (matches
|
|
157
|
+
// specs/protocols/<slug>.md), a `why` blurb the agent can quote, and a
|
|
158
|
+
// severity. Extension rule: add a new intent when a third unique use case
|
|
159
|
+
// appears — new intents land in their own tasks, not edits to this map.
|
|
160
|
+
const PREFLIGHT_INTENT_MAP = {
|
|
161
|
+
promote_research: [
|
|
162
|
+
{
|
|
163
|
+
procedure: 'research-capture-protocol',
|
|
164
|
+
why: 'Confirm the research doc is complete (prerequisites, conclusion, sources) before promotion.',
|
|
165
|
+
severity: 'required',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
procedure: 'spec-hierarchy',
|
|
169
|
+
why: 'Mandatory default: target_milestone NEW. Existing-milestone attach only when status: open and thesis clearly extends; in_progress requires explicit owner override.',
|
|
170
|
+
severity: 'required',
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
procedure: 'pillar-assignment-protocol',
|
|
174
|
+
why: 'Every spawned epic must name a pillar before execution breakdown.',
|
|
175
|
+
severity: 'recommended',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
file_iteration: [
|
|
179
|
+
{
|
|
180
|
+
procedure: 'supersession-protocol',
|
|
181
|
+
why: '§Iterates-on: choose the right tier; a re-attempt is neither supersession nor amends.',
|
|
182
|
+
severity: 'required',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
procedure: 'research-capture-protocol',
|
|
186
|
+
why: 'Promote flow asks "is this a re-attempt?" and populates iterates_on: — confirm iteration lineage is recorded.',
|
|
187
|
+
severity: 'required',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
procedure: 'internal-linking-protocol',
|
|
191
|
+
why: 'Iteration structure and tier-based lineage tracking; see entity-model §Iteration lineage for tier semantics.',
|
|
192
|
+
severity: 'recommended',
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
file_milestone: [
|
|
196
|
+
{
|
|
197
|
+
procedure: 'spec-hierarchy',
|
|
198
|
+
why: 'Defines which level a new spec belongs at and the milestone frontmatter shape.',
|
|
199
|
+
severity: 'required',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
procedure: 'spec-summary-authoring',
|
|
203
|
+
why: 'Every milestone needs a plain-language summary: at filing time — pass summary to heyspex_file_spec / POST /api/specs.',
|
|
204
|
+
severity: 'required',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
procedure: 'vault-id-assignment-protocol',
|
|
208
|
+
why: 'Allocates a unique HS-MIL-<suffix> id and prevents duplicates.',
|
|
209
|
+
severity: 'required',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
file_epic: [
|
|
213
|
+
{
|
|
214
|
+
procedure: 'spec-hierarchy',
|
|
215
|
+
why: 'Defines epic frontmatter, required parent-milestone link, and child layout.',
|
|
216
|
+
severity: 'required',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
procedure: 'spec-summary-authoring',
|
|
220
|
+
why: 'Every epic needs a plain-language summary at plan-ingest filing time — pass summary to heyspex_push_plan / heyspex_file_spec / POST /api/specs. Do not file tasks in the same pass.',
|
|
221
|
+
severity: 'required',
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
procedure: 'pillar-assignment-protocol',
|
|
225
|
+
why: 'Every epic must name a pillar (hard-blocked at filing). Covers picking an existing HS-PILR theme or authoring a net-new one.',
|
|
226
|
+
severity: 'required',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
procedure: 'vault-id-assignment-protocol',
|
|
230
|
+
why: 'Allocates a unique HS-EPIC-<suffix> id and prevents duplicates.',
|
|
231
|
+
severity: 'required',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
procedure: 'plan-ingest-agent-workflow',
|
|
235
|
+
why: 'Linkage + manifest registration steps when ingesting an external plan into the epic.',
|
|
236
|
+
severity: 'recommended',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
procedure: 'builder-fleet-capability-reference',
|
|
240
|
+
why: 'Optional builder_profile + ## Builder fleet notes at epic filing — anticipate local builder model class and context tier.',
|
|
241
|
+
severity: 'recommended',
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
file_task: [
|
|
245
|
+
{
|
|
246
|
+
procedure: 'spec-hierarchy',
|
|
247
|
+
why: 'Defines task frontmatter, required parent-epic link, and child-task fan-out rules.',
|
|
248
|
+
severity: 'required',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
procedure: 'builder-fleet-capability-reference',
|
|
252
|
+
why: 'Required ## Builder brief + dispatch frontmatter (suggested_model, recommended_num_ctx, context_files) for fleet-calibrated tasks.',
|
|
253
|
+
severity: 'required',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
procedure: 'spec-summary-authoring',
|
|
257
|
+
why: 'Every task needs a plain-language summary at the /hs-next breakdown moment — pass summary to heyspex_file_spec / POST /api/specs. Tasks are never filed during plan ingest.',
|
|
258
|
+
severity: 'required',
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
procedure: 'vault-id-assignment-protocol',
|
|
262
|
+
why: 'Allocates a unique HS-TASK-<suffix> id and prevents duplicates.',
|
|
263
|
+
severity: 'required',
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
procedure: 'checks-manifest-authoring',
|
|
267
|
+
why: 'Every task ships a FROZEN specs/checks/<task-id>.checks.json — each AC classified machine|property|human with named runnable checks, authored at breakdown before any builder sees the brief. A task without one is not ready to dispatch.',
|
|
268
|
+
severity: 'required',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
procedure: 'unit-test-authoring-protocol',
|
|
272
|
+
why: 'How the tests behind the manifest checks are written: pure-core/thin-shell, colocation, the five case families, and the mutation gate as the quality oracle.',
|
|
273
|
+
severity: 'recommended',
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
file_diagnose: [
|
|
277
|
+
{
|
|
278
|
+
procedure: 'spec-hierarchy',
|
|
279
|
+
why: 'Defines diagnose frontmatter, the standalone-default / optional epic-or-milestone parent rule, and severity.',
|
|
280
|
+
severity: 'required',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
procedure: 'vault-id-assignment-protocol',
|
|
284
|
+
why: 'Allocates a unique HS-DIAG-<suffix> id and prevents duplicates.',
|
|
285
|
+
severity: 'required',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
procedure: 'resolution-diagnose-workflow',
|
|
289
|
+
why: 'The end-to-end bug→diagnose procedure: intake, §Step 2 symptom-vs-core classification (if fixing exactly this leaves the same class of failure possible, you are still on the symptom), severity ranking, §Step 6 sibling sweep — which now owes a receipt in the body\'s ## Sweep section — and close-out.',
|
|
290
|
+
severity: 'required',
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
procedure: 'checks-manifest-authoring',
|
|
294
|
+
why: 'A bug is the case where a regression check is worth the most and costs the least — the reproduction is already written. Freeze one runnable check per acceptance criterion via `checks:` on heyspex_file_spec, and make at least one of them FAIL when the fix is reverted; "the test passes now" is also what a symptom-level fix produces. REQUIRED WITH AN OUT: a trivial bug may file with no manifest — filing still succeeds and returns a `no_checks_manifest` warning — but skipping is a decision to state, not a step to forget.',
|
|
295
|
+
severity: 'required',
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
file_research: [
|
|
299
|
+
{
|
|
300
|
+
procedure: 'spec-hierarchy',
|
|
301
|
+
why: 'Confirms research is a standalone entity (HS-RES-<id>), how it links back to consumers, and that promotion defaults to a NEW milestone.',
|
|
302
|
+
severity: 'required',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
procedure: 'research-capture-protocol',
|
|
306
|
+
why: 'Required template (Executive Summary + Sources) and the back-wikilink + maintenance rules for a research doc.',
|
|
307
|
+
severity: 'required',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
procedure: 'vault-id-assignment-protocol',
|
|
311
|
+
why: 'Allocates a unique HS-RES-<suffix> id and prevents duplicates.',
|
|
312
|
+
severity: 'required',
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
file_memory_lesson: [
|
|
316
|
+
{
|
|
317
|
+
procedure: 'memory-protocol',
|
|
318
|
+
why: 'How to file a memory-lesson and link it back to the originating spec; the four-axis capture rubric.',
|
|
319
|
+
severity: 'required',
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
procedure: 'vault-id-assignment-protocol',
|
|
323
|
+
why: 'Allocates a unique memory slug/id and prevents duplicates.',
|
|
324
|
+
severity: 'required',
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
file_memory_decision: [
|
|
328
|
+
{
|
|
329
|
+
procedure: 'memory-protocol',
|
|
330
|
+
why: 'How to file a memory-decision and link it back to the originating spec; the four-axis capture rubric.',
|
|
331
|
+
severity: 'required',
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
procedure: 'vault-id-assignment-protocol',
|
|
335
|
+
why: 'Allocates a unique memory slug/id and prevents duplicates.',
|
|
336
|
+
severity: 'required',
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
file_memory_reference: [
|
|
340
|
+
{
|
|
341
|
+
procedure: 'memory-protocol',
|
|
342
|
+
why: 'How to file a memory-reference and link it back to the originating spec; the four-axis capture rubric.',
|
|
343
|
+
severity: 'required',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
procedure: 'vault-id-assignment-protocol',
|
|
347
|
+
why: 'Allocates a unique memory slug/id and prevents duplicates.',
|
|
348
|
+
severity: 'required',
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
file_memory_ui_pattern: [
|
|
352
|
+
{
|
|
353
|
+
procedure: 'memory-protocol',
|
|
354
|
+
why: 'How to file a memory-ui-pattern and link it back to the originating spec; the four-axis capture rubric.',
|
|
355
|
+
severity: 'required',
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
procedure: 'vault-id-assignment-protocol',
|
|
359
|
+
why: 'Allocates a unique memory slug/id and prevents duplicates.',
|
|
360
|
+
severity: 'required',
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
file_review: [
|
|
364
|
+
{
|
|
365
|
+
procedure: 'session-close-protocol',
|
|
366
|
+
why: 'The HS-REV companion doc shape (four required sections) and the close-out flow it belongs to.',
|
|
367
|
+
severity: 'required',
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
procedure: 'spec-hierarchy',
|
|
371
|
+
why: 'Confirms the review parents the spec under review (parent: "[[<spec>]]" is required).',
|
|
372
|
+
severity: 'recommended',
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
file_capture: [
|
|
376
|
+
{
|
|
377
|
+
procedure: 'capture-guardrails',
|
|
378
|
+
why: 'Captures are session-based and NOT research — human words stay verbatim in `## Notes`; AI authors separate layers on an explicit pass. Drive them via heyspex_active_capture, not heyspex_file_spec.',
|
|
379
|
+
severity: 'required',
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
destructive_action: [
|
|
383
|
+
{
|
|
384
|
+
procedure: 'destructive-action-checklist',
|
|
385
|
+
why: 'Run before any PR that deletes, archives, or routes data-destroying actions.',
|
|
386
|
+
severity: 'required',
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
procedure: 'dev-server-lifecycle-and-db-safety',
|
|
390
|
+
why: 'If the action is a DB migration / Neon branch reset / .next delete, stop the running dev server first — check ground-truth status via heyspex_dev_server_status.',
|
|
391
|
+
severity: 'recommended',
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
protocol_edit: [
|
|
395
|
+
{
|
|
396
|
+
procedure: 'spec-hierarchy',
|
|
397
|
+
why: '§Agent autonomy row "Modify a protocol doc" — protocol edits require explicit user approval.',
|
|
398
|
+
severity: 'required',
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
memory_creation: [
|
|
402
|
+
{
|
|
403
|
+
procedure: 'memory-protocol',
|
|
404
|
+
why: 'How to file a lesson/decision/reference/ui-pattern memory and link it back to the originating spec.',
|
|
405
|
+
severity: 'required',
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
close_session: [
|
|
409
|
+
{
|
|
410
|
+
procedure: 'session-close-protocol',
|
|
411
|
+
why: 'Required agent close-out checklist (status flips, review-doc creation, hand-off).',
|
|
412
|
+
severity: 'required',
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
procedure: 'memory-protocol',
|
|
416
|
+
why: 'Self-check for learnings is step 0 of close-out — capture any non-obvious lesson/decision as a memory spec.',
|
|
417
|
+
severity: 'recommended',
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
context_rollover: [
|
|
421
|
+
{
|
|
422
|
+
procedure: 'context-rollover-checkpoint',
|
|
423
|
+
why: 'Lightweight mid-flight checkpoint when compaction interrupts implementation — record WIP + handoff, then resume. Do NOT flip status / write HS-REV (that is session close, not rollover).',
|
|
424
|
+
severity: 'required',
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
claim_spec: [
|
|
428
|
+
{
|
|
429
|
+
procedure: 'spec-hierarchy',
|
|
430
|
+
why: 'A spec is claimable iff it is its own current ClaimTarget — read §Claim eligibility before claiming an Epic/Milestone with open descendants; the console endpoint returns 409 claim_blocked_by_descendants otherwise ([[HS-TASK-0RVD]]).',
|
|
431
|
+
severity: 'required',
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
procedure: 'pre-execution-alignment-gate',
|
|
435
|
+
why: 'The /hs-next surface routes the claim through this gate; the brief sections (world_state_new / world_state_modified / graph_delta) and the 4-option menu are documented here.',
|
|
436
|
+
severity: 'recommended',
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
execute_spec: [
|
|
440
|
+
{
|
|
441
|
+
procedure: 'dev-factory-delegation-routing',
|
|
442
|
+
why: 'The COO operating contract: delegation is table-driven. Before delegating build/breakdown/critic work, consult dev-factory/routing-table.json — build → free local fleet, breakdown/critic → DeepSeek V4, cloud/Opus escalation-only. NEVER spawn a Claude subagent on your own (expensive) tier for mechanical, gate-verified build.',
|
|
443
|
+
severity: 'required',
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
procedure: 'pre-execution-alignment-gate',
|
|
447
|
+
why: 'The first pre-flight before executing an epic/milestone — break down a childless node or revisit a drifted breakdown; hard gate with override-with-reason.',
|
|
448
|
+
severity: 'required',
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
procedure: 'spec-hierarchy',
|
|
452
|
+
why: 'Confirms the level you are executing and the parent/child linkage the breakdown must respect.',
|
|
453
|
+
severity: 'recommended',
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
procedure: 'builder-fleet-capability-reference',
|
|
457
|
+
why: 'Context tier ladder + task Builder brief contract when breaking down or dispatching to the local fleet.',
|
|
458
|
+
severity: 'recommended',
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
procedure: 'unit-test-authoring-protocol',
|
|
462
|
+
why: 'Before writing any test: pure-core/thin-shell structure, colocated <module>.test.ts, the five case families every pure module covers, and the mutation gate as the test-quality oracle.',
|
|
463
|
+
severity: 'recommended',
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
procedure: 'checks-manifest-authoring',
|
|
467
|
+
why: 'The frozen specs/checks/<task-id>.checks.json is the external completion gate — builders satisfy its named checks and never edit it; hs check run <task-id> is the per-criterion verdict.',
|
|
468
|
+
severity: 'recommended',
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
procedure: 'dev-server-lifecycle-and-db-safety',
|
|
472
|
+
why: 'Execution often involves DB migrations / dependency installs / .next deletes that break a running dev server — check ground-truth status (heyspex_dev_server_status) and stop the server first.',
|
|
473
|
+
severity: 'recommended',
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
exit_plan_mode: [
|
|
477
|
+
{
|
|
478
|
+
procedure: 'plan-quality-canonical-check',
|
|
479
|
+
why: 'Run dual overlap+code audit and canonical/supersession check before exit; revise plan to V2.',
|
|
480
|
+
severity: 'required',
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
procedure: 'plan-ingest-agent-workflow',
|
|
484
|
+
why: 'After the plan is approved, do NOT start coding. First ingest the approved plan into the vault as an epic (file it with children:[]; tasks fan out later via /hs-next) AND claim it via heyspex_claim_spec before executing. ExitPlanMode is the plan→execute seam — without this step agents skip filing+claiming and go straight to code.',
|
|
485
|
+
severity: 'required',
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
procedure: 'supersession-protocol',
|
|
489
|
+
why: 'If the plan deprecates an existing spec, file the supersession edge per protocol.',
|
|
490
|
+
severity: 'recommended',
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
recommend_location: [
|
|
494
|
+
{
|
|
495
|
+
procedure: 'rule-placement-protocol',
|
|
496
|
+
why: 'Cost ranking for where a new rule belongs: procedure ≥ memory > spec > CLAUDE.md. CLAUDE.md is the most expensive surface (always-loaded, customer-inherited) — never the default.',
|
|
497
|
+
severity: 'required',
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
procedure: 'memory-protocol',
|
|
501
|
+
why: 'When the right home is a memory (rank 2 in rule-placement-protocol), this defines the lesson/decision/reference/ui-pattern shape and tagging conventions.',
|
|
502
|
+
severity: 'recommended',
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
procedure: 'foundations-placement-protocol',
|
|
506
|
+
why: 'When the thing to place is a high-level vision/strategy concept (a principle, a north-star idea, a business claim) rather than an agent-behavior rule: ranks the foundations homes (North Star > Operating Model > Principles > Architecture > Positioning > Research) and gates a foundational edit behind user sign-off.',
|
|
507
|
+
severity: 'recommended',
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
focus_pivot: [
|
|
511
|
+
{
|
|
512
|
+
procedure: 'focus-intent-routing',
|
|
513
|
+
why: 'How to detect a roadmap focus-pivot utterance and route it through heyspex_set_focus_areas — covers negation handling ("stop working on X" must NOT add X), the closed-tag-vocabulary constraint (heyspex_list_known_tags first), and the confirmation beat before applying.',
|
|
514
|
+
severity: 'required',
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
server_impacting_action: [
|
|
518
|
+
{
|
|
519
|
+
procedure: 'dev-server-lifecycle-and-db-safety',
|
|
520
|
+
why: 'Before a DB migration, Neon branch reset, dependency install, .next delete, or starting a second dev server: check ground-truth status (heyspex_dev_server_status / `hs server status`) and stop a live server first — running a migration under a live server strands its in-memory schema and forces a restart.',
|
|
521
|
+
severity: 'required',
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
restructure_content: [
|
|
525
|
+
{
|
|
526
|
+
procedure: 'internal-linking-protocol',
|
|
527
|
+
why: 'Before re-parenting an epic, editing a milestone epics[] roster, or moving trackable work to a new canonical home — leave a summary + wikilink stub at the old home so wikilinks and lexical retrieval signal are preserved.',
|
|
528
|
+
severity: 'required',
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
procedure: 'spec-hierarchy',
|
|
532
|
+
why: 'Confirms bidirectional linkage shape (epics: ⇄ milestone:, children: ⇄ parent:) that re-parent edits must maintain.',
|
|
533
|
+
severity: 'recommended',
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
procedure: 'supersession-protocol',
|
|
537
|
+
why: 'If the move replaces dead guidance (not merely relocates live work), use supersession instead of a cross-reference stub.',
|
|
538
|
+
severity: 'recommended',
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
procedure: 'doc-canonicalization-protocol',
|
|
542
|
+
why: 'If the restructure is an evergreen-doc split/merge rather than plan-side re-parent, use doc-canonicalization instead.',
|
|
543
|
+
severity: 'recommended',
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const VALID_INTENTS = Object.keys(PREFLIGHT_INTENT_MAP);
|
|
549
|
+
|
|
550
|
+
// Maps a file_* intent to the canonical filing tool the agent should call.
|
|
551
|
+
// This is the core of [[HS-EPIC-WDP3]]: surface the deterministic tool inline
|
|
552
|
+
// in the preflight response so an agent that reaches the procedure layer is
|
|
553
|
+
// routed at the right answer — instead of pattern-matching a sibling spec's
|
|
554
|
+
// frontmatter. Every non-capture kind files via heyspex_file_spec (the server
|
|
555
|
+
// generates frontmatter from the canonical template); captures are
|
|
556
|
+
// session-based and route at heyspex_active_capture. Intents not listed here
|
|
557
|
+
// (destructive_action, protocol_edit, close_session, claim_spec, execute_spec,
|
|
558
|
+
// recommend_location) govern actions, not spec creation — close_session carries
|
|
559
|
+
// close_tool; others carry no filing_tool block.
|
|
560
|
+
const CLOSE_TOOL_MAP = {
|
|
561
|
+
close_session: {
|
|
562
|
+
tool: 'heyspex_submit_for_review',
|
|
563
|
+
minimal_call: { spec_id: '<completed-review-unit-or-task-id>' },
|
|
564
|
+
note:
|
|
565
|
+
'Status SSOT is PATCH /api/specs/:id/status — not frontmatter edits. This tool applies review-grain coercion, parent rollup, and releases the claim. Still run session-close-protocol steps 2–7 (change-log, HS-REV or review seed, WIP, export).',
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// Build the close_tool block for action intents that flip status at session end.
|
|
570
|
+
function buildCloseTool(intent) {
|
|
571
|
+
return CLOSE_TOOL_MAP[intent] ?? null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const FILE_SPEC_NOTE =
|
|
575
|
+
'Use this. The server generates frontmatter from the canonical template. Do not compose frontmatter from a sibling spec.';
|
|
576
|
+
const FILING_TOOL_MAP = {
|
|
577
|
+
file_milestone: { type: 'milestone' },
|
|
578
|
+
file_epic: { type: 'epic', parentKey: 'milestone', parentExample: '[[HS-MIL-...]]' },
|
|
579
|
+
file_task: { type: 'task', parentKey: 'parent', parentExample: '[[HS-EPIC-...]]' },
|
|
580
|
+
file_diagnose: {
|
|
581
|
+
type: 'diagnose',
|
|
582
|
+
note: `${FILE_SPEC_NOTE} By default a diagnose is filed standalone — it has no parent and does not nest under any epic or milestone, so it lives on its own in the Resolution lane rather than inside a planned work item. Keep it standalone for a cross-cutting bug, or one that isn't part of any single epic's or milestone's scope (the common case). Only set parent: "[[HS-EPIC-...]]" or "[[HS-MIL-...]]" when the bug clearly belongs to that epic's or milestone's work and you want it tracked under it.`,
|
|
583
|
+
},
|
|
584
|
+
file_research: { type: 'research' },
|
|
585
|
+
file_memory_lesson: { type: 'memory-lesson' },
|
|
586
|
+
file_memory_decision: { type: 'memory-decision' },
|
|
587
|
+
file_memory_reference: { type: 'memory-reference' },
|
|
588
|
+
file_memory_ui_pattern: { type: 'memory-ui-pattern' },
|
|
589
|
+
file_review: {
|
|
590
|
+
type: 'review',
|
|
591
|
+
parentKey: 'parent',
|
|
592
|
+
parentExample: '[[<spec-under-review>]]',
|
|
593
|
+
note: `${FILE_SPEC_NOTE} A review's parent is the spec it reviews — parent is required.`,
|
|
594
|
+
},
|
|
595
|
+
file_capture: {
|
|
596
|
+
tool: 'heyspex_active_capture',
|
|
597
|
+
minimal_call: { spec_id: '<HS-CAP-...>' },
|
|
598
|
+
note: 'Captures are session-based; use heyspex_active_capture, NOT heyspex_file_spec.',
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// Build the filing_tool block for a given intent, or null when the intent does
|
|
603
|
+
// not create a spec.
|
|
604
|
+
function buildFilingTool(intent) {
|
|
605
|
+
const spec = FILING_TOOL_MAP[intent];
|
|
606
|
+
if (!spec) return null;
|
|
607
|
+
// Captures (and any future bespoke tool) carry an explicit shape.
|
|
608
|
+
if (spec.tool) {
|
|
609
|
+
return { tool: spec.tool, minimal_call: spec.minimal_call, note: spec.note };
|
|
610
|
+
}
|
|
611
|
+
const minimal_call = { type: spec.type };
|
|
612
|
+
if (spec.parentKey) minimal_call[spec.parentKey] = spec.parentExample;
|
|
613
|
+
minimal_call.name = '<title>';
|
|
614
|
+
return {
|
|
615
|
+
tool: 'heyspex_file_spec',
|
|
616
|
+
minimal_call,
|
|
617
|
+
note: spec.note ?? FILE_SPEC_NOTE,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
622
|
+
// Thread-type-aware session close-out ([[HS-RES-32MJ]]). Close-out is NOT
|
|
623
|
+
// one-size-fits-all: the thread type decides whether there is a close at all and
|
|
624
|
+
// which steps run; the grain ladder (session-close-protocol) decides which spec
|
|
625
|
+
// is the review unit on the execution paths. Thread-type is DECLARED, not
|
|
626
|
+
// inferred — two of the five candidate signals (specs-filed-by-type,
|
|
627
|
+
// research-saved) are not session-scoped on either tier, so a positive
|
|
628
|
+
// declaration is the robust path. Mirrors the prose decision table in
|
|
629
|
+
// session-close-protocol §"Thread-type close-out".
|
|
630
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
631
|
+
const CLOSE_SESSION_THREAD_PATHS = {
|
|
632
|
+
capture_only: {
|
|
633
|
+
label: 'Capture-only — brain-offload via /hs-capture; no claim, no code',
|
|
634
|
+
is_close: false,
|
|
635
|
+
close_tool: false,
|
|
636
|
+
procedures: ['research-capture-protocol'],
|
|
637
|
+
steps: [
|
|
638
|
+
'No session close-out is owed — a capture is not a review unit.',
|
|
639
|
+
'Exit = promote to research (heyspex_promote_capture) or leave the capture pinned.',
|
|
640
|
+
'Do NOT submit_for_review, mint an HS-REV, record a WIP ledger, or flush an audit export.',
|
|
641
|
+
],
|
|
642
|
+
},
|
|
643
|
+
research_only: {
|
|
644
|
+
label: 'Research-only — saved an HS-RES; no claimed spec',
|
|
645
|
+
is_close: false,
|
|
646
|
+
close_tool: false,
|
|
647
|
+
procedures: ['research-capture-protocol'],
|
|
648
|
+
steps: [
|
|
649
|
+
'Save findings via heyspex_save_research / heyspex_complete_research (external scope needs >=1 cited URL).',
|
|
650
|
+
'Research is evergreen: do NOT submit_for_review or flip status, and do NOT mint an HS-REV.',
|
|
651
|
+
'No execution WIP ledger is owed. Audit export (heyspex_export_events) is optional.',
|
|
652
|
+
],
|
|
653
|
+
},
|
|
654
|
+
plan_ingest_only: {
|
|
655
|
+
label: 'Plan/ingest-only — filed milestone+epics; no task execution',
|
|
656
|
+
is_close: false,
|
|
657
|
+
close_tool: false,
|
|
658
|
+
procedures: ['plan-ingest-agent-workflow', 'spec-hierarchy'],
|
|
659
|
+
steps: [
|
|
660
|
+
'Confirm every new spec is registered + linked: heyspex_validate_linkage (heyspex_register_spec for any orphan).',
|
|
661
|
+
'Confirm each milestone/epic carries an authored summary and a re-derived ## Intended outcome.',
|
|
662
|
+
'No task close, no HS-REV, no execution WIP ledger — epics are filed with children: [].',
|
|
663
|
+
],
|
|
664
|
+
},
|
|
665
|
+
one_task: {
|
|
666
|
+
label: 'One-task execution — claimed and completed a single task',
|
|
667
|
+
is_close: true,
|
|
668
|
+
close_tool: true,
|
|
669
|
+
procedures: ['session-close-protocol', 'memory-protocol'],
|
|
670
|
+
steps: [
|
|
671
|
+
'heyspex_submit_for_review({ spec_id }) on the completed unit (status API SSOT — never hand-edit status:). Grain-coercion, parent rollup, and claim release happen server-side.',
|
|
672
|
+
'Append a change-log roll-up / review seed to the task.',
|
|
673
|
+
'Create an HS-REV companion + parent ## Review back-link ONLY if the active review grain is task; otherwise the change-log seed is enough.',
|
|
674
|
+
'heyspex_record_wip for the worked epic (skip only if the epic closed this session).',
|
|
675
|
+
'heyspex_export_events to flush the audit log. Do NOT git commit / push.',
|
|
676
|
+
],
|
|
677
|
+
},
|
|
678
|
+
multi_task_epic: {
|
|
679
|
+
label: 'Multi-task / epic push — completed several tasks under an epic',
|
|
680
|
+
is_close: true,
|
|
681
|
+
close_tool: true,
|
|
682
|
+
procedures: ['session-close-protocol', 'memory-protocol'],
|
|
683
|
+
steps: [
|
|
684
|
+
'heyspex_submit_for_review on each completed unit; parent rollup is automatic when the last child lands.',
|
|
685
|
+
'One consolidated change-log + the HS-REV companion at the ACTIVE review grain (epic in steady-state, milestone in MVP).',
|
|
686
|
+
'heyspex_record_wip once for the worked epic; heyspex_export_events; do NOT git commit / push.',
|
|
687
|
+
],
|
|
688
|
+
},
|
|
689
|
+
resolve_diagnose: {
|
|
690
|
+
label: 'Resolve / diagnose — worked an HS-DIAG or a Resolution item',
|
|
691
|
+
is_close: true,
|
|
692
|
+
close_tool: true,
|
|
693
|
+
procedures: ['session-close-protocol', 'resolution-diagnose-workflow'],
|
|
694
|
+
steps: [
|
|
695
|
+
'If the flag was a stale-cache false positive (the file on disk contradicts it): re-stamp via heyspex_record_alignment — do NOT re-file work that already exists.',
|
|
696
|
+
'If a real diagnose/fix completed: heyspex_submit_for_review on it + change-log; HS-REV per active grain.',
|
|
697
|
+
'heyspex_record_wip if anything remains open; heyspex_export_events.',
|
|
698
|
+
],
|
|
699
|
+
},
|
|
700
|
+
mid_rollover: {
|
|
701
|
+
label: 'Mid-implementation rollover — claimed but not finished',
|
|
702
|
+
is_close: false,
|
|
703
|
+
close_tool: false,
|
|
704
|
+
procedures: ['context-rollover-checkpoint'],
|
|
705
|
+
steps: [
|
|
706
|
+
'This is NOT a session close. Do NOT flip status, mint an HS-REV, or submit_for_review.',
|
|
707
|
+
'Record a lightweight WIP checkpoint (heyspex_record_wip) so the next session resumes; leave the spec in_progress.',
|
|
708
|
+
'See heyspex_preflight({ intent: "context_rollover" }) / the context-rollover-checkpoint procedure for the full checkpoint shape.',
|
|
709
|
+
],
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
const CLOSE_SESSION_THREAD_TYPES = Object.keys(CLOSE_SESSION_THREAD_PATHS);
|
|
714
|
+
|
|
715
|
+
const CLOSE_SESSION_MIXED_RULE =
|
|
716
|
+
'If the session spanned MORE THAN ONE thread type (e.g. filed a plan AND finished a task), request each applicable type and run the UNION of their steps — ordered, idempotent (re-running submit_for_review / export_events is safe), and surface the ambiguity. Never drop an owed obligation; never duplicate one.';
|
|
717
|
+
|
|
718
|
+
function buildCloseSessionPreflight(vaultRoot, threadType, injectedCatalog) {
|
|
719
|
+
const path = CLOSE_SESSION_THREAD_PATHS[threadType];
|
|
720
|
+
if (!path) {
|
|
721
|
+
return {
|
|
722
|
+
error: 'UNKNOWN_THREAD_TYPE',
|
|
723
|
+
message: `Unknown thread_type "${threadType}" for close_session. Valid thread types: ${CLOSE_SESSION_THREAD_TYPES.join(', ')}.`,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
const catalog = injectedCatalog !== undefined ? injectedCatalog : loadCatalog(vaultRoot);
|
|
727
|
+
if (!catalog) {
|
|
728
|
+
return {
|
|
729
|
+
error: 'CATALOG_NOT_FOUND',
|
|
730
|
+
message:
|
|
731
|
+
'specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.',
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const procedures = path.procedures.map((name) => {
|
|
735
|
+
const entry = findEntry(catalog, name);
|
|
736
|
+
return {
|
|
737
|
+
procedure: name,
|
|
738
|
+
severity: 'required',
|
|
739
|
+
path: entry?.path ?? null,
|
|
740
|
+
title: entry?.title ?? null,
|
|
741
|
+
missing_from_catalog: entry ? undefined : true,
|
|
742
|
+
};
|
|
743
|
+
});
|
|
744
|
+
const close_tool = path.close_tool ? buildCloseTool('close_session') : null;
|
|
745
|
+
return {
|
|
746
|
+
intent: 'close_session',
|
|
747
|
+
thread_type: threadType,
|
|
748
|
+
thread_label: path.label,
|
|
749
|
+
is_close: path.is_close,
|
|
750
|
+
steps: path.steps,
|
|
751
|
+
procedures,
|
|
752
|
+
mixed_thread_rule: CLOSE_SESSION_MIXED_RULE,
|
|
753
|
+
...(close_tool ? { close_tool } : {}),
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* `injectedCatalog` — see {@link executeListProcedures}; same contract here
|
|
759
|
+
* (threaded through to `buildCloseSessionPreflight` for the close_session
|
|
760
|
+
* thread-type path too, so a single HTTP fetch covers both branches).
|
|
761
|
+
*/
|
|
762
|
+
export function executeRunPreflight(vaultRoot, args = {}, injectedCatalog) {
|
|
763
|
+
const intent = String(args.intent ?? '').trim();
|
|
764
|
+
if (!intent) {
|
|
765
|
+
return {
|
|
766
|
+
error: 'MISSING_INTENT',
|
|
767
|
+
message: `intent is required. Valid intents: ${VALID_INTENTS.join(', ')}.`,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
// Thread-type-aware close-out: when a thread_type is declared, return that
|
|
771
|
+
// type's CONDITIONAL close path instead of the static checklist ([[HS-RES-32MJ]]).
|
|
772
|
+
const threadType = String(args.thread_type ?? '').trim();
|
|
773
|
+
if (intent === 'close_session' && threadType) {
|
|
774
|
+
return buildCloseSessionPreflight(vaultRoot, threadType, injectedCatalog);
|
|
775
|
+
}
|
|
776
|
+
const recommendations = PREFLIGHT_INTENT_MAP[intent];
|
|
777
|
+
if (!recommendations) {
|
|
778
|
+
return {
|
|
779
|
+
error: 'UNKNOWN_INTENT',
|
|
780
|
+
message: `Unknown intent "${intent}". Valid intents: ${VALID_INTENTS.join(', ')}.`,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const catalog = injectedCatalog !== undefined ? injectedCatalog : loadCatalog(vaultRoot);
|
|
784
|
+
if (!catalog) {
|
|
785
|
+
return {
|
|
786
|
+
error: 'CATALOG_NOT_FOUND',
|
|
787
|
+
message:
|
|
788
|
+
'specs/protocols/.catalog.json not found. Run `node scripts/build-procedure-catalog.mjs`.',
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const procedures = recommendations.map((row) => {
|
|
793
|
+
const entry = findEntry(catalog, row.procedure);
|
|
794
|
+
return {
|
|
795
|
+
procedure: row.procedure,
|
|
796
|
+
why: row.why,
|
|
797
|
+
severity: row.severity,
|
|
798
|
+
path: entry?.path ?? null,
|
|
799
|
+
title: entry?.title ?? null,
|
|
800
|
+
missing_from_catalog: entry ? undefined : true,
|
|
801
|
+
};
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
const filing_tool = buildFilingTool(intent);
|
|
805
|
+
const close_tool = buildCloseTool(intent);
|
|
806
|
+
|
|
807
|
+
const result = {
|
|
808
|
+
intent,
|
|
809
|
+
procedures,
|
|
810
|
+
...(filing_tool ? { filing_tool } : {}),
|
|
811
|
+
...(close_tool ? { close_tool } : {}),
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
// Family-overlap advisory for filing intents: warn agents to check whether
|
|
815
|
+
// a closed family already covered this goal with no iterates_on edge.
|
|
816
|
+
const FILING_ADVISORY =
|
|
817
|
+
'Family-overlap check (warn-only): before filing, check whether a CLOSED family already shipped against this goal with no iterates_on: edge — compare both TITLE and JTBD (job-to-be-done) statement, not title alone, since two differently-titled specs can still be the same job (owner-approved 2026-08-23 job-based-duplicate-detection extension) — if so this may be round 2; see entity-model §Iteration lineage. Never blocks filing.';
|
|
818
|
+
const FILING_INTENTS_WITH_ADVISORY = ['file_epic', 'file_task', 'promote_research'];
|
|
819
|
+
if (FILING_INTENTS_WITH_ADVISORY.includes(intent)) {
|
|
820
|
+
result.advisory = FILING_ADVISORY;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Static close_session call (no thread_type declared): nudge toward the
|
|
824
|
+
// conditional path so the agent runs the right minimal-complete close-out.
|
|
825
|
+
if (intent === 'close_session') {
|
|
826
|
+
result.thread_type_hint = {
|
|
827
|
+
note: 'Close-out is thread-type-aware. Re-call heyspex_preflight({ intent: "close_session", thread_type: "<type>" }) to get the conditional close path for what this session actually did.',
|
|
828
|
+
thread_types: CLOSE_SESSION_THREAD_TYPES.map((t) => ({
|
|
829
|
+
thread_type: t,
|
|
830
|
+
label: CLOSE_SESSION_THREAD_PATHS[t].label,
|
|
831
|
+
is_close: CLOSE_SESSION_THREAD_PATHS[t].is_close,
|
|
832
|
+
})),
|
|
833
|
+
mixed: CLOSE_SESSION_MIXED_RULE,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return result;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export const PREFLIGHT_INTENTS = VALID_INTENTS;
|
|
841
|
+
export { CLOSE_SESSION_THREAD_TYPES };
|
|
842
|
+
|
|
843
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
844
|
+
// Read-path selector wiring ([[HS-DJWP-MCPC-TASK-wire-the-mcp-server-s-vault-reads]]
|
|
845
|
+
// of [[HS-T6GS-MCPC-EPIC-mcp-server-reads-the-vault-through-the]]).
|
|
846
|
+
//
|
|
847
|
+
// The three functions below are the cloud-reliant siblings of
|
|
848
|
+
// `executeListProcedures` / `executeGetProcedure` / `executeRunPreflight`.
|
|
849
|
+
// They never call `loadCatalog(vaultRoot)` in the `neon` tier — the catalog
|
|
850
|
+
// (or, for `executeGetProcedureViaTier`, the full procedure body) is fetched
|
|
851
|
+
// once over HTTP via the caller-supplied `consoleFetch` and handed to the
|
|
852
|
+
// existing pure functions as an `injectedCatalog`, so the filtering /
|
|
853
|
+
// preflight-mapping logic itself is never duplicated. In `disk` tier each
|
|
854
|
+
// wrapper calls the untouched, byte-for-byte-unchanged pure function — same
|
|
855
|
+
// disk-only code path as before this task.
|
|
856
|
+
//
|
|
857
|
+
// `consoleFetch` is passed in (not imported) because this module is
|
|
858
|
+
// deliberately transport-free "pure-logic" (see file header) — cli.mjs owns
|
|
859
|
+
// the bound `makeConsoleFetch` client and threads it through here, mirroring
|
|
860
|
+
// how `readViaSelector` itself never imports console-fetch.mjs.
|
|
861
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
862
|
+
|
|
863
|
+
/** Row shape from `GET /api/procedures` — mirrors `summarize()` above (see that route's docstring: "do not invent a new shape here"). */
|
|
864
|
+
function catalogFromProceduresRoute(body) {
|
|
865
|
+
const rows = Array.isArray(body?.procedures) ? body.procedures : [];
|
|
866
|
+
return { procedures: rows };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Fetch the procedure catalog via `GET /api/procedures` (disk: `loadCatalog`).
|
|
871
|
+
* Exported for callers that need the raw catalog object outside the three
|
|
872
|
+
* higher-level wrappers below (none today; kept for symmetry / testability).
|
|
873
|
+
*/
|
|
874
|
+
export async function loadCatalogViaTier({ vaultRoot, tier, consoleFetch, consoleUrl }) {
|
|
875
|
+
return readViaSelector({
|
|
876
|
+
tier,
|
|
877
|
+
diskRead: () => loadCatalog(vaultRoot),
|
|
878
|
+
neonRead: async () => {
|
|
879
|
+
const res = await consoleFetch('/api/procedures');
|
|
880
|
+
if (!res.ok) {
|
|
881
|
+
throw new Error(
|
|
882
|
+
`GET /api/procedures returned ${res.status}: ${res.error || JSON.stringify(res.body ?? {})}`,
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
return catalogFromProceduresRoute(res.body);
|
|
886
|
+
},
|
|
887
|
+
routeLabel: 'GET /api/procedures',
|
|
888
|
+
consoleUrl,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/** Cloud-reliant sibling of {@link executeListProcedures}. */
|
|
893
|
+
export async function executeListProceduresViaTier({ vaultRoot, args, tier, consoleFetch, consoleUrl }) {
|
|
894
|
+
return readViaSelector({
|
|
895
|
+
tier,
|
|
896
|
+
diskRead: () => executeListProcedures(vaultRoot, args),
|
|
897
|
+
neonRead: async () => {
|
|
898
|
+
const catalog = await loadCatalogViaTier({ vaultRoot, tier: 'neon', consoleFetch, consoleUrl });
|
|
899
|
+
if (!catalog) {
|
|
900
|
+
return {
|
|
901
|
+
error: 'CATALOG_NOT_FOUND',
|
|
902
|
+
message: 'GET /api/procedures returned no catalog (console reports it missing).',
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
// Filtering happens HERE, client-side — the route has no tag/trigger/query
|
|
906
|
+
// params (it always answers the full unfiltered set), unlike the disk path
|
|
907
|
+
// which filters inside executeListProcedures itself. Passing the fetched
|
|
908
|
+
// catalog as `injectedCatalog` runs the SAME filter logic either way.
|
|
909
|
+
return executeListProcedures(vaultRoot, args, catalog);
|
|
910
|
+
},
|
|
911
|
+
routeLabel: 'GET /api/procedures',
|
|
912
|
+
consoleUrl,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** Cloud-reliant sibling of {@link executeGetProcedure}. Single round trip: `GET /api/procedures/<name>` returns catalog entry + full body together. */
|
|
917
|
+
export async function executeGetProcedureViaTier({ vaultRoot, args, tier, consoleFetch, consoleUrl }) {
|
|
918
|
+
return readViaSelector({
|
|
919
|
+
tier,
|
|
920
|
+
diskRead: () => executeGetProcedure(vaultRoot, args),
|
|
921
|
+
neonRead: async () => {
|
|
922
|
+
const name = String(args?.name ?? '').trim();
|
|
923
|
+
if (!name) {
|
|
924
|
+
return { error: 'MISSING_NAME', message: 'name is required (e.g. "spec-hierarchy")' };
|
|
925
|
+
}
|
|
926
|
+
const res = await consoleFetch(`/api/procedures/${encodeURIComponent(name)}`);
|
|
927
|
+
if (res.ok) {
|
|
928
|
+
return { procedure: res.body?.procedure, content: res.body?.content, source: res.body?.source ?? null };
|
|
929
|
+
}
|
|
930
|
+
// Aligned with the sibling *ViaTier functions ([[HS-DJWP-MCPC-TASK]]
|
|
931
|
+
// round 3 fix — a 500 {error:'boom'} was previously swallowed as a real
|
|
932
|
+
// answer regardless of status code): ONLY a 404 PROCEDURE_NOT_FOUND is a
|
|
933
|
+
// real "no such procedure" answer, matching the route's own contract
|
|
934
|
+
// (its docstring: "an unknown name is a 404 error code, never an empty
|
|
935
|
+
// 200 success"). Everything else — a 500 CATALOG_NOT_FOUND included, a
|
|
936
|
+
// reachable console reporting a server-side config break — throws so the
|
|
937
|
+
// selector surfaces CONSOLE_UNREACHABLE_CLOUD_MODE rather than silently
|
|
938
|
+
// degrading; the epic's no-silent-degrade rule treats "console answered,
|
|
939
|
+
// but the answer means something is broken" the same as unreachable.
|
|
940
|
+
if (res.status === 404 && res.body?.error === 'PROCEDURE_NOT_FOUND') {
|
|
941
|
+
return { error: res.body.error, message: res.body.message };
|
|
942
|
+
}
|
|
943
|
+
throw new Error(
|
|
944
|
+
`GET /api/procedures/${name} returned ${res.status}: ${res.body?.error ? `${res.body.error}${res.body.message ? ` — ${res.body.message}` : ''}` : res.error || JSON.stringify(res.body ?? {})}`,
|
|
945
|
+
);
|
|
946
|
+
},
|
|
947
|
+
routeLabel: 'GET /api/procedures/[name]',
|
|
948
|
+
consoleUrl,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** Cloud-reliant sibling of {@link executeRunPreflight}. One catalog fetch covers both the static intent map AND the close_session thread-type path. */
|
|
953
|
+
export async function executeRunPreflightViaTier({ vaultRoot, args, tier, consoleFetch, consoleUrl }) {
|
|
954
|
+
return readViaSelector({
|
|
955
|
+
tier,
|
|
956
|
+
diskRead: () => executeRunPreflight(vaultRoot, args),
|
|
957
|
+
neonRead: async () => {
|
|
958
|
+
const catalog = await loadCatalogViaTier({ vaultRoot, tier: 'neon', consoleFetch, consoleUrl });
|
|
959
|
+
if (!catalog) {
|
|
960
|
+
return {
|
|
961
|
+
error: 'CATALOG_NOT_FOUND',
|
|
962
|
+
message: 'GET /api/procedures returned no catalog (console reports it missing).',
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
return executeRunPreflight(vaultRoot, args, catalog);
|
|
966
|
+
},
|
|
967
|
+
routeLabel: 'GET /api/procedures',
|
|
968
|
+
consoleUrl,
|
|
969
|
+
});
|
|
970
|
+
}
|