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,1145 @@
1
+ import { createRequire as __hsCreateRequire } from 'node:module';
2
+ const require = __hsCreateRequire(import.meta.url);
3
+
4
+ // spex-console/bin/hs-init.ts
5
+ import {
6
+ copyFileSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync as readFileSync4,
11
+ writeFileSync
12
+ } from "node:fs";
13
+ import { dirname, join as join3, resolve } from "node:path";
14
+ import process2 from "node:process";
15
+ import { createInterface } from "node:readline/promises";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ // spex-console/src/lib/cli/customer-claude-md.ts
19
+ function renderCustomerClaudeMd({
20
+ vaultName,
21
+ templateVersion
22
+ }) {
23
+ return `# Agent Instructions \u2014 ${vaultName}
24
+
25
+ This project's work is tracked in a git-tracked markdown vault under \`specs/\` \u2014 the files *are* the system of record (no separate database or issue tracker). The **HeySpex MCP server** is the source of truth for how to work here: procedures, project memory, and design patterns load **on demand** through its tools, so this file stays small and rarely needs editing.
26
+
27
+ ## Session start
28
+
29
+ Recall project memory first: \`heyspex_recall_memories({ limit: 10 })\` \u2014 surfaces lessons, decisions, and conventions specific to this vault before you touch anything.
30
+
31
+ ## Procedure discovery \u2014 before any high-stakes action
32
+
33
+ Before filing work, a destructive action, closing a session, or claiming a spec, consult the procedures via MCP (never guess from an example file):
34
+
35
+ - \`heyspex_preflight({ intent })\` \u2014 call first; returns which procedures to consult. Intents include \`file_task\`, \`file_epic\`, \`destructive_action\`, \`close_session\`, \`claim_spec\`.
36
+ - \`heyspex_get_procedure({ name })\` \u2014 fetch one full procedure body.
37
+ - \`heyspex_list_procedures({ query? })\` \u2014 browse by tag or keyword.
38
+
39
+ ## The claim handshake
40
+
41
+ 1. \`/hs-next\` returns the on-deck task as a \`prompt_markdown\`.
42
+ 2. Confirm with the user, then call \`heyspex_claim_spec({ spec_id })\` \u2014 never start work unclaimed.
43
+ 3. After each acceptance criterion, call \`heyspex_report_progress({ acProgress, currentStage })\` so progress shows live.
44
+ 4. When every criterion is met, call \`heyspex_submit_for_review({ spec_id })\`. **Never hand-edit \`status:\` in frontmatter** \u2014 the status API flips it and rolls parents up.
45
+ 5. Release the claim (\`heyspex_release_spec\`) on hand-off. The agent does not commit or push \u2014 a dirty working tree is the expected hand-off shape; a human reviews and commits.
46
+
47
+ ## Filing new work
48
+
49
+ The vault under \`specs/\` is the only persistent queue. File new work via \`heyspex_file_spec\` (or \`heyspex_push_plan\` for a batch of epics/tasks) \u2014 **never set an \`id\` by hand**, the server mints it. Cross-link specs with \`[[wikilinks]]\`; every link must resolve to a real file under \`specs/\`.
50
+
51
+ ## Capture knowledge as you go
52
+
53
+ - Use \`/hs-capture\` to fast-offload an idea before it slips; it promotes to research when ready.
54
+ - After any research or web search, save it via \`heyspex_save_research\` (Executive Summary + at least one cited source).
55
+ - Notable decisions and lessons become memory specs under \`specs/memory/{lessons,decisions,references}\` \u2014 link them to the specs they bear on.
56
+
57
+ ## Conventions
58
+
59
+ - New entities are always filed via \`heyspex_file_spec\` \u2014 never hand-write frontmatter \`id:\`.
60
+ - The vault is the only durable queue \u2014 track real work as spec files, not ad-hoc TODOs.
61
+ - Agents do not commit or push. A dirty tree at session end is normal hand-off, not an error.
62
+
63
+ ## This file vs. your vault
64
+
65
+ This CLAUDE.md was generated for **${vaultName}** from HeySpex template version **${templateVersion}** \u2014 it is yours to edit. Unlike HeySpex's own platform contract (which carries HeySpex's internal build tooling), everything above applies to *your* vault and *your* agents. Edit it freely as your project's conventions evolve; a later template update will not overwrite your changes.
66
+ `;
67
+ }
68
+ function customerClaudeMdHook() {
69
+ return (ctx) => {
70
+ const content = renderCustomerClaudeMd({
71
+ vaultName: ctx.vaultName,
72
+ templateVersion: ctx.templateVersion
73
+ });
74
+ const destPath = ctx.deps.joinPath(ctx.targetDir, "CLAUDE.md");
75
+ ctx.deps.writeFile(destPath, content);
76
+ ctx.recordWrite(destPath);
77
+ };
78
+ }
79
+
80
+ // spex-console/src/lib/cli/init.ts
81
+ import { randomUUID } from "node:crypto";
82
+
83
+ // scripts/lib/console-url-resolver.mjs
84
+ import { readFileSync } from "node:fs";
85
+ import { homedir } from "node:os";
86
+ import { join } from "node:path";
87
+ var DEFAULT_CONSOLE_URL = "https://app.heyspex.com";
88
+ function trimTrailingSlash(url) {
89
+ return url.replace(/\/+$/, "");
90
+ }
91
+ function readCredentialsConsoleUrl({ homeDir = homedir() } = {}) {
92
+ try {
93
+ const raw = readFileSync(join(homeDir, ".heyspex", "credentials.yaml"), "utf8");
94
+ const match = raw.match(/^console_url:\s*["']?([^"'\r\n#]+)/m);
95
+ const value = match?.[1]?.trim();
96
+ return value && value.length > 0 ? trimTrailingSlash(value) : null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ function readDiscoveryFileConsoleUrl(repoRoot) {
102
+ if (!repoRoot) return null;
103
+ for (const name of [
104
+ "console-url",
105
+ "cockpit-url"
106
+ /* legacy-cockpit */
107
+ ]) {
108
+ try {
109
+ const discovery = readFileSync(join(repoRoot, ".heyspex", name), "utf8").trim();
110
+ if (discovery.length > 0) return trimTrailingSlash(discovery);
111
+ } catch {
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+ var ConsoleUrlUnresolvedError = class extends Error {
117
+ constructor() {
118
+ super(
119
+ `resolveConsoleUrl: no HEYSPEX_CONSOLE_URL/HEYSPEX_COCKPIT_URL env, no console_url: in ~/.heyspex/credentials.yaml, and no .heyspex/console-url discovery file \u2014 refusing to silently fall back to the ${DEFAULT_CONSOLE_URL} default in strict mode.`
120
+ );
121
+ this.name = "ConsoleUrlUnresolvedError";
122
+ }
123
+ };
124
+ function resolveConsoleUrl({
125
+ repoRoot,
126
+ env = process.env,
127
+ homeDir = homedir(),
128
+ strict = false
129
+ } = {}) {
130
+ const envUrl = (env.HEYSPEX_CONSOLE_URL ?? env.HEYSPEX_COCKPIT_URL)?.trim();
131
+ if (envUrl) return trimTrailingSlash(envUrl);
132
+ const credentialsUrl = readCredentialsConsoleUrl({ homeDir });
133
+ if (credentialsUrl) return credentialsUrl;
134
+ const discoveryUrl = readDiscoveryFileConsoleUrl(repoRoot);
135
+ if (discoveryUrl) return discoveryUrl;
136
+ if (strict) throw new ConsoleUrlUnresolvedError();
137
+ return DEFAULT_CONSOLE_URL;
138
+ }
139
+
140
+ // spex-console/src/lib/cli/control-shared.ts
141
+ import { readFileSync as readFileSync2 } from "node:fs";
142
+ import { homedir as homedir2 } from "node:os";
143
+ import { join as join2 } from "node:path";
144
+ var EXIT = {
145
+ /** Success. */
146
+ OK: 0,
147
+ /** Usage error — unknown subcommand/flag, or a missing required argument. */
148
+ USAGE: 1,
149
+ /** Claim race lost — the spec is already claimed by another agent (409). */
150
+ CONFLICT: 9,
151
+ /** Target not found — release of an unheld claim, etc. (404). */
152
+ NOT_FOUND: 10,
153
+ /** Invalid argument — bad `--ttl`, unknown `--kind`, unparseable date. */
154
+ INVALID_ARG: 11,
155
+ /** SpexConsole unreachable (network error reaching the dev server). */
156
+ CONSOLE_UNREACHABLE: 12,
157
+ /** Server failure — a non-OK response that isn't 400/402/404/409. */
158
+ WRITE_FAILED: 13,
159
+ /** Neon control plane absent — the endpoint returned 402 (upgrade required). */
160
+ NEON_REQUIRED: 14,
161
+ /**
162
+ * A cross-account `--account <id>` override was refused — the caller is
163
+ * not a builder/admin (`resolveScopedAccountId`'s `forbidden_account_override`,
164
+ * [[HS-X3GB-EKLJ-TASK-hs-cli-account-flag-for-cross-account]]). Distinct
165
+ * from `USAGE`/`INVALID_ARG` — this is an authorization refusal, not a
166
+ * malformed command.
167
+ */
168
+ FORBIDDEN: 15
169
+ };
170
+
171
+ // spex-console/src/lib/cli/init.ts
172
+ var HEYSPEX_YAML_SCHEMA_VERSION = 2;
173
+ var DEFAULT_CONSOLE_URL2 = resolveConsoleUrl();
174
+ function mintVaultId() {
175
+ return randomUUID();
176
+ }
177
+ function parseVaultId(content) {
178
+ const m = content.match(/^vault_id:\s*(.+?)\s*$/m);
179
+ if (!m) return null;
180
+ const raw = m[1].trim();
181
+ const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
182
+ return unquoted.length > 0 ? unquoted : null;
183
+ }
184
+ function appendVaultIdLine(content, vaultId) {
185
+ const trimmed = content.replace(/\n+$/, "");
186
+ return `${trimmed}
187
+ vault_id: ${vaultId}
188
+ `;
189
+ }
190
+ function parseInitArgs(argv) {
191
+ let consoleUrl = (process.env.HEYSPEX_CONSOLE_URL ?? process.env.HEYSPEX_COCKPIT_URL)?.trim() || // legacy-cockpit env
192
+ DEFAULT_CONSOLE_URL2;
193
+ let vaultRoot = "specs/";
194
+ let force = false;
195
+ let json = false;
196
+ let customerVaultDir;
197
+ let principles;
198
+ let vaultName;
199
+ let noGit = false;
200
+ for (let i = 0; i < argv.length; i++) {
201
+ const arg = argv[i];
202
+ if (arg === "--help" || arg === "-h") return { help: true };
203
+ else if (arg === "--json") json = true;
204
+ else if (arg === "--force") force = true;
205
+ else if (arg === "--no-git") noGit = true;
206
+ else if (arg === "--principles" || arg.startsWith("--principles=")) {
207
+ const v = arg === "--principles" ? argv[++i] : arg.slice("--principles=".length);
208
+ if (v === void 0 || v.trim() === "") {
209
+ return { error: { code: EXIT.INVALID_ARG, message: "--principles requires a value" } };
210
+ }
211
+ principles = v.trim();
212
+ } else if (arg === "--vault-name" || arg.startsWith("--vault-name=")) {
213
+ const v = arg === "--vault-name" ? argv[++i] : arg.slice("--vault-name=".length);
214
+ if (v === void 0 || v.trim() === "") {
215
+ return { error: { code: EXIT.INVALID_ARG, message: "--vault-name requires a value" } };
216
+ }
217
+ vaultName = v.trim();
218
+ } else if (arg === "--console-url" || arg.startsWith("--console-url=") || arg === "--cockpit-url" || // legacy-cockpit alias
219
+ arg.startsWith("--cockpit-url=")) {
220
+ const eq = arg.indexOf("=");
221
+ const v = eq === -1 ? argv[++i] : arg.slice(eq + 1);
222
+ if (v === void 0 || v.trim() === "") {
223
+ return {
224
+ error: {
225
+ code: EXIT.INVALID_ARG,
226
+ message: `${eq === -1 ? arg : arg.slice(0, eq)} requires a value`
227
+ }
228
+ };
229
+ }
230
+ consoleUrl = v.trim();
231
+ } else if (arg === "--vault-root" || arg.startsWith("--vault-root=")) {
232
+ const v = arg === "--vault-root" ? argv[++i] : arg.slice("--vault-root=".length);
233
+ if (v === void 0 || v.trim() === "") {
234
+ return { error: { code: EXIT.INVALID_ARG, message: "--vault-root requires a value" } };
235
+ }
236
+ vaultRoot = v.trim();
237
+ } else if (arg === "--customer-vault" || arg.startsWith("--customer-vault=")) {
238
+ const v = arg === "--customer-vault" ? argv[++i] : arg.slice("--customer-vault=".length);
239
+ if (v === void 0 || v.trim() === "") {
240
+ return { error: { code: EXIT.INVALID_ARG, message: "--customer-vault requires a value" } };
241
+ }
242
+ customerVaultDir = v.trim();
243
+ } else if (arg.startsWith("-")) {
244
+ return { error: { code: EXIT.USAGE, message: `unknown flag: ${arg}` } };
245
+ } else {
246
+ return { error: { code: EXIT.USAGE, message: `unexpected argument: ${arg}` } };
247
+ }
248
+ }
249
+ return {
250
+ args: { consoleUrl, vaultRoot, force, json, customerVaultDir, principles, vaultName, noGit }
251
+ };
252
+ }
253
+ function renderHeyspexYaml(args) {
254
+ return [
255
+ `schema_version: ${HEYSPEX_YAML_SCHEMA_VERSION}`,
256
+ `console_url: ${args.consoleUrl}`,
257
+ `vault_root: ${args.vaultRoot}`,
258
+ `vault_id: ${args.vaultId}`,
259
+ ""
260
+ ].join("\n");
261
+ }
262
+ function readExistingContent(deps, markerPath) {
263
+ if (!deps.readFile) return null;
264
+ try {
265
+ return deps.readFile(markerPath);
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+ function runInit(args, deps) {
271
+ const markerPath = deps.joinPath(deps.repoRoot, "heyspex.yaml");
272
+ const exists = deps.fileExists(markerPath);
273
+ const existingContent = exists ? readExistingContent(deps, markerPath) : null;
274
+ const existingVaultId = existingContent ? parseVaultId(existingContent) : null;
275
+ if (exists && !args.force) {
276
+ if (existingVaultId) {
277
+ return {
278
+ code: EXIT.OK,
279
+ message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).`,
280
+ payload: {
281
+ ok: true,
282
+ created: false,
283
+ path: markerPath,
284
+ reason: "already_initialized",
285
+ vault_id: existingVaultId
286
+ },
287
+ isError: false
288
+ };
289
+ }
290
+ if (existingContent === null) {
291
+ return {
292
+ code: EXIT.OK,
293
+ message: `heyspex.yaml already exists at ${markerPath} \u2014 vault already initialized (use --force to overwrite).`,
294
+ payload: { ok: true, created: false, path: markerPath, reason: "already_initialized" },
295
+ isError: false
296
+ };
297
+ }
298
+ const mintedId = args.adoptedVaultId ?? mintVaultId();
299
+ const updatedContent = appendVaultIdLine(existingContent, mintedId);
300
+ try {
301
+ deps.writeFile(markerPath, updatedContent);
302
+ } catch (err) {
303
+ const message = err instanceof Error ? err.message : String(err);
304
+ return {
305
+ code: EXIT.WRITE_FAILED,
306
+ message: `failed to write heyspex.yaml: ${message}`,
307
+ payload: { ok: false, error: "write_failed", path: markerPath, message },
308
+ isError: true
309
+ };
310
+ }
311
+ return {
312
+ code: EXIT.OK,
313
+ message: `Added vault_id to existing heyspex.yaml at ${markerPath} (schema_version unchanged).`,
314
+ payload: {
315
+ ok: true,
316
+ created: false,
317
+ backfilled: true,
318
+ path: markerPath,
319
+ vault_id: mintedId
320
+ },
321
+ isError: false
322
+ };
323
+ }
324
+ const vaultId = existingVaultId ?? args.adoptedVaultId ?? mintVaultId();
325
+ const content = renderHeyspexYaml({ ...args, vaultId });
326
+ try {
327
+ deps.writeFile(markerPath, content);
328
+ } catch (err) {
329
+ const message = err instanceof Error ? err.message : String(err);
330
+ return {
331
+ code: EXIT.WRITE_FAILED,
332
+ message: `failed to write heyspex.yaml: ${message}`,
333
+ payload: { ok: false, error: "write_failed", path: markerPath, message },
334
+ isError: true
335
+ };
336
+ }
337
+ return {
338
+ code: EXIT.OK,
339
+ message: `${exists ? "Overwrote" : "Wrote"} ${markerPath} (schema_version ${HEYSPEX_YAML_SCHEMA_VERSION}).`,
340
+ payload: {
341
+ ok: true,
342
+ created: true,
343
+ overwrote: exists,
344
+ path: markerPath,
345
+ schema_version: HEYSPEX_YAML_SCHEMA_VERSION,
346
+ console_url: args.consoleUrl,
347
+ vault_root: args.vaultRoot,
348
+ vault_id: vaultId
349
+ },
350
+ isError: false
351
+ };
352
+ }
353
+ var HELP = [
354
+ "Usage:",
355
+ " hs init [--console-url <url>] [--vault-root <path>] [--force] [--json]",
356
+ " hs init --customer-vault <dir> [--console-url <url>] [--principles <all|none|a,b>]",
357
+ " [--vault-name <name>] [--no-git] [--json]",
358
+ "",
359
+ "Write the top-level heyspex.yaml vault marker (schema_version, console_url,",
360
+ "vault_root, vault_id) \u2014 the committed file the console / web companion read",
361
+ "to bootstrap a vault. Idempotent: an existing marker is left untouched unless",
362
+ "--force, EXCEPT a missing vault_id is backfilled in place either way (backward",
363
+ "compatible \u2014 no schema_version bump). vault_id is reused exactly when already",
364
+ "present; a fresh one is minted only when absent.",
365
+ "Does not modify .heyspex/config.yaml or its templates (ADR-0009).",
366
+ "",
367
+ "--customer-vault <dir> scaffolds a REAL customer vault at <dir> instead: the",
368
+ "specs/ folder tree, heyspex.yaml, and every row of the template shipping",
369
+ "manifest (spex-console/src/lib/cli/template-manifest.ts) \u2014 universal docs",
370
+ "copied verbatim, template-shape docs written as author-prompt skeletons \u2014",
371
+ "plus a generated CLAUDE.md, the kept-principles index, and (unless --no-git)",
372
+ "one initial `git init` + commit pinning the template version. Refuses a",
373
+ "non-empty <dir> with a plain error. See init.customer-vault.ts.",
374
+ "",
375
+ "Flags:",
376
+ " --console-url <url> console_url value (default: $HEYSPEX_CONSOLE_URL or",
377
+ " https://console.heyspex.local).",
378
+ " --vault-root <path> vault_root value (default: specs/).",
379
+ " --customer-vault <dir> Scaffold a new customer vault at <dir> (see above).",
380
+ " --principles <all|none|a,b> Which universal principle docs to ship",
381
+ " (--customer-vault only). Omitted + a TTY: interactive",
382
+ " keep/skip prompt per principle. Omitted + no TTY: all.",
383
+ " --vault-name <name> Display name for the scaffolded vault (--customer-vault",
384
+ " only; default: <dir>'s basename).",
385
+ " --no-git Skip git init + the initial commit (--customer-vault only).",
386
+ " --force Overwrite an existing heyspex.yaml.",
387
+ " --json Emit a machine-readable JSON result.",
388
+ " --help, -h Show this help.",
389
+ "",
390
+ "Exit codes: 0 ok \xB7 1 usage \xB7 11 bad-arg \xB7 13 write-failed"
391
+ ].join("\n");
392
+
393
+ // spex-console/src/lib/cli/init.customer-vault.ts
394
+ import { basename } from "node:path";
395
+
396
+ // spex-console/src/lib/cli/template-manifest.sources.mjs
397
+ var UNIVERSAL_TEMPLATE_ROWS = [
398
+ { platformSource: "specs/entity-model.md", dest: "specs/entity-model.md" },
399
+ {
400
+ platformSource: "specs/protocols/canonical-frontmatter-templates.md",
401
+ dest: "specs/protocols/canonical-frontmatter-templates.md"
402
+ },
403
+ {
404
+ platformSource: "specs/protocols/spec-hierarchy.md",
405
+ dest: "specs/protocols/spec-hierarchy.md"
406
+ },
407
+ {
408
+ platformSource: "specs/protocols/memory-protocol.md",
409
+ dest: "specs/protocols/memory-protocol.md"
410
+ }
411
+ ];
412
+
413
+ // dist/cli/._template-manifest-sources.cli-shim.mjs
414
+ var CURATED_TEMPLATE_ROOT = "dist/cli-templates/customer-vault";
415
+
416
+ // spex-console/src/lib/cli/template-manifest.ts
417
+ var TEMPLATE_VERSION = "1.0.0";
418
+ var TODO_NORTH_STAR = `---
419
+ id: HS-L1
420
+ name: TODO \u2014 your product name
421
+ type: l1
422
+ status: open
423
+ priority: 0
424
+ owner: TODO \u2014 your name/handle
425
+ created: TODO \u2014 YYYY-MM-DD
426
+ updated: TODO \u2014 YYYY-MM-DD
427
+ tags:
428
+ - north-star
429
+ ---
430
+
431
+ # TODO \u2014 your product name \u2014 North Star
432
+
433
+ <!-- TODO: replace this whole file. This is a skeleton, not HeySpex's own vision doc \u2014 write
434
+ YOUR product's constitutional vision here: the problem, the customer, and the small number
435
+ of non-negotiable principles that every future architecture / entity-model / roadmap
436
+ decision must trace back to. -->
437
+
438
+ ## Problem
439
+
440
+ TODO: what problem does this product solve, and for whom?
441
+
442
+ ## Vision
443
+
444
+ TODO: what does "done" look like at the highest altitude?
445
+
446
+ ## Principles
447
+
448
+ TODO: the small number of non-negotiable rules every feature decision must satisfy. See
449
+ \`specs/principles-index.md\` for the index pattern this vault ships with.
450
+ `;
451
+ var TODO_PRINCIPLES_INDEX = `---
452
+ id: HS-L2-PRINCIPLES-INDEX
453
+ name: Design Principles Index
454
+ type: l2
455
+ status: open
456
+ priority: 0
457
+ owner: TODO \u2014 your name/handle
458
+ created: TODO \u2014 YYYY-MM-DD
459
+ updated: TODO \u2014 YYYY-MM-DD
460
+ tags:
461
+ - l2
462
+ - principles
463
+ - index
464
+ ---
465
+
466
+ # Design Principles Index
467
+
468
+ <!-- TODO: this is a skeleton index, not content. Populate the table below with YOUR product's
469
+ own numbered design principles (P1, P2, ...) \u2014 each principle should be its own
470
+ \`memory-decision\` spec under specs/memory/decisions/, linked from this table. A few
471
+ HeySpex-originated architecture commitments, generalized, are pre-seeded there as a
472
+ starting point \u2014 keep, edit, or delete each one. -->
473
+
474
+ One-page reference for the design principles that govern every feature decision in this vault.
475
+
476
+ ## The principles at a glance
477
+
478
+ | # | One-line rule | When to cite | Refusal trigger | Memory |
479
+ | --- | --- | --- | --- | --- |
480
+ | P1 | TODO | TODO | TODO | TODO: link a real \`[[memory-decision]]\` spec here |
481
+
482
+ ## How to use this index
483
+
484
+ TODO: describe how code review / feature design / roadmap decisions consult this index.
485
+ `;
486
+ var TODO_STARTER_TASK = `---
487
+ id: HS-START-0001
488
+ name: Your first task
489
+ summary: TODO \u2014 one-line summary of this task.
490
+ type: task
491
+ status: open
492
+ priority: 2
493
+ kind: feature
494
+ effort: 1
495
+ owner: TODO \u2014 your name/handle
496
+ created: TODO \u2014 YYYY-MM-DD
497
+ updated: TODO \u2014 YYYY-MM-DD
498
+ parent: '[[HS-START-0000-EPIC-your-first-epic]]'
499
+ tags:
500
+ - near-term
501
+ ---
502
+
503
+ # Your first task
504
+
505
+ <!-- TODO: replace this with a real first task for your vault. This file exists so there is
506
+ something claimable the moment your vault is initialized \u2014 see
507
+ specs/protocols/canonical-frontmatter-templates.md for the full task template. Its
508
+ parent: field points at the starter epic (HS-START-0000) shipped alongside it, so the
509
+ vault lints clean (a task with no epic ancestor is an orphan) \u2014 see that epic's own file
510
+ for what it in turn required (a starter milestone + roadmap). -->
511
+
512
+ ## Intended outcome
513
+
514
+ TODO: what observable condition does this task produce when done?
515
+
516
+ ## Scope
517
+
518
+ TODO: what's in scope, what's explicitly out.
519
+
520
+ ## Acceptance criteria
521
+
522
+ 1. TODO: first checkable condition.
523
+ 2. TODO: second checkable condition.
524
+ `;
525
+ var TODO_STARTER_EPIC = `---
526
+ id: HS-START-0000
527
+ name: Your first epic
528
+ summary: TODO \u2014 one-line summary of this epic.
529
+ type: epic
530
+ status: open
531
+ kind: feature
532
+ effort: 3
533
+ owner: TODO \u2014 your name/handle
534
+ created: TODO \u2014 YYYY-MM-DD
535
+ updated: TODO \u2014 YYYY-MM-DD
536
+ milestone: '[[HS-START-0002-MIL-your-first-milestone]]'
537
+ children:
538
+ - '[[HS-START-0001-TASK-your-first-task]]'
539
+ tags: []
540
+ ---
541
+
542
+ # Your first epic
543
+
544
+ <!-- TODO: replace this with a real first epic for your vault. This file (plus its starter
545
+ milestone and roadmap, shipped alongside it) exists so the starter task has a home the
546
+ moment your vault is initialized \u2014 see specs/protocols/canonical-frontmatter-templates.md
547
+ for the full epic template. -->
548
+
549
+ ## Intended outcome
550
+
551
+ **User-visible outcome of this epic (required):** TODO \u2014 what changes for the user when this
552
+ epic ships?
553
+
554
+ **Moves (\u2191 link, required):** TODO \u2014 which milestone outcome does it move, and how?
555
+
556
+ ## Why this epic exists
557
+
558
+ **Problem right now (required):** TODO \u2014 one sentence: what specific problem does this solve,
559
+ and why is now the right time?
560
+
561
+ ## Tasks
562
+
563
+ - [[HS-START-0001-TASK-your-first-task]]
564
+
565
+ ## Acceptance gate \u2014 epic close
566
+
567
+ TODO: what must be true of every child task for this epic to be done?
568
+
569
+ ## Change log
570
+ `;
571
+ var TODO_STARTER_MILESTONE = `---
572
+ id: HS-START-0002
573
+ name: Your first milestone
574
+ summary: TODO \u2014 one-line summary of this milestone.
575
+ type: milestone
576
+ status: open
577
+ owner: TODO \u2014 your name/handle
578
+ created: TODO \u2014 YYYY-MM-DD
579
+ updated: TODO \u2014 YYYY-MM-DD
580
+ epics:
581
+ - '[[HS-START-0000-EPIC-your-first-epic]]'
582
+ target_date: ''
583
+ tags:
584
+ - milestone
585
+ ---
586
+
587
+ # Your first milestone
588
+
589
+ <!-- TODO: replace this with a real first milestone for your vault. It exists so the starter
590
+ epic has a home \u2014 see specs/protocols/canonical-frontmatter-templates.md for the full
591
+ milestone template. -->
592
+
593
+ ## Intended outcome
594
+
595
+ **Outcome at the milestone altitude (required):** TODO \u2014 what outcome does this milestone
596
+ deliver?
597
+
598
+ ## Why this is a milestone, not loose epics
599
+
600
+ TODO.
601
+
602
+ ## Suggested landing order
603
+
604
+ - [[HS-START-0000-EPIC-your-first-epic]]
605
+
606
+ ## Out of scope
607
+
608
+ TODO.
609
+ `;
610
+ var TODO_STARTER_ROADMAP = `---
611
+ id: HS-START-ROADMAP
612
+ name: Your roadmap
613
+ type: roadmap
614
+ status: open
615
+ owner: TODO \u2014 your name/handle
616
+ created: TODO \u2014 YYYY-MM-DD
617
+ updated: TODO \u2014 YYYY-MM-DD
618
+ roadmap_manifest:
619
+ version: 1
620
+ milestones:
621
+ - HS-START-0002-MIL-your-first-milestone
622
+ tags:
623
+ - roadmap
624
+ ---
625
+
626
+ # Your roadmap
627
+
628
+ <!-- TODO: this is a skeleton roadmap, not content. \`roadmap_manifest.milestones\` is what makes
629
+ a milestone reachable from the roadmap (specs/entity-model.md \xA7roadmap) \u2014 list every
630
+ milestone you file here, in the order you intend to land them. -->
631
+
632
+ ## Now
633
+
634
+ - [[HS-START-0000-EPIC-your-first-epic]] \u2014 TODO: one-line status.
635
+
636
+ ## Next
637
+
638
+ TODO.
639
+
640
+ ## Later
641
+
642
+ TODO.
643
+ `;
644
+ function principleSkeleton(args) {
645
+ return `---
646
+ id: HS-DEC-${args.id}
647
+ name: "${args.name}"
648
+ type: memory-decision
649
+ status: open
650
+ priority: 0
651
+ owner: TODO \u2014 your name/handle
652
+ created: TODO \u2014 YYYY-MM-DD
653
+ updated: TODO \u2014 YYYY-MM-DD
654
+ topic: ${args.topic}
655
+ decided: TODO \u2014 YYYY-MM-DD
656
+ deciders:
657
+ - TODO \u2014 your name/handle
658
+ tags:
659
+ - decision
660
+ - principle
661
+ - architecture
662
+ ---
663
+
664
+ # ${args.name}
665
+
666
+ <!-- TODO: this is one of HeySpex's own architecture commitments, generalized as a starting
667
+ point. Confirm it fits your product as-is, edit it, or delete this file and drop it from
668
+ specs/principles-index.md. -->
669
+
670
+ ## Decision
671
+
672
+ ${args.decision}
673
+
674
+ ## Rationale
675
+
676
+ ${args.rationalePrompt}
677
+ `;
678
+ }
679
+ var TEMPLATE_MANIFEST = [
680
+ // --- Universal rows: copied byte-identical — but from the CURATED copy
681
+ // (`${CURATED_TEMPLATE_ROOT}/${dest}`), not the raw platform doc. The raw
682
+ // platform doc carries ~449 wikilinks to platform-only specs that would
683
+ // never resolve in a customer vault (HS-I2U9); `scripts/regenerate-
684
+ // customer-templates.mjs` rewrites those into plain text once, ahead of
685
+ // time, from this SAME `UNIVERSAL_TEMPLATE_ROWS` list (one list, shared —
686
+ // see that module's + `template-manifest.sources.mjs`'s doc comments).
687
+ ...UNIVERSAL_TEMPLATE_ROWS.map(
688
+ (row) => ({
689
+ source: `${CURATED_TEMPLATE_ROOT}/${row.dest}`,
690
+ classification: "universal-template",
691
+ dest: row.dest
692
+ })
693
+ ),
694
+ // --- Shape rows: inline skeleton content with a TODO author prompt. ---
695
+ {
696
+ source: "specs/heyspex-north-star.md",
697
+ classification: "template-shape",
698
+ dest: "specs/north-star.md",
699
+ content: TODO_NORTH_STAR
700
+ },
701
+ {
702
+ source: "specs/principles-index.md",
703
+ classification: "template-shape",
704
+ dest: "specs/principles-index.md",
705
+ content: TODO_PRINCIPLES_INDEX
706
+ },
707
+ {
708
+ // No single platform analog — authored against the task-entity schema so
709
+ // there is something claimable the moment a customer vault exists.
710
+ source: "specs/protocols/canonical-frontmatter-templates.md",
711
+ classification: "template-shape",
712
+ dest: "specs/tasks/HS-START-0001-TASK-your-first-task.md",
713
+ content: TODO_STARTER_TASK
714
+ },
715
+ {
716
+ // No single platform analog — authored against the epic-entity schema so
717
+ // the starter task (above) has a `parent:` and the vault lints clean
718
+ // (HS-I2U9). See `TODO_STARTER_EPIC`'s doc comment for the full chain
719
+ // this requires (epic -> milestone -> roadmap).
720
+ source: "specs/protocols/canonical-frontmatter-templates.md",
721
+ classification: "template-shape",
722
+ dest: "specs/epics/HS-START-0000-EPIC-your-first-epic.md",
723
+ content: TODO_STARTER_EPIC
724
+ },
725
+ {
726
+ // No single platform analog — the starter epic's `milestone:` target.
727
+ source: "specs/protocols/canonical-frontmatter-templates.md",
728
+ classification: "template-shape",
729
+ dest: "specs/milestones/HS-START-0002-MIL-your-first-milestone.md",
730
+ content: TODO_STARTER_MILESTONE
731
+ },
732
+ {
733
+ // No single platform analog — schema per specs/entity-model.md §roadmap.
734
+ // Makes the starter milestone reachable ("not an orphan" per
735
+ // validateLinkage): it must appear in a roadmap_manifest.milestones list.
736
+ source: "specs/entity-model.md",
737
+ classification: "template-shape",
738
+ dest: "specs/roadmap.md",
739
+ content: TODO_STARTER_ROADMAP
740
+ },
741
+ // --- Universal principle docs (shape rows, group: "principle") ---
742
+ // Generalized from HeySpex's own P3/P4/P6/P7/P8 design-principle decisions
743
+ // (see specs/principles-index.md). Of those five, P6 (JTBD-parity) was
744
+ // excluded from this set of four: its decision body is inseparably about
745
+ // HeySpex's own bd/SuperClaude migration inventory (specific JTBD counts,
746
+ // named predecessor tools), not a portable architecture commitment the way
747
+ // the other four are. P3/P4/P7/P8 generalize cleanly.
748
+ {
749
+ source: "specs/memory/decisions/vault-as-format-primacy-validated-by-bd-retreat.md",
750
+ classification: "template-shape",
751
+ dest: "specs/memory/decisions/principle-vault-is-canonical.md",
752
+ group: "principle",
753
+ summary: "Markdown-in-git is the canonical store; nothing else drifts.",
754
+ content: principleSkeleton({
755
+ id: "principle-vault-is-canonical",
756
+ name: "Vault-as-Format Primacy \u2014 Markdown Files in Git Are Canonical",
757
+ topic: "principle-vault-is-canonical",
758
+ decision: "Markdown files in the git-tracked spec vault are the canonical store. Any derived index (cache, search index, dashboard) follows the vault \u2014 never the other way around.",
759
+ rationalePrompt: "TODO: why does this decision hold for your product? What alternative (a database, a JSON export) did you reject, and why?"
760
+ })
761
+ },
762
+ {
763
+ source: "specs/memory/decisions/composability-via-git-commit-boundary.md",
764
+ classification: "template-shape",
765
+ dest: "specs/memory/decisions/principle-git-commit-is-the-atomic-batch.md",
766
+ group: "principle",
767
+ summary: "A git commit is the atomic multi-file batch; no bespoke orchestration tier.",
768
+ content: principleSkeleton({
769
+ id: "principle-git-commit-is-the-atomic-batch",
770
+ name: "Composability via Git Commit Boundary \u2014 No Orchestration Tier",
771
+ topic: "principle-git-commit-is-the-atomic-batch",
772
+ decision: "A git commit is the atomic batch for multi-file operations. There is no custom orchestration verb (batch/transaction/pipeline command) layered above the git commit boundary.",
773
+ rationalePrompt: "TODO: why does this decision hold for your product?"
774
+ })
775
+ },
776
+ {
777
+ source: "specs/memory/decisions/orthogonal-asset-composition-p7.md",
778
+ classification: "template-shape",
779
+ dest: "specs/memory/decisions/principle-orthogonal-asset-composition.md",
780
+ group: "principle",
781
+ summary: "Independent behavior axes (command/persona/mode/workflow) compose, not bundle.",
782
+ content: principleSkeleton({
783
+ id: "principle-orthogonal-asset-composition",
784
+ name: "Orthogonal Asset Composition",
785
+ topic: "principle-orthogonal-asset-composition",
786
+ decision: "Independent axes of configurable behavior (e.g. command, persona, mode, workflow) compose orthogonally rather than being hard-coded together \u2014 each axis is a separate, combinable asset.",
787
+ rationalePrompt: "TODO: what are the independent axes in your product, and why keep them orthogonal instead of bundling them?"
788
+ })
789
+ },
790
+ {
791
+ source: "specs/memory/decisions/tool-agnostic-mcp-first-cli-second.md",
792
+ classification: "template-shape",
793
+ dest: "specs/memory/decisions/principle-tool-agnostic-by-construction.md",
794
+ group: "principle",
795
+ summary: "The product works across host tools/IDEs, not locked to one.",
796
+ content: principleSkeleton({
797
+ id: "principle-tool-agnostic-by-construction",
798
+ name: "Tool-Agnostic by Construction",
799
+ topic: "principle-tool-agnostic-by-construction",
800
+ decision: "The product does not depend on one specific host tool/IDE/agent. Every feature is built across compatibility layers so it works wherever at least one integration point is supported.",
801
+ rationalePrompt: "TODO: which compatibility layers does your product need (CLI? API? plugin? an MCP-equivalent)?"
802
+ })
803
+ }
804
+ ];
805
+
806
+ // spex-console/src/lib/cli/init.customer-vault.ts
807
+ var DEFAULT_CONSOLE_URL3 = "https://console.heyspex.local";
808
+ var SCAFFOLD_FOLDERS = [
809
+ "tasks",
810
+ "epics",
811
+ "milestones",
812
+ "research",
813
+ "memory/lessons",
814
+ "memory/decisions",
815
+ "memory/references",
816
+ "memory/ui-patterns",
817
+ "protocols",
818
+ "reviews",
819
+ "captures",
820
+ "adr",
821
+ "checks",
822
+ "events"
823
+ ];
824
+ function defaultVaultName(targetDir) {
825
+ return basename(targetDir) || targetDir;
826
+ }
827
+ function runCustomerVaultInit(args, deps, options = {}) {
828
+ const { targetDir } = args;
829
+ const written = [];
830
+ if (deps.exists(targetDir)) {
831
+ const entries = deps.readdir(targetDir);
832
+ if (entries.length > 0) {
833
+ return {
834
+ code: EXIT.INVALID_ARG,
835
+ message: `${targetDir} is not empty \u2014 customer-vault init refuses to write into an existing, non-empty directory.`,
836
+ payload: { ok: false, error: "target_not_empty", path: targetDir },
837
+ isError: true,
838
+ written: []
839
+ };
840
+ }
841
+ } else {
842
+ deps.mkdirRecursive(targetDir);
843
+ }
844
+ const specsRoot = deps.joinPath(targetDir, "specs");
845
+ deps.mkdirRecursive(specsRoot);
846
+ for (const folder of SCAFFOLD_FOLDERS) {
847
+ deps.mkdirRecursive(deps.joinPath(specsRoot, folder));
848
+ }
849
+ const vaultId = mintVaultId();
850
+ const yamlContent = renderHeyspexYaml({
851
+ consoleUrl: options.consoleUrl ?? DEFAULT_CONSOLE_URL3,
852
+ vaultRoot: "specs/",
853
+ vaultId
854
+ });
855
+ const heyspexYamlPath = deps.joinPath(targetDir, "heyspex.yaml");
856
+ deps.writeFile(heyspexYamlPath, yamlContent);
857
+ written.push(heyspexYamlPath);
858
+ const filter = options.manifestFilter ?? (() => true);
859
+ const rows = TEMPLATE_MANIFEST.filter(filter);
860
+ for (const row of rows) {
861
+ const destAbs = deps.joinPath(targetDir, ...row.dest.split("/"));
862
+ if (row.classification === "universal-template") {
863
+ const srcAbs = deps.joinPath(deps.platformRoot, ...row.source.split("/"));
864
+ deps.copyFile(srcAbs, destAbs);
865
+ } else {
866
+ if (row.content === void 0) {
867
+ throw new Error(
868
+ `template-manifest row for dest "${row.dest}" is classified template-shape but has no inline content`
869
+ );
870
+ }
871
+ deps.writeFile(destAbs, row.content);
872
+ }
873
+ written.push(destAbs);
874
+ }
875
+ if (options.afterScaffold && options.afterScaffold.length > 0) {
876
+ const ctx = {
877
+ targetDir,
878
+ vaultName: options.vaultName ?? defaultVaultName(targetDir),
879
+ templateVersion: TEMPLATE_VERSION,
880
+ manifestRows: rows,
881
+ deps,
882
+ recordWrite: (path) => {
883
+ written.push(path);
884
+ }
885
+ };
886
+ for (const hook of options.afterScaffold) hook(ctx);
887
+ }
888
+ return {
889
+ code: EXIT.OK,
890
+ message: `Initialized customer vault at ${targetDir} (${written.length} files written, template v${TEMPLATE_VERSION}).`,
891
+ payload: {
892
+ ok: true,
893
+ targetDir,
894
+ templateVersion: TEMPLATE_VERSION,
895
+ vaultId,
896
+ written
897
+ },
898
+ isError: false,
899
+ written
900
+ };
901
+ }
902
+
903
+ // spex-console/src/lib/cli/init-git-finalize.ts
904
+ import { spawnSync } from "node:child_process";
905
+ import { readFileSync as readFileSync3 } from "node:fs";
906
+ var nodeGitRunner = {
907
+ run(args, cwd) {
908
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
909
+ return {
910
+ ok: result.error === void 0 && result.status === 0,
911
+ stdout: result.stdout ?? "",
912
+ stderr: result.stderr ?? ""
913
+ };
914
+ }
915
+ };
916
+ function readTemplateVersion(yamlText) {
917
+ const m = yamlText.match(/^template_version:\s*(.+?)\s*$/m);
918
+ if (!m) return null;
919
+ const raw = m[1].trim();
920
+ const unquoted = raw.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1");
921
+ return unquoted.length > 0 ? unquoted : null;
922
+ }
923
+ function ensureTemplateVersionLine(content, version) {
924
+ if (readTemplateVersion(content) !== null) {
925
+ return content.replace(/^template_version:.*$/m, `template_version: ${version}`);
926
+ }
927
+ const trimmed = content.replace(/\n+$/, "");
928
+ return `${trimmed}
929
+ template_version: ${version}
930
+ `;
931
+ }
932
+ function gitFinalizeHook(runner, notify) {
933
+ return (ctx) => {
934
+ const target = ctx.targetDir;
935
+ const yamlPath = ctx.deps.joinPath(target, "heyspex.yaml");
936
+ const existingYaml = readFileSync3(yamlPath, "utf8");
937
+ const updatedYaml = ensureTemplateVersionLine(existingYaml, ctx.templateVersion);
938
+ ctx.deps.writeFile(yamlPath, updatedYaml);
939
+ ctx.recordWrite(yamlPath);
940
+ const probe = runner.run(["--version"], target);
941
+ if (!probe.ok) {
942
+ notify("git not found - vault left uninitialized, run git init yourself");
943
+ return;
944
+ }
945
+ runner.run(["-C", target, "init"], target);
946
+ runner.run(["-C", target, "add", "-A"], target);
947
+ const commitMessage = `hs init --customer-vault - initial scaffold from HeySpex templates v${ctx.templateVersion}`;
948
+ runner.run(
949
+ [
950
+ "-C",
951
+ target,
952
+ "-c",
953
+ "user.name=HeySpex",
954
+ "-c",
955
+ "user.email=init@heyspex.local",
956
+ "commit",
957
+ "-m",
958
+ commitMessage
959
+ ],
960
+ target
961
+ );
962
+ };
963
+ }
964
+
965
+ // spex-console/src/lib/cli/principle-selection.ts
966
+ import { basename as basename2 } from "node:path";
967
+ var PRINCIPLES_INDEX_DEST = "specs/principles-index.md";
968
+ function principleKey(row) {
969
+ const base = basename2(row.dest, ".md");
970
+ return base.startsWith("principle-") ? base.slice("principle-".length) : base;
971
+ }
972
+ function principleWikilink(row) {
973
+ return basename2(row.dest, ".md");
974
+ }
975
+ function parsePrinciplesFlag(value, rows) {
976
+ const principleRows = rows.filter((row) => row.group === "principle");
977
+ const validKeys = principleRows.map(principleKey);
978
+ const trimmed = value?.trim();
979
+ if (trimmed === void 0 || trimmed === "" || trimmed === "all") {
980
+ return { ok: true, selected: new Set(validKeys) };
981
+ }
982
+ if (trimmed === "none") {
983
+ return { ok: true, selected: /* @__PURE__ */ new Set() };
984
+ }
985
+ const requested = trimmed.split(",").map((key) => key.trim()).filter((key) => key.length > 0);
986
+ const unknown = requested.filter((key) => !validKeys.includes(key));
987
+ if (unknown.length > 0) {
988
+ return {
989
+ ok: false,
990
+ message: `Unknown principle key(s): ${unknown.join(", ")}. Valid keys are: ${validKeys.join(", ")} (or "all" / "none").`
991
+ };
992
+ }
993
+ return { ok: true, selected: new Set(requested) };
994
+ }
995
+ function listPrincipleChoices(rows) {
996
+ return rows.filter((row) => row.group === "principle").map((row) => ({ key: principleKey(row), summary: row.summary ?? "", dest: row.dest }));
997
+ }
998
+ async function promptPrincipleSelection(ask, rows) {
999
+ const selected = /* @__PURE__ */ new Set();
1000
+ for (const choice of listPrincipleChoices(rows)) {
1001
+ const keep = await ask(`Keep principle "${choice.key}" \u2014 ${choice.summary}? (y/n)`);
1002
+ if (keep) {
1003
+ selected.add(choice.key);
1004
+ }
1005
+ }
1006
+ return selected;
1007
+ }
1008
+ function principleManifestFilter(selected) {
1009
+ return (row) => {
1010
+ if (row.group !== "principle") {
1011
+ return true;
1012
+ }
1013
+ return selected.has(principleKey(row));
1014
+ };
1015
+ }
1016
+ function principlesIndexHook(selected, rows) {
1017
+ return (ctx) => {
1018
+ const indexRow = rows.find((row) => row.dest === PRINCIPLES_INDEX_DEST);
1019
+ if (!indexRow || indexRow.content === void 0) {
1020
+ throw new Error(
1021
+ `principlesIndexHook: no template-manifest row with inline content found for dest "${PRINCIPLES_INDEX_DEST}"`
1022
+ );
1023
+ }
1024
+ const principleRows = rows.filter((row) => row.group === "principle");
1025
+ const kept = principleRows.filter((row) => selected.has(principleKey(row)));
1026
+ const keptSection = kept.length === 0 ? "" : "\n## Universal principles kept\n\n" + kept.map((row) => `- [[${principleWikilink(row)}]] \u2014 ${row.summary ?? ""}`).join("\n") + "\n";
1027
+ const rendered = indexRow.content + keptSection;
1028
+ const destAbs = ctx.deps.joinPath(ctx.targetDir, ...PRINCIPLES_INDEX_DEST.split("/"));
1029
+ ctx.deps.writeFile(destAbs, rendered);
1030
+ ctx.recordWrite(destAbs);
1031
+ };
1032
+ }
1033
+
1034
+ // spex-console/bin/hs-init.ts
1035
+ var INTERACTIVE = process2.stdin.isTTY === true;
1036
+ async function askYesNo(question) {
1037
+ const rl = createInterface({ input: process2.stdin, output: process2.stdout });
1038
+ try {
1039
+ const answer = (await rl.question(`${question} `)).trim().toLowerCase();
1040
+ return answer === "y" || answer === "yes" || answer === "";
1041
+ } finally {
1042
+ rl.close();
1043
+ }
1044
+ }
1045
+ async function resolvePrincipleSelection(principlesFlag) {
1046
+ if (principlesFlag !== void 0) {
1047
+ return parsePrinciplesFlag(principlesFlag, TEMPLATE_MANIFEST);
1048
+ }
1049
+ if (INTERACTIVE) {
1050
+ process2.stdout.write("Select which universal principle docs to ship with this vault:\n");
1051
+ for (const choice of listPrincipleChoices(TEMPLATE_MANIFEST)) {
1052
+ process2.stdout.write(` - ${choice.key}: ${choice.summary}
1053
+ `);
1054
+ }
1055
+ const selected = await promptPrincipleSelection(askYesNo, TEMPLATE_MANIFEST);
1056
+ return { ok: true, selected };
1057
+ }
1058
+ return parsePrinciplesFlag(void 0, TEMPLATE_MANIFEST);
1059
+ }
1060
+ async function main() {
1061
+ const repoRoot = process2.env.HS_VAULT_ROOT ? resolve(process2.env.HS_VAULT_ROOT) : process2.cwd();
1062
+ const parsed = parseInitArgs(process2.argv.slice(3));
1063
+ if (parsed.help) {
1064
+ process2.stdout.write(`${HELP}
1065
+ `);
1066
+ process2.exit(EXIT.OK);
1067
+ }
1068
+ if (parsed.error) {
1069
+ process2.stderr.write(`hs init: ${parsed.error.message}
1070
+ `);
1071
+ process2.exit(parsed.error.code);
1072
+ }
1073
+ const args = parsed.args;
1074
+ if (args.customerVaultDir) {
1075
+ const here = dirname(fileURLToPath(import.meta.url));
1076
+ const platformRoot = resolve(here, "..", "..");
1077
+ const targetDir = resolve(args.customerVaultDir);
1078
+ const deps = {
1079
+ platformRoot,
1080
+ exists: (p) => existsSync(p),
1081
+ readdir: (p) => readdirSync(p),
1082
+ mkdirRecursive: (p) => {
1083
+ mkdirSync(p, { recursive: true });
1084
+ },
1085
+ writeFile: (p, c) => writeFileSync(p, c, "utf8"),
1086
+ copyFile: (src, dest) => copyFileSync(src, dest),
1087
+ joinPath: (...s) => join3(...s)
1088
+ };
1089
+ const principlesResult = await resolvePrincipleSelection(args.principles);
1090
+ if (!principlesResult.ok) {
1091
+ process2.stderr.write(`hs init: ${principlesResult.message}
1092
+ `);
1093
+ process2.exit(EXIT.INVALID_ARG);
1094
+ }
1095
+ const selected = principlesResult.selected;
1096
+ const notices = [];
1097
+ const afterScaffold = [
1098
+ customerClaudeMdHook(),
1099
+ principlesIndexHook(selected, TEMPLATE_MANIFEST)
1100
+ ];
1101
+ if (!args.noGit) {
1102
+ afterScaffold.push(gitFinalizeHook(nodeGitRunner, (msg) => notices.push(msg)));
1103
+ }
1104
+ const result2 = runCustomerVaultInit({ targetDir }, deps, {
1105
+ consoleUrl: args.consoleUrl,
1106
+ vaultName: args.vaultName,
1107
+ manifestFilter: principleManifestFilter(selected),
1108
+ afterScaffold
1109
+ });
1110
+ const sink2 = result2.isError ? process2.stderr : process2.stdout;
1111
+ if (args.json) {
1112
+ sink2.write(`${JSON.stringify({ ...result2.payload, notices })}
1113
+ `);
1114
+ } else {
1115
+ sink2.write(`${result2.message}
1116
+ `);
1117
+ for (const notice of notices) sink2.write(`${notice}
1118
+ `);
1119
+ }
1120
+ process2.exit(result2.code);
1121
+ }
1122
+ const result = runInit(args, {
1123
+ repoRoot,
1124
+ fileExists: (p) => existsSync(p),
1125
+ readFile: (p) => {
1126
+ try {
1127
+ return readFileSync4(p, "utf8");
1128
+ } catch {
1129
+ return null;
1130
+ }
1131
+ },
1132
+ writeFile: (p, c) => writeFileSync(p, c, "utf8"),
1133
+ joinPath: (...s) => join3(...s)
1134
+ });
1135
+ const sink = result.isError ? process2.stderr : process2.stdout;
1136
+ sink.write(`${args.json ? JSON.stringify(result.payload) : result.message}
1137
+ `);
1138
+ process2.exit(result.code);
1139
+ }
1140
+ main().catch((err) => {
1141
+ const message = err instanceof Error ? err.message : String(err);
1142
+ process2.stderr.write(`hs init: unhandled: ${message}
1143
+ `);
1144
+ process2.exit(EXIT.WRITE_FAILED);
1145
+ });