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,284 @@
1
+ /**
2
+ * Deferred split-child reclaim + Resolution DLQ ([[HS-TASK-6OU5-deferred-work-reclaim-and-resolution-dlq]]).
3
+ * Consumes lease/heartbeat reclaim from [[HS-EPIC-96YK-claim-leases-heartbeats-and-passive-nudg]] —
4
+ * does not implement the sweep itself.
5
+ */
6
+
7
+ import { readFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ import { allocateRandomVaultId } from '../../scripts/vault-id-lib.mjs';
11
+ import { yamlScalar } from './yaml-scalar.mjs';
12
+ import { mintFamilyKey, mintFileKey, parseSpecId } from './spec-id-lib.mjs';
13
+ import {
14
+ isoNow,
15
+ deriveSlug,
16
+ getFrontmatterBlock,
17
+ getScalarField,
18
+ setScalarField,
19
+ bumpUpdated,
20
+ appendChangeLogRow,
21
+ commitWrites,
22
+ } from './promotion-helpers.mjs';
23
+ import { extractHandoffEnvelopeYaml } from '../../spex-console/src/lib/indexer/handoff-envelope.mjs';
24
+ import { buildDiagnoseSkeletonSections } from './diagnose-skeleton.mjs';
25
+
26
+ /** Default cap when frontmatter omits `attempt_count` ([[HS-TASK-6OU5]]). */
27
+ export const DEFAULT_DEFER_RECLAIM_ATTEMPT_CAP = 3;
28
+
29
+ function collectOccupiedDiagnoseIds(vaultRoot) {
30
+ const occupied = new Set();
31
+ const dir = join(vaultRoot, 'specs', 'diagnoses');
32
+ if (!existsSync(dir)) return occupied;
33
+ for (const file of readdirSync(dir)) {
34
+ // Key the occupied set on the canonical family-first id (dual-read on both
35
+ // the basename and the frontmatter id) so suffix-collision detection matches
36
+ // what allocateRandomVaultId generates/compares ([[HS-EPIC-DLE3]]).
37
+ const fromName = parseSpecId(file.replace(/\.md$/i, ''));
38
+ if (fromName && fromName.typeLabel === 'DIAG') {
39
+ occupied.add(`HS-${fromName.familyKey}-DIAG`);
40
+ }
41
+ try {
42
+ const raw = readFileSync(join(dir, file), 'utf8');
43
+ const idMatch = raw.match(/^id:\s*(\S+)/m);
44
+ if (idMatch) {
45
+ const fromId = parseSpecId(idMatch[1].replace(/^['"]|['"]$/g, ''));
46
+ if (fromId && fromId.typeLabel === 'DIAG') {
47
+ occupied.add(`HS-${fromId.familyKey}-DIAG`);
48
+ }
49
+ }
50
+ } catch {
51
+ /* skip */
52
+ }
53
+ }
54
+ return occupied;
55
+ }
56
+
57
+ export function readAttemptCount(fmBlock) {
58
+ const raw = getScalarField(fmBlock, 'attempt_count');
59
+ if (raw == null || raw === '') return 0;
60
+ const n = Number.parseInt(String(raw), 10);
61
+ return Number.isFinite(n) && n >= 0 ? n : 0;
62
+ }
63
+
64
+ export function isDeferredSplitTaskRaw(raw) {
65
+ const fm = getFrontmatterBlock(raw);
66
+ if (!fm) return false;
67
+ const splitOf = getScalarField(fm.block, 'split_of');
68
+ const splitIndex = getScalarField(fm.block, 'split_index');
69
+ return Boolean(splitOf && splitIndex);
70
+ }
71
+
72
+ function handoffEnvelopeSectionFromBody(body) {
73
+ const extracted = extractHandoffEnvelopeYaml(body);
74
+ if (!extracted || 'missingFence' in extracted) return '';
75
+ return `## Handoff envelope
76
+
77
+ \`\`\`yaml
78
+ ${extracted.yaml.trimEnd()}
79
+ \`\`\``;
80
+ }
81
+
82
+ function buildDlqDiagnoseContent({
83
+ diagnoseId,
84
+ fileKey,
85
+ familyKey,
86
+ taskBasename,
87
+ taskName,
88
+ attemptCount,
89
+ reclaimReason,
90
+ handoffSection,
91
+ now,
92
+ }) {
93
+ const slugPart = deriveSlug(`Deferred work DLQ — ${taskName}`);
94
+ const today = now.slice(0, 10);
95
+ const fm = `---
96
+ id: ${diagnoseId}
97
+ file_id: ${fileKey}
98
+ family: ${familyKey}
99
+ name: ${yamlScalar(`Deferred work DLQ — ${taskName}`)}
100
+ type: diagnose
101
+ status: open
102
+ owner: pwher
103
+ severity: warning
104
+ summary: |-
105
+ Deferred split child [[${taskBasename}]] exceeded the reclaim attempt cap (${attemptCount}) after repeated lease/heartbeat expiry.
106
+ Routed to Resolution for human triage; verify partial work on disk before continuing.
107
+ tags:
108
+ - delegation
109
+ - dlq
110
+ related:
111
+ - '[[${taskBasename}]]'
112
+ created: '${now}'
113
+ updated: '${now}'
114
+ ---
115
+ `;
116
+
117
+ const body = `## Symptom
118
+
119
+ Deferred split child [[${taskBasename}]] was abandoned ${attemptCount} time(s). The last reclaim reason was \`${reclaimReason}\`.
120
+
121
+ ## Reproduction
122
+
123
+ 1. Open [[${taskBasename}]] and read the handoff envelope below.
124
+ 2. Compare \`summary_done\` / git diff against what remains in \`remaining\`.
125
+ 3. Decide whether to re-open the task, split again, or close as wont-fix.
126
+
127
+ ${handoffSection || '_(No handoff envelope block found on the deferred task.)_'}
128
+
129
+ ${buildDiagnoseSkeletonSections({
130
+ 'Root cause': '_DLQ hint: likely repeated agent timeout / context loss on deferred pickup._',
131
+ Fix: '_DLQ hint: resume from verified partial state, or re-defer with a fresh handoff_id._',
132
+ })}
133
+
134
+ ## Change log
135
+
136
+ | Date | Author | Change |
137
+ |---|---|---|
138
+ | ${today} | deferred-reclaim | Auto-filed from [[${taskBasename}]] after attempt_count ≥ cap. |
139
+ `;
140
+
141
+ // Era-3 file-key-first basename: `HS-<fileKey>-<familyKey>-DIAG-<slug>`.
142
+ return { content: fm + body, basename: `${diagnoseId}-${familyKey}-DIAG-${slugPart}` };
143
+ }
144
+
145
+ function bumpAttemptCountInContent(raw, { now, author, reclaimReason }) {
146
+ const fm = getFrontmatterBlock(raw);
147
+ if (!fm) return { changed: false, content: raw, attemptCount: 0 };
148
+ const prev = readAttemptCount(fm.block);
149
+ const next = prev + 1;
150
+ let block = setScalarField(fm.block, 'attempt_count', String(next));
151
+ block = bumpUpdated(block, now);
152
+ let body = appendChangeLogRow(
153
+ fm.body,
154
+ now.slice(0, 10),
155
+ author,
156
+ `Reclaim attempt ${next} (${reclaimReason})`,
157
+ );
158
+ return {
159
+ changed: true,
160
+ content: `${fm.open}${block}${fm.close}${body}`,
161
+ attemptCount: next,
162
+ };
163
+ }
164
+
165
+ function cancelDeferredTaskContent(raw, diagnoseBasename, now, author) {
166
+ const fm = getFrontmatterBlock(raw);
167
+ if (!fm) return { changed: false, content: raw };
168
+ let block = setScalarField(fm.block, 'status', 'cancelled');
169
+ block = setScalarField(block, 'close_reason', 'duplicate');
170
+ block = bumpUpdated(block, now);
171
+ let body = appendChangeLogRow(
172
+ fm.body,
173
+ now.slice(0, 10),
174
+ author,
175
+ `Routed to Resolution DLQ → [[${diagnoseBasename}]]`,
176
+ );
177
+ return { changed: true, content: `${fm.open}${block}${fm.close}${body}` };
178
+ }
179
+
180
+ /**
181
+ * After a lease/heartbeat reclaim on a deferred split child: bump attempt_count;
182
+ * past cap → file HS-DIAG + cancel the deferred task.
183
+ *
184
+ * @param {string} vaultRoot
185
+ * @param {{ absPath: string, relPath: string, basename: string, raw: string }} task
186
+ * @param {{ reclaimReason: string, author?: string, attemptCap?: number, randomUnit?: () => number }} opts
187
+ */
188
+ export function executeDeferredSplitReclaim(vaultRoot, task, opts = {}) {
189
+ if (!isDeferredSplitTaskRaw(task.raw)) {
190
+ return { ok: true, skipped: true, reason: 'not_deferred_split' };
191
+ }
192
+
193
+ const now = isoNow();
194
+ const author = opts.author ?? 'deferred-reclaim';
195
+ const reclaimReason = opts.reclaimReason ?? 'lease_expired';
196
+ const attemptCap = opts.attemptCap ?? DEFAULT_DEFER_RECLAIM_ATTEMPT_CAP;
197
+
198
+ const bumped = bumpAttemptCountInContent(task.raw, { now, author, reclaimReason });
199
+ if (!bumped.changed) {
200
+ return { ok: false, error: 'INVALID_TASK', message: 'deferred task has no frontmatter' };
201
+ }
202
+
203
+ const writes = [{ kind: 'update', absPath: task.absPath, content: bumped.content }];
204
+
205
+ if (bumped.attemptCount < attemptCap) {
206
+ const commitResult = commitWrites(writes);
207
+ if (!commitResult.ok) {
208
+ return {
209
+ ok: false,
210
+ error: 'WRITE_FAILED',
211
+ message: commitResult.error?.message ?? 'unknown write failure',
212
+ };
213
+ }
214
+ return {
215
+ ok: true,
216
+ action: 'reopened',
217
+ attempt_count: bumped.attemptCount,
218
+ attempt_cap: attemptCap,
219
+ };
220
+ }
221
+
222
+ const fm = getFrontmatterBlock(task.raw);
223
+ const taskName = getScalarField(fm?.block ?? '', 'name') ?? task.basename;
224
+ const handoffSection = handoffEnvelopeSectionFromBody(fm?.body ?? '');
225
+
226
+ const occupied = collectOccupiedDiagnoseIds(vaultRoot);
227
+ // Era-3 file-key-first ([[naming-v3-7-rulings-hs-namespace-kept]]): the
228
+ // FAMILY token still comes from the legacy allocator's `HS-<family>-DIAG`
229
+ // draw; `id:` keys onto the file key alone.
230
+ const allocatedFamilyId = allocateRandomVaultId('diagnose', occupied, { randomUnit: opts.randomUnit });
231
+ const familyKey =
232
+ parseSpecId(allocatedFamilyId)?.familyKey ?? mintFamilyKey(vaultRoot, { randomUnit: opts.randomUnit });
233
+ // Per-file FILE KEY (owner ruling 2026-07-10) — filename + `file_id:` SSOT,
234
+ // and (V3) the anchor for `id:` itself.
235
+ const fileKey = mintFileKey(vaultRoot, { randomUnit: opts.randomUnit });
236
+ const diagnoseId = `HS-${fileKey}`;
237
+ const built = buildDlqDiagnoseContent({
238
+ diagnoseId,
239
+ fileKey,
240
+ familyKey,
241
+ taskBasename: task.basename,
242
+ taskName: String(taskName).replace(/^['"]|['"]$/g, ''),
243
+ attemptCount: bumped.attemptCount,
244
+ reclaimReason,
245
+ handoffSection,
246
+ now,
247
+ });
248
+
249
+ const diagnoseDir = join(vaultRoot, 'specs', 'diagnoses');
250
+ if (!existsSync(diagnoseDir)) mkdirSync(diagnoseDir, { recursive: true });
251
+ const diagnoseAbsPath = join(diagnoseDir, `${built.basename}.md`);
252
+ writes.push({ kind: 'create', absPath: diagnoseAbsPath, content: built.content });
253
+
254
+ const cancelled = cancelDeferredTaskContent(bumped.content, built.basename, now, author);
255
+ if (cancelled.changed) {
256
+ writes[0] = { kind: 'update', absPath: task.absPath, content: cancelled.content };
257
+ }
258
+
259
+ const commitResult = commitWrites(writes);
260
+ if (!commitResult.ok) {
261
+ return {
262
+ ok: false,
263
+ error: 'WRITE_FAILED',
264
+ message: commitResult.error?.message ?? 'unknown write failure',
265
+ };
266
+ }
267
+
268
+ return {
269
+ ok: true,
270
+ action: 'dlq',
271
+ attempt_count: bumped.attemptCount,
272
+ attempt_cap: attemptCap,
273
+ diagnose_id: diagnoseId,
274
+ diagnose_basename: built.basename,
275
+ diagnose_path: join('specs', 'diagnoses', `${built.basename}.md`),
276
+ };
277
+ }
278
+
279
+ export const __testables = {
280
+ readAttemptCount,
281
+ isDeferredSplitTaskRaw,
282
+ bumpAttemptCountInContent,
283
+ buildDlqDiagnoseContent,
284
+ };