@lumenflow/core 2.1.1 → 2.2.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.
@@ -16,6 +16,10 @@
16
16
  * @see {@link tools/__tests__/status-date-from-event.test.mjs} - Date tests
17
17
  * @see {@link tools/lib/wu-state-store.mjs} - State store
18
18
  */
19
+ interface BacklogYamlOptions {
20
+ wuDir?: string;
21
+ projectRoot?: string;
22
+ }
19
23
  /**
20
24
  * Generates backlog.md markdown from WUStateStore
21
25
  *
@@ -26,6 +30,9 @@
26
30
  * - Placeholder text for empty sections
27
31
  *
28
32
  * @param {import('./wu-state-store.js').WUStateStore} store - State store to read from
33
+ * @param {object} [options] - Optional settings
34
+ * @param {string} [options.wuDir] - Absolute or repo-relative path to WU YAML directory
35
+ * @param {string} [options.projectRoot] - Project root override for path resolution
29
36
  * @returns {Promise<string>} Markdown content for backlog.md
30
37
  *
31
38
  * @example
@@ -34,25 +41,7 @@
34
41
  * const markdown = await generateBacklog(store);
35
42
  * await fs.writeFile('backlog.md', markdown, 'utf-8');
36
43
  */
37
- export declare function generateBacklog(store: any): Promise<string>;
38
- /**
39
- * Generates status.md markdown from WUStateStore
40
- *
41
- * Format matches current status.md exactly:
42
- * - Header with last updated timestamp
43
- * - In Progress section
44
- * - Completed section with dates
45
- * - Placeholder for empty sections
46
- *
47
- * @param {import('./wu-state-store.js').WUStateStore} store - State store to read from
48
- * @returns {Promise<string>} Markdown content for status.md
49
- *
50
- * @example
51
- * const store = new WUStateStore('/path/to/state');
52
- * await store.load();
53
- * const markdown = await generateStatus(store);
54
- * await fs.writeFile('status.md', markdown, 'utf-8');
55
- */
44
+ export declare function generateBacklog(store: any, options?: BacklogYamlOptions): Promise<string>;
56
45
  export declare function generateStatus(store: any): Promise<string>;
57
46
  /**
58
47
  * WU-2244: Get completion date for a WU from state store
@@ -109,3 +98,4 @@ export declare function validateBacklogConsistency(store: any, markdown: any): P
109
98
  valid: boolean;
110
99
  errors: any[];
111
100
  }>;
101
+ export {};
@@ -17,6 +17,82 @@
17
17
  * @see {@link tools/lib/wu-state-store.mjs} - State store
18
18
  */
19
19
  import { createHash } from 'node:crypto';
20
+ import { existsSync, readdirSync } from 'node:fs';
21
+ import path from 'node:path';
22
+ import { readWURaw } from './wu-yaml.js';
23
+ import { WU_STATUS, WU_STATUS_GROUPS } from './wu-constants.js';
24
+ import { createWuPaths, resolveFromProjectRoot } from './wu-paths.js';
25
+ const WU_FILENAME_PATTERN = /^WU-\d+\.yaml$/;
26
+ function normalizeYamlScalar(value) {
27
+ if (value === undefined || value === null) {
28
+ return '';
29
+ }
30
+ return typeof value === 'string' ? value : String(value);
31
+ }
32
+ function normalizeYamlStatus(value) {
33
+ const normalized = normalizeYamlScalar(value).trim().toLowerCase();
34
+ return normalized === '' ? WU_STATUS.READY : normalized;
35
+ }
36
+ function mapYamlStatusToSection(status) {
37
+ if (WU_STATUS_GROUPS.UNCLAIMED.includes(status)) {
38
+ return WU_STATUS.READY;
39
+ }
40
+ if (status === WU_STATUS.IN_PROGRESS) {
41
+ return WU_STATUS.IN_PROGRESS;
42
+ }
43
+ if (status === WU_STATUS.BLOCKED) {
44
+ return WU_STATUS.BLOCKED;
45
+ }
46
+ if (WU_STATUS_GROUPS.TERMINAL.includes(status)) {
47
+ return WU_STATUS.DONE;
48
+ }
49
+ return WU_STATUS.READY;
50
+ }
51
+ function compareWuIds(a, b) {
52
+ const numA = Number.parseInt(a.replace(/^WU-/, ''), 10);
53
+ const numB = Number.parseInt(b.replace(/^WU-/, ''), 10);
54
+ if (!Number.isNaN(numA) && !Number.isNaN(numB) && numA !== numB) {
55
+ return numA - numB;
56
+ }
57
+ return a.localeCompare(b);
58
+ }
59
+ function resolveWuDir(options = {}) {
60
+ const paths = createWuPaths({ projectRoot: options.projectRoot });
61
+ const configured = options.wuDir || paths.WU_DIR();
62
+ return path.isAbsolute(configured) ? configured : resolveFromProjectRoot(configured);
63
+ }
64
+ function loadYamlWuEntries(wuDir) {
65
+ if (!existsSync(wuDir)) {
66
+ return new Map();
67
+ }
68
+ const files = readdirSync(wuDir).filter((file) => WU_FILENAME_PATTERN.test(file));
69
+ files.sort((a, b) => compareWuIds(a.replace(/\.yaml$/, ''), b.replace(/\.yaml$/, '')));
70
+ const entries = new Map();
71
+ for (const file of files) {
72
+ const wuId = file.replace(/\.yaml$/, '');
73
+ const doc = readWURaw(path.join(wuDir, file));
74
+ if (!doc || typeof doc !== 'object') {
75
+ continue;
76
+ }
77
+ entries.set(wuId, {
78
+ status: normalizeYamlStatus(doc.status),
79
+ title: normalizeYamlScalar(doc.title),
80
+ lane: normalizeYamlScalar(doc.lane),
81
+ });
82
+ }
83
+ return entries;
84
+ }
85
+ function getMergedBacklogEntry(store, yamlEntries, wuId) {
86
+ const state = typeof store.getWUState === 'function' ? store.getWUState(wuId) : store.wuState.get(wuId);
87
+ if (state) {
88
+ return { title: state.title, lane: state.lane };
89
+ }
90
+ const yamlEntry = yamlEntries.get(wuId);
91
+ if (!yamlEntry) {
92
+ return null;
93
+ }
94
+ return { title: yamlEntry.title, lane: yamlEntry.lane };
95
+ }
20
96
  /**
21
97
  * Generates backlog.md markdown from WUStateStore
22
98
  *
@@ -27,6 +103,9 @@ import { createHash } from 'node:crypto';
27
103
  * - Placeholder text for empty sections
28
104
  *
29
105
  * @param {import('./wu-state-store.js').WUStateStore} store - State store to read from
106
+ * @param {object} [options] - Optional settings
107
+ * @param {string} [options.wuDir] - Absolute or repo-relative path to WU YAML directory
108
+ * @param {string} [options.projectRoot] - Project root override for path resolution
30
109
  * @returns {Promise<string>} Markdown content for backlog.md
31
110
  *
32
111
  * @example
@@ -36,7 +115,7 @@ import { createHash } from 'node:crypto';
36
115
  * await fs.writeFile('backlog.md', markdown, 'utf-8');
37
116
  */
38
117
  // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
39
- export async function generateBacklog(store) {
118
+ export async function generateBacklog(store, options = {}) {
40
119
  // Start with frontmatter
41
120
  const frontmatter = `---
42
121
  sections:
@@ -54,25 +133,60 @@ sections:
54
133
  insertion: after_heading_blank_line
55
134
  ---
56
135
 
57
- > Agent: Read **docs/04-operations/_frameworks/lumenflow/agent/onboarding/starting-prompt.md** first, then follow **docs/04-operations/\\_frameworks/lumenflow/lumenflow-complete.md** for execution.
136
+ > Agent: Read **docs/04-operations/_frameworks/lumenflow/agent/onboarding/starting-prompt.md** first, then follow **docs/04-operations/\_frameworks/lumenflow/lumenflow-complete.md** for execution.
58
137
 
59
138
  # Backlog (single source of truth)
60
139
 
61
140
  `;
141
+ const yamlEntries = loadYamlWuEntries(resolveWuDir(options));
142
+ const storeReady = Array.from(store.getByStatus('ready'));
143
+ const storeInProgress = Array.from(store.getByStatus('in_progress'));
144
+ const storeBlocked = Array.from(store.getByStatus('blocked'));
145
+ const storeDone = Array.from(store.getByStatus('done'));
146
+ const storeIds = new Set([...storeReady, ...storeInProgress, ...storeBlocked, ...storeDone]);
147
+ const yamlReady = [];
148
+ const yamlInProgress = [];
149
+ const yamlBlocked = [];
150
+ const yamlDone = [];
151
+ for (const [wuId, entry] of yamlEntries.entries()) {
152
+ if (storeIds.has(wuId)) {
153
+ continue;
154
+ }
155
+ const status = mapYamlStatusToSection(entry.status);
156
+ if (status === WU_STATUS.IN_PROGRESS) {
157
+ yamlInProgress.push(wuId);
158
+ }
159
+ else if (status === WU_STATUS.BLOCKED) {
160
+ yamlBlocked.push(wuId);
161
+ }
162
+ else if (status === WU_STATUS.DONE) {
163
+ yamlDone.push(wuId);
164
+ }
165
+ else {
166
+ yamlReady.push(wuId);
167
+ }
168
+ }
169
+ yamlReady.sort(compareWuIds);
170
+ yamlInProgress.sort(compareWuIds);
171
+ yamlBlocked.sort(compareWuIds);
172
+ yamlDone.sort(compareWuIds);
173
+ const ready = [...storeReady, ...yamlReady];
174
+ const inProgress = [...storeInProgress, ...yamlInProgress];
175
+ const blocked = [...storeBlocked, ...yamlBlocked];
176
+ const done = [...storeDone, ...yamlDone];
62
177
  // Generate sections
63
178
  const sections = [];
64
179
  // Ready section (WUs with status: ready)
65
180
  sections.push('## 🚀 Ready (pull from here)');
66
181
  sections.push('');
67
- const ready = store.getByStatus('ready');
68
- if (ready.size === 0) {
182
+ if (ready.length === 0) {
69
183
  sections.push('(No items ready)');
70
184
  }
71
185
  else {
72
186
  for (const wuId of ready) {
73
- const state = store.wuState.get(wuId);
74
- if (state) {
75
- sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml) — ${state.lane}`);
187
+ const entry = getMergedBacklogEntry(store, yamlEntries, wuId);
188
+ if (entry) {
189
+ sections.push(`- [${wuId} — ${entry.title}](wu/${wuId}.yaml) — ${entry.lane}`);
76
190
  }
77
191
  }
78
192
  }
@@ -80,15 +194,14 @@ sections:
80
194
  sections.push('');
81
195
  sections.push('## 🔧 In progress');
82
196
  sections.push('');
83
- const inProgress = store.getByStatus('in_progress');
84
- if (inProgress.size === 0) {
197
+ if (inProgress.length === 0) {
85
198
  sections.push('(No items currently in progress)');
86
199
  }
87
200
  else {
88
201
  for (const wuId of inProgress) {
89
- const state = store.wuState.get(wuId);
90
- if (state) {
91
- sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml) — ${state.lane}`);
202
+ const entry = getMergedBacklogEntry(store, yamlEntries, wuId);
203
+ if (entry) {
204
+ sections.push(`- [${wuId} — ${entry.title}](wu/${wuId}.yaml) — ${entry.lane}`);
92
205
  }
93
206
  }
94
207
  }
@@ -96,15 +209,14 @@ sections:
96
209
  sections.push('');
97
210
  sections.push('## ⛔ Blocked');
98
211
  sections.push('');
99
- const blocked = store.getByStatus('blocked');
100
- if (blocked.size === 0) {
212
+ if (blocked.length === 0) {
101
213
  sections.push('(No items currently blocked)');
102
214
  }
103
215
  else {
104
216
  for (const wuId of blocked) {
105
- const state = store.wuState.get(wuId);
106
- if (state) {
107
- sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml) — ${state.lane}`);
217
+ const entry = getMergedBacklogEntry(store, yamlEntries, wuId);
218
+ if (entry) {
219
+ sections.push(`- [${wuId} — ${entry.title}](wu/${wuId}.yaml) — ${entry.lane}`);
108
220
  }
109
221
  }
110
222
  }
@@ -112,39 +224,19 @@ sections:
112
224
  sections.push('');
113
225
  sections.push('## ✅ Done');
114
226
  sections.push('');
115
- const done = store.getByStatus('done');
116
- if (done.size === 0) {
227
+ if (done.length === 0) {
117
228
  sections.push('(No completed items)');
118
229
  }
119
230
  else {
120
231
  for (const wuId of done) {
121
- const state = store.wuState.get(wuId);
122
- if (state) {
123
- sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml)`);
232
+ const entry = getMergedBacklogEntry(store, yamlEntries, wuId);
233
+ if (entry) {
234
+ sections.push(`- [${wuId} — ${entry.title}](wu/${wuId}.yaml)`);
124
235
  }
125
236
  }
126
237
  }
127
238
  return frontmatter + sections.join('\n');
128
239
  }
129
- /**
130
- * Generates status.md markdown from WUStateStore
131
- *
132
- * Format matches current status.md exactly:
133
- * - Header with last updated timestamp
134
- * - In Progress section
135
- * - Completed section with dates
136
- * - Placeholder for empty sections
137
- *
138
- * @param {import('./wu-state-store.js').WUStateStore} store - State store to read from
139
- * @returns {Promise<string>} Markdown content for status.md
140
- *
141
- * @example
142
- * const store = new WUStateStore('/path/to/state');
143
- * await store.load();
144
- * const markdown = await generateStatus(store);
145
- * await fs.writeFile('status.md', markdown, 'utf-8');
146
- */
147
- // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
148
240
  export async function generateStatus(store) {
149
241
  const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
150
242
  // Header
package/dist/index.d.ts CHANGED
@@ -22,6 +22,7 @@ export * from './wu-yaml.js';
22
22
  export { getAssignedEmail } from './wu-claim-helpers.js';
23
23
  export * from './wu-done-worktree.js';
24
24
  export * from './wu-done-validators.js';
25
+ export * from './wu-done-concurrent-merge.js';
25
26
  export * from './wu-helpers.js';
26
27
  export * from './wu-schema.js';
27
28
  export * from './wu-validator.js';
@@ -31,6 +32,7 @@ export * from './spawn-tree.js';
31
32
  export * from './spawn-recovery.js';
32
33
  export * from './spawn-monitor.js';
33
34
  export * from './spawn-escalation.js';
35
+ export { SPAWN_SENTINEL, SPAWN_PROMPT_VERSION, SpawnPromptSchema, computeChecksum, createSpawnPrompt, validateSpawnPrompt, parseSpawnPrompt, serializeSpawnPrompt, checkSentinel, type SpawnPrompt, type SpawnPromptValidationResult, type ParseResult as SpawnPromptParseResult, } from './spawn-prompt-schema.js';
34
36
  export * from './backlog-generator.js';
35
37
  export * from './backlog-parser.js';
36
38
  export * from './backlog-editor.js';
package/dist/index.js CHANGED
@@ -37,6 +37,8 @@ export * from './wu-yaml.js';
37
37
  export { getAssignedEmail } from './wu-claim-helpers.js';
38
38
  export * from './wu-done-worktree.js';
39
39
  export * from './wu-done-validators.js';
40
+ // WU-1145: Concurrent backlog merge utilities
41
+ export * from './wu-done-concurrent-merge.js';
40
42
  export * from './wu-helpers.js';
41
43
  export * from './wu-schema.js';
42
44
  export * from './wu-validator.js';
@@ -47,6 +49,9 @@ export * from './spawn-tree.js';
47
49
  export * from './spawn-recovery.js';
48
50
  export * from './spawn-monitor.js';
49
51
  export * from './spawn-escalation.js';
52
+ // WU-1142: Spawn prompt schema for truncation-resistant prompts
53
+ // Explicit exports to avoid ValidationResult conflict with validation/index.js
54
+ export { SPAWN_SENTINEL, SPAWN_PROMPT_VERSION, SpawnPromptSchema, computeChecksum, createSpawnPrompt, validateSpawnPrompt, parseSpawnPrompt, serializeSpawnPrompt, checkSentinel, } from './spawn-prompt-schema.js';
50
55
  // Backlog management
51
56
  export * from './backlog-generator.js';
52
57
  export * from './backlog-parser.js';
@@ -162,6 +162,7 @@ export declare const ClientBlockSchema: z.ZodObject<{
162
162
  export declare const ClientSkillsSchema: z.ZodObject<{
163
163
  instructions: z.ZodOptional<z.ZodString>;
164
164
  recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
165
+ byLane: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
165
166
  }, z.core.$strip>;
166
167
  /**
167
168
  * Client configuration (per-client settings)
@@ -176,6 +177,7 @@ export declare const ClientConfigSchema: z.ZodObject<{
176
177
  skills: z.ZodOptional<z.ZodObject<{
177
178
  instructions: z.ZodOptional<z.ZodString>;
178
179
  recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
180
+ byLane: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
179
181
  }, z.core.$strip>>;
180
182
  }, z.core.$strip>;
181
183
  /**
@@ -193,6 +195,7 @@ export declare const AgentsConfigSchema: z.ZodObject<{
193
195
  skills: z.ZodOptional<z.ZodObject<{
194
196
  instructions: z.ZodOptional<z.ZodString>;
195
197
  recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
198
+ byLane: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
196
199
  }, z.core.$strip>>;
197
200
  }, z.core.$strip>>>;
198
201
  methodology: z.ZodDefault<z.ZodObject<{
@@ -346,6 +349,7 @@ export declare const LumenFlowConfigSchema: z.ZodObject<{
346
349
  skills: z.ZodOptional<z.ZodObject<{
347
350
  instructions: z.ZodOptional<z.ZodString>;
348
351
  recommended: z.ZodDefault<z.ZodArray<z.ZodString>>;
352
+ byLane: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
349
353
  }, z.core.$strip>>;
350
354
  }, z.core.$strip>>>;
351
355
  methodology: z.ZodDefault<z.ZodObject<{
@@ -509,6 +513,7 @@ export declare function validateConfig(data: unknown): z.ZodSafeParseResult<{
509
513
  skills?: {
510
514
  recommended: string[];
511
515
  instructions?: string;
516
+ byLane?: Record<string, string[]>;
512
517
  };
513
518
  }>;
514
519
  methodology: {
@@ -256,6 +256,15 @@ export const ClientSkillsSchema = z.object({
256
256
  instructions: z.string().optional(),
257
257
  /** Recommended skills to load for this client */
258
258
  recommended: z.array(z.string()).default([]),
259
+ /**
260
+ * WU-1142: Lane-specific skills to recommend
261
+ * Maps lane names to arrays of skill names
262
+ * @example
263
+ * byLane:
264
+ * 'Framework: Core': ['tdd-workflow', 'lumenflow-gates']
265
+ * 'Content: Documentation': ['worktree-discipline']
266
+ */
267
+ byLane: z.record(z.string(), z.array(z.string())).optional(),
259
268
  });
260
269
  /**
261
270
  * Client configuration (per-client settings)
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Spawn Prompt Schema (WU-1142)
3
+ *
4
+ * Zod schema for truncation-resistant spawn prompts.
5
+ * Implements a YAML envelope format with:
6
+ * - SHA256 checksums for integrity validation
7
+ * - Sentinel values for truncation detection
8
+ * - Schema validation for agent consumers
9
+ *
10
+ * Three-Layer Defense:
11
+ * 1. YAML Envelope - head/tail truncation breaks YAML parse
12
+ * 2. Checksum - validates content integrity
13
+ * 3. Sentinel - confirms complete transmission
14
+ *
15
+ * @module spawn-prompt-schema
16
+ */
17
+ import { z } from 'zod';
18
+ /**
19
+ * Sentinel value that marks complete spawn prompt transmission
20
+ */
21
+ export declare const SPAWN_SENTINEL = "LUMENFLOW_SPAWN_COMPLETE";
22
+ /**
23
+ * Current schema version
24
+ */
25
+ export declare const SPAWN_PROMPT_VERSION = "1.0.0";
26
+ /**
27
+ * Zod schema for spawn prompt envelope
28
+ */
29
+ export declare const SpawnPromptSchema: z.ZodObject<{
30
+ wu_id: z.ZodString;
31
+ version: z.ZodDefault<z.ZodString>;
32
+ checksum: z.ZodString;
33
+ content: z.ZodString;
34
+ sentinel: z.ZodLiteral<"LUMENFLOW_SPAWN_COMPLETE">;
35
+ }, z.core.$strip>;
36
+ /**
37
+ * Type for a valid spawn prompt
38
+ */
39
+ export type SpawnPrompt = z.infer<typeof SpawnPromptSchema>;
40
+ /**
41
+ * Compute SHA256 checksum of content
42
+ *
43
+ * @param content - Content to checksum
44
+ * @returns Hex-encoded SHA256 hash
45
+ */
46
+ export declare function computeChecksum(content: string): string;
47
+ /**
48
+ * Create a spawn prompt envelope with computed checksum
49
+ *
50
+ * @param wuId - WU ID for the spawn prompt
51
+ * @param content - The spawn prompt content
52
+ * @returns Valid spawn prompt envelope
53
+ */
54
+ export declare function createSpawnPrompt(wuId: string, content: string): SpawnPrompt;
55
+ /**
56
+ * Spawn prompt validation result type
57
+ */
58
+ export interface SpawnPromptValidationResult {
59
+ valid: boolean;
60
+ error?: string;
61
+ }
62
+ /**
63
+ * Validate a spawn prompt, checking both schema and checksum
64
+ *
65
+ * @param data - Data to validate
66
+ * @returns Validation result with error message if invalid
67
+ */
68
+ export declare function validateSpawnPrompt(data: unknown): SpawnPromptValidationResult;
69
+ /**
70
+ * Parse result type
71
+ */
72
+ export interface ParseResult {
73
+ success: boolean;
74
+ data?: SpawnPrompt;
75
+ error?: string;
76
+ }
77
+ /**
78
+ * Parse a YAML spawn prompt string
79
+ *
80
+ * This function handles:
81
+ * 1. YAML parsing (head/tail truncation breaks parse)
82
+ * 2. Schema validation
83
+ * 3. Checksum validation (detects content corruption)
84
+ *
85
+ * @param yamlString - YAML string to parse
86
+ * @returns Parse result with data or error
87
+ */
88
+ export declare function parseSpawnPrompt(yamlString: string): ParseResult;
89
+ /**
90
+ * Serialize a spawn prompt to YAML format
91
+ *
92
+ * @param prompt - Spawn prompt to serialize
93
+ * @returns YAML string
94
+ */
95
+ export declare function serializeSpawnPrompt(prompt: SpawnPrompt): string;
96
+ /**
97
+ * Quick validation check for truncated output
98
+ *
99
+ * This is a fast check that can be used before full parsing:
100
+ * - Checks if sentinel appears at the end of the string
101
+ * - Does not validate checksum (use parseSpawnPrompt for full validation)
102
+ *
103
+ * @param yamlString - String to check
104
+ * @returns true if sentinel found near end, false otherwise
105
+ */
106
+ export declare function checkSentinel(yamlString: string): boolean;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Spawn Prompt Schema (WU-1142)
3
+ *
4
+ * Zod schema for truncation-resistant spawn prompts.
5
+ * Implements a YAML envelope format with:
6
+ * - SHA256 checksums for integrity validation
7
+ * - Sentinel values for truncation detection
8
+ * - Schema validation for agent consumers
9
+ *
10
+ * Three-Layer Defense:
11
+ * 1. YAML Envelope - head/tail truncation breaks YAML parse
12
+ * 2. Checksum - validates content integrity
13
+ * 3. Sentinel - confirms complete transmission
14
+ *
15
+ * @module spawn-prompt-schema
16
+ */
17
+ import { z } from 'zod';
18
+ import { createHash } from 'node:crypto';
19
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
20
+ /**
21
+ * Sentinel value that marks complete spawn prompt transmission
22
+ */
23
+ export const SPAWN_SENTINEL = 'LUMENFLOW_SPAWN_COMPLETE';
24
+ /**
25
+ * Current schema version
26
+ */
27
+ export const SPAWN_PROMPT_VERSION = '1.0.0';
28
+ /**
29
+ * Zod schema for spawn prompt envelope
30
+ */
31
+ export const SpawnPromptSchema = z.object({
32
+ /** WU ID for this spawn prompt */
33
+ wu_id: z.string().regex(/^WU-\d+$/i, 'Invalid WU ID format'),
34
+ /** Schema version */
35
+ version: z.string().default(SPAWN_PROMPT_VERSION),
36
+ /** SHA256 checksum of content field */
37
+ checksum: z.string().length(64, 'Checksum must be 64 hex characters'),
38
+ /** The actual spawn prompt content */
39
+ content: z.string().min(1, 'Content cannot be empty'),
40
+ /** Sentinel value confirming complete transmission */
41
+ sentinel: z.literal(SPAWN_SENTINEL),
42
+ });
43
+ /**
44
+ * Compute SHA256 checksum of content
45
+ *
46
+ * @param content - Content to checksum
47
+ * @returns Hex-encoded SHA256 hash
48
+ */
49
+ export function computeChecksum(content) {
50
+ return createHash('sha256').update(content, 'utf8').digest('hex');
51
+ }
52
+ /**
53
+ * Create a spawn prompt envelope with computed checksum
54
+ *
55
+ * @param wuId - WU ID for the spawn prompt
56
+ * @param content - The spawn prompt content
57
+ * @returns Valid spawn prompt envelope
58
+ */
59
+ export function createSpawnPrompt(wuId, content) {
60
+ const checksum = computeChecksum(content);
61
+ return {
62
+ wu_id: wuId,
63
+ version: SPAWN_PROMPT_VERSION,
64
+ checksum,
65
+ content,
66
+ sentinel: SPAWN_SENTINEL,
67
+ };
68
+ }
69
+ /**
70
+ * Validate a spawn prompt, checking both schema and checksum
71
+ *
72
+ * @param data - Data to validate
73
+ * @returns Validation result with error message if invalid
74
+ */
75
+ export function validateSpawnPrompt(data) {
76
+ // First, validate schema
77
+ const schemaResult = SpawnPromptSchema.safeParse(data);
78
+ if (!schemaResult.success) {
79
+ // Zod v4: use schemaResult.error.issues instead of .errors
80
+ const errorMessages = schemaResult.error.issues
81
+ .map((e) => `${e.path.join('.')}: ${e.message}`)
82
+ .join('; ');
83
+ return {
84
+ valid: false,
85
+ error: `Schema validation failed: ${errorMessages}`,
86
+ };
87
+ }
88
+ // Then, validate checksum matches content
89
+ const prompt = schemaResult.data;
90
+ const computedChecksum = computeChecksum(prompt.content);
91
+ if (computedChecksum !== prompt.checksum) {
92
+ return {
93
+ valid: false,
94
+ error: `Checksum mismatch: expected ${prompt.checksum}, computed ${computedChecksum}. Content may be corrupted or truncated.`,
95
+ };
96
+ }
97
+ return { valid: true };
98
+ }
99
+ /**
100
+ * Parse a YAML spawn prompt string
101
+ *
102
+ * This function handles:
103
+ * 1. YAML parsing (head/tail truncation breaks parse)
104
+ * 2. Schema validation
105
+ * 3. Checksum validation (detects content corruption)
106
+ *
107
+ * @param yamlString - YAML string to parse
108
+ * @returns Parse result with data or error
109
+ */
110
+ export function parseSpawnPrompt(yamlString) {
111
+ // Step 1: Parse YAML
112
+ let parsed;
113
+ try {
114
+ parsed = parseYaml(yamlString);
115
+ }
116
+ catch (e) {
117
+ return {
118
+ success: false,
119
+ error: `YAML parse failed: ${e instanceof Error ? e.message : 'Unknown error'}. Output may be truncated.`,
120
+ };
121
+ }
122
+ // Step 2: Validate schema and checksum
123
+ const validationResult = validateSpawnPrompt(parsed);
124
+ if (!validationResult.valid) {
125
+ return {
126
+ success: false,
127
+ error: validationResult.error,
128
+ };
129
+ }
130
+ // Safe cast since validation passed
131
+ return {
132
+ success: true,
133
+ data: parsed,
134
+ };
135
+ }
136
+ /**
137
+ * Serialize a spawn prompt to YAML format
138
+ *
139
+ * @param prompt - Spawn prompt to serialize
140
+ * @returns YAML string
141
+ */
142
+ export function serializeSpawnPrompt(prompt) {
143
+ return stringifyYaml(prompt, {
144
+ lineWidth: 0, // Don't wrap lines
145
+ });
146
+ }
147
+ /**
148
+ * Quick validation check for truncated output
149
+ *
150
+ * This is a fast check that can be used before full parsing:
151
+ * - Checks if sentinel appears at the end of the string
152
+ * - Does not validate checksum (use parseSpawnPrompt for full validation)
153
+ *
154
+ * @param yamlString - String to check
155
+ * @returns true if sentinel found near end, false otherwise
156
+ */
157
+ export function checkSentinel(yamlString) {
158
+ const trimmed = yamlString.trim();
159
+ return trimmed.endsWith(SPAWN_SENTINEL) || trimmed.includes(`sentinel: ${SPAWN_SENTINEL}`);
160
+ }