heyspex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +157 -0
  2. package/bin/hs.mjs +1028 -0
  3. package/bin/postinstall.mjs +88 -0
  4. package/dev-factory/lib/fleet-readiness.mjs +274 -0
  5. package/dev-factory/lib/fleet-state.mjs +250 -0
  6. package/dev-factory/lib/sufficiency-score.mjs +441 -0
  7. package/dist/cli/hs-check.mjs +747 -0
  8. package/dist/cli/hs-claim.mjs +541 -0
  9. package/dist/cli/hs-connect.mjs +3436 -0
  10. package/dist/cli/hs-defer.mjs +8455 -0
  11. package/dist/cli/hs-env.mjs +36845 -0
  12. package/dist/cli/hs-init.mjs +1145 -0
  13. package/dist/cli/hs-install.mjs +1390 -0
  14. package/dist/cli/hs-join.mjs +38553 -0
  15. package/dist/cli/hs-vault-pull.mjs +7782 -0
  16. package/dist/cli/hs-vault.mjs +4166 -0
  17. package/dist/cli/hs-workspace.mjs +2898 -0
  18. package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
  19. package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
  20. package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
  21. package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
  22. package/docs/mcp-client-config.md +200 -0
  23. package/package.json +124 -0
  24. package/scripts/check-mcp-freshness.mjs +379 -0
  25. package/scripts/dev-server-status.mjs +722 -0
  26. package/scripts/gateway-skills-lib.mjs +421 -0
  27. package/scripts/lib/console-url-resolver.mjs +152 -0
  28. package/scripts/lib/dev-mode.mjs +369 -0
  29. package/scripts/lib/hs-link.mjs +154 -0
  30. package/scripts/lib/hs-source-commit.mjs +207 -0
  31. package/scripts/lib/hs-source-log.mjs +366 -0
  32. package/scripts/lib/hs-source-preflight.mjs +402 -0
  33. package/scripts/lib/hs-source-push.mjs +307 -0
  34. package/scripts/lib/hs-source-secret-scan.mjs +263 -0
  35. package/scripts/lib/standalone-doctor.mjs +89 -0
  36. package/scripts/vault-id-lib.mjs +173 -0
  37. package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
  38. package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
  39. package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
  40. package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
  41. package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
  42. package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
  43. package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
  44. package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
  45. package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
  46. package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
  47. package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
  48. package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
  49. package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
  50. package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
  51. package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
  52. package/vendor/heyspex-mcp/cli.mjs +15754 -0
  53. package/vendor/heyspex-mcp/config-settings.mjs +419 -0
  54. package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
  55. package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
  56. package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
  57. package/vendor/heyspex-mcp/design-tools.mjs +449 -0
  58. package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
  59. package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
  60. package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
  61. package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
  62. package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
  63. package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
  64. package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
  65. package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
  66. package/vendor/heyspex-mcp/next-review.mjs +548 -0
  67. package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
  68. package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
  69. package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
  70. package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
  71. package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
  72. package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
  73. package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
  74. package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
  75. package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
  76. package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
  77. package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
  78. package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
  79. package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
  80. package/vendor/heyspex-mcp/read-path.mjs +138 -0
  81. package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
  82. package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
  83. package/vendor/heyspex-mcp/register-spec.mjs +417 -0
  84. package/vendor/heyspex-mcp/research-focus.mjs +476 -0
  85. package/vendor/heyspex-mcp/research-tools.mjs +109 -0
  86. package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
  87. package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
  88. package/vendor/heyspex-mcp/review-focus.mjs +55 -0
  89. package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
  90. package/vendor/heyspex-mcp/run-front.mjs +60 -0
  91. package/vendor/heyspex-mcp/scheduler.mjs +813 -0
  92. package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
  93. package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
  94. package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
  95. package/vendor/heyspex-mcp/split-research.mjs +832 -0
  96. package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
  97. package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
  98. package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
  99. package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
  100. package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
  101. package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
  102. package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
  103. package/vendor/heyspex-mcp/why-now.mjs +64 -0
  104. package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
@@ -0,0 +1,326 @@
1
+ /**
2
+ * heyspex_regen_task + heyspex_regen_tasks_for_epic — user-invoked regen
3
+ * tools (T6 of [[HS-EPIC-VQZ8]], implementation under [[HS-EPIC-HSN1]]).
4
+ *
5
+ * The tools operate in two modes:
6
+ *
7
+ * 1. **Brief mode** (no `commit` param). Returns a deterministic context
8
+ * brief — parent epic, acceptance criteria, current task body, linked
9
+ * plans, sibling tasks. The calling agent reads this and produces a
10
+ * new task body in its own thinking.
11
+ *
12
+ * 2. **Commit mode** (`commit: true`, `new_body` supplied). Soft-deletes
13
+ * the existing task file to `.trash/<epic-slug>/<timestamp>-<basename>.md`,
14
+ * then writes the new body in place. Reversible via the `.trash/`
15
+ * directory — recovery is `git mv` or copy back from `.trash/`.
16
+ *
17
+ * Per [[destructive-action-checklist]] both modes are soft-delete only; no
18
+ * `fs.unlink`. The brief shape mirrors `buildDecompositionPromptMarkdown` so
19
+ * agents see a consistent surface across all the breakdown-related tools.
20
+ */
21
+
22
+ import { execFileSync } from 'node:child_process';
23
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs';
24
+ import { dirname, join } from 'node:path';
25
+
26
+ const TERMINAL_STATUSES = new Set([
27
+ 'closed',
28
+ 'shipped',
29
+ 'in_review',
30
+ 'archived',
31
+ 'cancelled',
32
+ 'duplicate',
33
+ 'hibernated',
34
+ 'superseded',
35
+ ]);
36
+
37
+ function timestamp() {
38
+ return new Date().toISOString().replace(/[:.]/g, '-');
39
+ }
40
+
41
+ /**
42
+ * Soft-delete a spec file to `<vault>/specs/.trash/<bucket>/<ts>-<basename>`.
43
+ * `bucket` lets the caller group related retirements (e.g. one epic's
44
+ * batch lands in `.trash/<epic-slug>/`). Returns the absolute path of the
45
+ * file in `.trash/` so the recovery procedure can find it.
46
+ */
47
+ export function softDeleteToTrash(vaultRoot, relPath, bucket = 'misc') {
48
+ const abs = join(vaultRoot, relPath);
49
+ if (!existsSync(abs)) return { ok: false, error: 'not_found', source: relPath };
50
+ const basename = relPath.split(/[\\/]/).pop() || `${timestamp()}-spec.md`;
51
+ const trashDir = join(vaultRoot, 'specs', '.trash', bucket);
52
+ mkdirSync(trashDir, { recursive: true });
53
+ const dest = join(trashDir, `${timestamp()}-${basename}`);
54
+ renameSync(abs, dest);
55
+ const trashRel = dest.slice(vaultRoot.length).replace(/\\/g, '/').replace(/^\/+/, '');
56
+ return { ok: true, trash_path: trashRel, original_rel_path: relPath };
57
+ }
58
+
59
+ function gitLastCommitTime(vaultRoot, relPath) {
60
+ try {
61
+ const out = execFileSync('git', ['log', '-1', '--format=%ct', '--', relPath], {
62
+ cwd: vaultRoot,
63
+ encoding: 'utf8',
64
+ stdio: ['ignore', 'pipe', 'ignore'],
65
+ });
66
+ const secs = Number(out.trim());
67
+ return Number.isFinite(secs) ? new Date(secs * 1000).toISOString() : null;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function loadSpecRecord(loadTask, ref) {
74
+ if (!ref || typeof loadTask !== 'function') return null;
75
+ const t = loadTask(ref);
76
+ return t || null;
77
+ }
78
+
79
+ function summarizeSpec(spec) {
80
+ if (!spec) return null;
81
+ const fm = spec.frontmatter || {};
82
+ return {
83
+ id: spec.id || fm.id,
84
+ name: spec.name || fm.name,
85
+ type: spec.type || fm.type,
86
+ status: spec.status || fm.status || 'unknown',
87
+ rel_path: spec.relPath || spec.rel_path || spec.specPath || null,
88
+ body: spec.body || '',
89
+ };
90
+ }
91
+
92
+ function wikilinkTarget(raw) {
93
+ if (typeof raw !== 'string') return null;
94
+ const m = raw.match(/\[\[([^\]]+)\]\]/);
95
+ return (m ? m[1].trim() : raw.trim()) || null;
96
+ }
97
+
98
+ function parentEpicOf(task, loadTask) {
99
+ const parentRef = wikilinkTarget(task?.frontmatter?.parent);
100
+ if (!parentRef) return null;
101
+ return loadSpecRecord(loadTask, parentRef);
102
+ }
103
+
104
+ function siblingTasksOf(epic, loadTask, excludeId) {
105
+ const refs = Array.isArray(epic?.frontmatter?.children)
106
+ ? epic.frontmatter.children.map(wikilinkTarget).filter(Boolean)
107
+ : [];
108
+ const out = [];
109
+ for (const ref of refs) {
110
+ const t = loadSpecRecord(loadTask, ref);
111
+ if (!t) continue;
112
+ if ((t.id || t.frontmatter?.id) === excludeId) continue;
113
+ out.push(summarizeSpec(t));
114
+ }
115
+ return out;
116
+ }
117
+
118
+ function relatedPlans(spec) {
119
+ const fm = spec?.frontmatter || {};
120
+ return (Array.isArray(fm.related) ? fm.related : []).map(wikilinkTarget).filter(Boolean);
121
+ }
122
+
123
+ function epicSlugFromRel(relPath) {
124
+ // specs/epics/HS-EPIC-XXXX-slug.md → HS-EPIC-XXXX-slug
125
+ if (!relPath) return 'misc';
126
+ const basename = relPath.split(/[\\/]/).pop() || '';
127
+ return basename.replace(/\.md$/i, '') || 'misc';
128
+ }
129
+
130
+ /**
131
+ * `heyspex_regen_task` — single-task regen.
132
+ *
133
+ * - Brief mode: returns context for the agent to author a new task body.
134
+ * - Commit mode (`commit: true` + `new_body`): soft-delete old to `.trash/`,
135
+ * write `new_body` in its place.
136
+ *
137
+ * Both modes return `{ ok, mode, task, epic, brief?, commit_result? }`.
138
+ */
139
+ export function executeRegenTask(vaultRoot, { task_id, commit = false, new_body, retire_reason, loadTask }) {
140
+ if (!task_id) {
141
+ return { ok: false, error: 'MISSING_TASK_ID', message: 'task_id is required' };
142
+ }
143
+ const task = loadSpecRecord(loadTask, task_id);
144
+ if (!task) {
145
+ return { ok: false, error: 'TASK_NOT_FOUND', task_id };
146
+ }
147
+ const taskSummary = summarizeSpec(task);
148
+ if (!taskSummary) {
149
+ return { ok: false, error: 'TASK_INVALID', task_id };
150
+ }
151
+ const epic = parentEpicOf(task, loadTask);
152
+ const epicSummary = epic ? summarizeSpec(epic) : null;
153
+
154
+ if (commit) {
155
+ if (typeof new_body !== 'string' || !new_body.trim()) {
156
+ return {
157
+ ok: false,
158
+ error: 'MISSING_NEW_BODY',
159
+ message: 'commit: true requires non-empty new_body',
160
+ };
161
+ }
162
+ if (!taskSummary.rel_path) {
163
+ return { ok: false, error: 'NO_REL_PATH', task_id };
164
+ }
165
+ const bucket = epicSummary ? epicSlugFromRel(epicSummary.rel_path) : 'misc';
166
+ const deletion = softDeleteToTrash(vaultRoot, taskSummary.rel_path, bucket);
167
+ if (!deletion.ok) {
168
+ return { ok: false, error: 'TRASH_FAILED', detail: deletion };
169
+ }
170
+ // Write the new body at the original rel_path. The agent's new_body is
171
+ // expected to include frontmatter; if it doesn't, we don't reconstruct
172
+ // it — that's an agent error and surfaces as a malformed spec at the
173
+ // next /hs-next.
174
+ const destAbs = join(vaultRoot, taskSummary.rel_path);
175
+ mkdirSync(dirname(destAbs), { recursive: true });
176
+ writeFileSync(destAbs, new_body, 'utf8');
177
+ return {
178
+ ok: true,
179
+ mode: 'commit',
180
+ task: { id: taskSummary.id, rel_path: taskSummary.rel_path },
181
+ retired_to: deletion.trash_path,
182
+ retire_reason: retire_reason || null,
183
+ epic: epicSummary ? { id: epicSummary.id } : null,
184
+ message:
185
+ `Soft-deleted previous body to ${deletion.trash_path}; wrote new body at ${taskSummary.rel_path}. ` +
186
+ `Recovery: git restore or copy from ${deletion.trash_path}.`,
187
+ };
188
+ }
189
+
190
+ // Brief mode — return the context the agent needs to author the new body.
191
+ return {
192
+ ok: true,
193
+ mode: 'brief',
194
+ task: {
195
+ ...taskSummary,
196
+ last_git_commit_at: gitLastCommitTime(vaultRoot, taskSummary.rel_path),
197
+ },
198
+ epic: epicSummary
199
+ ? {
200
+ ...epicSummary,
201
+ last_git_commit_at: gitLastCommitTime(vaultRoot, epicSummary.rel_path),
202
+ }
203
+ : null,
204
+ sibling_tasks: epic ? siblingTasksOf(epic, loadTask, taskSummary.id) : [],
205
+ related_plans: relatedPlans(task),
206
+ next_action:
207
+ 'Author a new task body (markdown + frontmatter) grounded in the epic acceptance criteria + ' +
208
+ 'related_plans + any context from sibling_tasks. Then re-call this tool with ' +
209
+ "`{ task_id, commit: true, new_body, retire_reason }` to soft-delete the old body and write the new one.",
210
+ };
211
+ }
212
+
213
+ /**
214
+ * `heyspex_regen_tasks_for_epic` — epic-scope Stage-1-2-3 regen.
215
+ *
216
+ * - Brief mode: returns the context the agent needs (epic acceptance,
217
+ * all current tasks, linked plans) plus an ordered Stage-1-2-3 plan.
218
+ * - Commit mode (`commit: true` + `task_specs[]`): each entry is either
219
+ * `{ id, new_body, retire_reason }` (replace existing) or `{ new_id, body, rel_path }`
220
+ * (file new). Each retirement soft-deletes to `.trash/<epic-slug>/`.
221
+ *
222
+ * The tool is deliberately deterministic — the LLM thinking happens between
223
+ * brief mode and commit mode in the calling agent.
224
+ */
225
+ export function executeRegenTasksForEpic(vaultRoot, { epic_id, commit = false, task_specs, loadTask }) {
226
+ if (!epic_id) {
227
+ return { ok: false, error: 'MISSING_EPIC_ID', message: 'epic_id is required' };
228
+ }
229
+ const epic = loadSpecRecord(loadTask, epic_id);
230
+ if (!epic) {
231
+ return { ok: false, error: 'EPIC_NOT_FOUND', epic_id };
232
+ }
233
+ const epicSummary = summarizeSpec(epic);
234
+ if (!epicSummary || (epicSummary.type || '').toLowerCase() !== 'epic') {
235
+ return { ok: false, error: 'NOT_AN_EPIC', epic_id };
236
+ }
237
+ const childRefs = Array.isArray(epic.frontmatter?.children)
238
+ ? epic.frontmatter.children.map(wikilinkTarget).filter(Boolean)
239
+ : [];
240
+ const currentTasks = [];
241
+ for (const ref of childRefs) {
242
+ const t = loadSpecRecord(loadTask, ref);
243
+ if (!t) continue;
244
+ currentTasks.push({
245
+ ...summarizeSpec(t),
246
+ last_git_commit_at: gitLastCommitTime(vaultRoot, t.relPath || t.rel_path || t.specPath),
247
+ });
248
+ }
249
+
250
+ if (commit) {
251
+ if (!Array.isArray(task_specs)) {
252
+ return {
253
+ ok: false,
254
+ error: 'MISSING_TASK_SPECS',
255
+ message: 'commit: true requires task_specs: [{ id, new_body, retire_reason } | { new_id, body, rel_path }]',
256
+ };
257
+ }
258
+ const bucket = epicSlugFromRel(epicSummary.rel_path);
259
+ const retired = [];
260
+ const wrote = [];
261
+ const errors = [];
262
+ for (const entry of task_specs) {
263
+ if (entry && typeof entry === 'object' && entry.id && typeof entry.new_body === 'string') {
264
+ const target = loadSpecRecord(loadTask, entry.id);
265
+ const rel = target?.relPath || target?.rel_path || target?.specPath;
266
+ if (!rel) {
267
+ errors.push({ id: entry.id, error: 'NO_REL_PATH' });
268
+ continue;
269
+ }
270
+ const del = softDeleteToTrash(vaultRoot, rel, bucket);
271
+ if (!del.ok) {
272
+ errors.push({ id: entry.id, error: 'TRASH_FAILED', detail: del });
273
+ continue;
274
+ }
275
+ const abs = join(vaultRoot, rel);
276
+ mkdirSync(dirname(abs), { recursive: true });
277
+ writeFileSync(abs, entry.new_body, 'utf8');
278
+ retired.push({ id: entry.id, trash_path: del.trash_path, retire_reason: entry.retire_reason || null });
279
+ wrote.push({ id: entry.id, rel_path: rel });
280
+ } else if (entry && typeof entry === 'object' && entry.new_id && typeof entry.body === 'string' && entry.rel_path) {
281
+ const abs = join(vaultRoot, entry.rel_path);
282
+ if (existsSync(abs)) {
283
+ errors.push({ new_id: entry.new_id, error: 'CONFLICT', rel_path: entry.rel_path });
284
+ continue;
285
+ }
286
+ mkdirSync(dirname(abs), { recursive: true });
287
+ writeFileSync(abs, entry.body, 'utf8');
288
+ wrote.push({ id: entry.new_id, rel_path: entry.rel_path });
289
+ } else {
290
+ errors.push({ entry, error: 'INVALID_SHAPE' });
291
+ }
292
+ }
293
+ return {
294
+ ok: errors.length === 0,
295
+ mode: 'commit',
296
+ epic: { id: epicSummary.id },
297
+ retired,
298
+ wrote,
299
+ errors,
300
+ bucket,
301
+ message:
302
+ `Stage-1-2-3 commit for ${epicSummary.id}: retired ${retired.length}, wrote ${wrote.length}, errors ${errors.length}.` +
303
+ ` Recovery from .trash/specs/.trash/${bucket}/.`,
304
+ };
305
+ }
306
+
307
+ // Brief mode — Stage 1-2-3 instructions + context.
308
+ return {
309
+ ok: true,
310
+ mode: 'brief',
311
+ epic: {
312
+ ...epicSummary,
313
+ last_git_commit_at: gitLastCommitTime(vaultRoot, epicSummary.rel_path),
314
+ },
315
+ current_tasks: currentTasks,
316
+ open_task_count: currentTasks.filter((t) => !TERMINAL_STATUSES.has(String(t.status).toLowerCase())).length,
317
+ related_plans: relatedPlans(epic),
318
+ stage_1_generate:
319
+ 'Read the epic Acceptance criteria + related_plans. Ignore current_tasks at this step. Produce a fresh task set, one per acceptance section.',
320
+ stage_2_reconcile:
321
+ 'Compare your fresh set against current_tasks. Fold residual value (e.g. a child task that captured a constraint not in the acceptance criteria) into the new set. Mark which current_tasks are superseded.',
322
+ stage_3_retire:
323
+ "Re-call this tool with `{ epic_id, commit: true, task_specs: [{ id, new_body, retire_reason }, ...] }` to soft-delete superseded tasks to .trash/ and write the new bodies. New tasks not replacing an existing one use the `{ new_id, body, rel_path }` shape.",
324
+ recovery: 'All retired bodies land in .trash/specs/.trash/<epic-slug>/; recoverable via git or copy.',
325
+ };
326
+ }