feed-the-machine 1.1.0 → 1.3.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 (92) hide show
  1. package/bin/generate-manifest.mjs +253 -0
  2. package/bin/install.mjs +372 -26
  3. package/docs/INBOX.md +233 -0
  4. package/ftm/SKILL.md +34 -0
  5. package/ftm-audit/SKILL.md +69 -0
  6. package/ftm-brainstorm/SKILL.md +51 -0
  7. package/ftm-browse/SKILL.md +39 -0
  8. package/ftm-capture/SKILL.md +370 -0
  9. package/ftm-capture.yml +4 -0
  10. package/ftm-codex-gate/SKILL.md +59 -0
  11. package/ftm-config/SKILL.md +35 -0
  12. package/ftm-council/SKILL.md +56 -0
  13. package/ftm-dashboard/SKILL.md +34 -0
  14. package/ftm-debug/SKILL.md +84 -0
  15. package/ftm-diagram/SKILL.md +44 -0
  16. package/ftm-executor/SKILL.md +97 -0
  17. package/ftm-git/SKILL.md +60 -0
  18. package/ftm-inbox/backend/__init__.py +0 -0
  19. package/ftm-inbox/backend/adapters/__init__.py +0 -0
  20. package/ftm-inbox/backend/adapters/_retry.py +64 -0
  21. package/ftm-inbox/backend/adapters/base.py +230 -0
  22. package/ftm-inbox/backend/adapters/freshservice.py +104 -0
  23. package/ftm-inbox/backend/adapters/gmail.py +125 -0
  24. package/ftm-inbox/backend/adapters/jira.py +136 -0
  25. package/ftm-inbox/backend/adapters/registry.py +192 -0
  26. package/ftm-inbox/backend/adapters/slack.py +110 -0
  27. package/ftm-inbox/backend/db/__init__.py +0 -0
  28. package/ftm-inbox/backend/db/connection.py +54 -0
  29. package/ftm-inbox/backend/db/schema.py +78 -0
  30. package/ftm-inbox/backend/executor/__init__.py +7 -0
  31. package/ftm-inbox/backend/executor/engine.py +149 -0
  32. package/ftm-inbox/backend/executor/step_runner.py +98 -0
  33. package/ftm-inbox/backend/main.py +103 -0
  34. package/ftm-inbox/backend/models/__init__.py +1 -0
  35. package/ftm-inbox/backend/models/unified_task.py +36 -0
  36. package/ftm-inbox/backend/planner/__init__.py +6 -0
  37. package/ftm-inbox/backend/planner/generator.py +127 -0
  38. package/ftm-inbox/backend/planner/schema.py +34 -0
  39. package/ftm-inbox/backend/requirements.txt +5 -0
  40. package/ftm-inbox/backend/routes/__init__.py +0 -0
  41. package/ftm-inbox/backend/routes/execute.py +186 -0
  42. package/ftm-inbox/backend/routes/health.py +52 -0
  43. package/ftm-inbox/backend/routes/inbox.py +68 -0
  44. package/ftm-inbox/backend/routes/plan.py +271 -0
  45. package/ftm-inbox/bin/launchagent.mjs +91 -0
  46. package/ftm-inbox/bin/setup.mjs +188 -0
  47. package/ftm-inbox/bin/start.sh +10 -0
  48. package/ftm-inbox/bin/status.sh +17 -0
  49. package/ftm-inbox/bin/stop.sh +8 -0
  50. package/ftm-inbox/config.example.yml +55 -0
  51. package/ftm-inbox/package-lock.json +2898 -0
  52. package/ftm-inbox/package.json +26 -0
  53. package/ftm-inbox/postcss.config.js +6 -0
  54. package/ftm-inbox/src/app.css +199 -0
  55. package/ftm-inbox/src/app.html +18 -0
  56. package/ftm-inbox/src/lib/api.ts +166 -0
  57. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -0
  58. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -0
  59. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -0
  60. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -0
  61. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -0
  62. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -0
  63. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -0
  64. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -0
  65. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -0
  66. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -0
  67. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -0
  68. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -0
  69. package/ftm-inbox/src/lib/theme.ts +47 -0
  70. package/ftm-inbox/src/routes/+layout.svelte +76 -0
  71. package/ftm-inbox/src/routes/+page.svelte +401 -0
  72. package/ftm-inbox/static/favicon.png +0 -0
  73. package/ftm-inbox/svelte.config.js +12 -0
  74. package/ftm-inbox/tailwind.config.ts +63 -0
  75. package/ftm-inbox/tsconfig.json +13 -0
  76. package/ftm-inbox/vite.config.ts +6 -0
  77. package/ftm-intent/SKILL.md +44 -0
  78. package/ftm-manifest.json +3794 -0
  79. package/ftm-map/SKILL.md +50 -0
  80. package/ftm-mind/SKILL.md +173 -66
  81. package/ftm-pause/SKILL.md +43 -0
  82. package/ftm-researcher/SKILL.md +55 -0
  83. package/ftm-resume/SKILL.md +47 -0
  84. package/ftm-retro/SKILL.md +54 -0
  85. package/ftm-routine/SKILL.md +36 -0
  86. package/ftm-state/blackboard/capabilities.json +5 -0
  87. package/ftm-state/blackboard/capabilities.schema.json +27 -0
  88. package/ftm-upgrade/SKILL.md +41 -0
  89. package/hooks/ftm-blackboard-enforcer.sh +28 -27
  90. package/hooks/ftm-plan-gate.sh +21 -25
  91. package/install.sh +238 -111
  92. package/package.json +6 -2
@@ -131,10 +131,214 @@ function extractBlackboardPaths(lines) {
131
131
  return paths;
132
132
  }
133
133
 
134
+ // ---------------------------------------------------------------------------
135
+ // New section parsers for the 6 structured YAML contracts
136
+ // ---------------------------------------------------------------------------
137
+
138
+ /**
139
+ * Parses the ## Requirements section.
140
+ * Format: - type: `name` | required|optional | description
141
+ * Returns: Array of { type, name, required, description }
142
+ */
143
+ function parseRequirements(lines) {
144
+ const requirements = [];
145
+ // Match lines like: - tool: `knip` | required | static analysis engine
146
+ // or: - config: `knip.config.ts` | optional | custom knip config
147
+ const reqRegex = /^-\s+(tool|config|reference|env):\s+`([^`]+)`\s*\|\s*(required|optional)\s*\|\s*(.+)/;
148
+
149
+ for (const line of lines) {
150
+ const match = line.match(reqRegex);
151
+ if (match) {
152
+ requirements.push({
153
+ type: match[1],
154
+ name: match[2],
155
+ required: match[3] === 'required',
156
+ description: match[4].trim(),
157
+ });
158
+ }
159
+ }
160
+
161
+ return requirements;
162
+ }
163
+
164
+ /**
165
+ * Parses the ## Risk section.
166
+ * Format:
167
+ * - level: read_only | low_write | medium_write | high_write | destructive
168
+ * - scope: description
169
+ * - rollback: description
170
+ * Returns: { level, scope, rollback }
171
+ */
172
+ function parseRisk(lines) {
173
+ let level = null;
174
+ let scope = null;
175
+ let rollback = null;
176
+
177
+ for (const line of lines) {
178
+ const levelMatch = line.match(/^-\s+level:\s+(.+)/);
179
+ const scopeMatch = line.match(/^-\s+scope:\s+(.+)/);
180
+ const rollbackMatch = line.match(/^-\s+rollback:\s+(.+)/);
181
+
182
+ if (levelMatch) level = levelMatch[1].trim();
183
+ if (scopeMatch) scope = scopeMatch[1].trim();
184
+ if (rollbackMatch) rollback = rollbackMatch[1].trim();
185
+ }
186
+
187
+ return { level, scope, rollback };
188
+ }
189
+
190
+ /**
191
+ * Parses the ## Approval Gates section.
192
+ * Format:
193
+ * - trigger: condition | action: what happens
194
+ * - complexity_routing: micro → auto | small → auto | ...
195
+ * Returns: { gates: Array<{ trigger, action }>, complexity_routing: object }
196
+ */
197
+ function parseApprovalGates(lines) {
198
+ const gates = [];
199
+ let complexity_routing = null;
200
+
201
+ for (const line of lines) {
202
+ // complexity_routing line
203
+ const crMatch = line.match(/^-\s+complexity_routing:\s+(.+)/);
204
+ if (crMatch) {
205
+ // Parse: micro → auto | small → auto | medium → plan_first | ...
206
+ const routing = {};
207
+ const parts = crMatch[1].split('|').map(s => s.trim());
208
+ for (const part of parts) {
209
+ const arrowMatch = part.match(/^(\w+)\s+[→>-]+\s+(.+)/);
210
+ if (arrowMatch) {
211
+ routing[arrowMatch[1].trim()] = arrowMatch[2].trim();
212
+ }
213
+ }
214
+ complexity_routing = routing;
215
+ continue;
216
+ }
217
+
218
+ // trigger/action line
219
+ const triggerMatch = line.match(/^-\s+trigger:\s+(.+?)\s*\|\s*action:\s+(.+)/);
220
+ if (triggerMatch) {
221
+ gates.push({
222
+ trigger: triggerMatch[1].trim(),
223
+ action: triggerMatch[2].trim(),
224
+ });
225
+ }
226
+ }
227
+
228
+ return { gates, complexity_routing };
229
+ }
230
+
231
+ /**
232
+ * Parses the ## Fallbacks section.
233
+ * Format: - condition: description | action: what happens
234
+ * Returns: Array of { condition, action }
235
+ */
236
+ function parseFallbacks(lines) {
237
+ const fallbacks = [];
238
+ const fallbackRegex = /^-\s+condition:\s+(.+?)\s*\|\s*action:\s+(.+)/;
239
+
240
+ for (const line of lines) {
241
+ const match = line.match(fallbackRegex);
242
+ if (match) {
243
+ fallbacks.push({
244
+ condition: match[1].trim(),
245
+ action: match[2].trim(),
246
+ });
247
+ }
248
+ }
249
+
250
+ return fallbacks;
251
+ }
252
+
253
+ /**
254
+ * Parses the ## Capabilities section.
255
+ * Format: - mcp|cli|env: `name` | required|optional | description
256
+ * Returns: Array of { type, name, required, description }
257
+ */
258
+ function parseCapabilities(lines) {
259
+ const capabilities = [];
260
+ const capRegex = /^-\s+(mcp|cli|env):\s+`([^`]+)`\s*\|\s*(required|optional)\s*\|\s*(.+)/;
261
+
262
+ for (const line of lines) {
263
+ const match = line.match(capRegex);
264
+ if (match) {
265
+ capabilities.push({
266
+ type: match[1],
267
+ name: match[2],
268
+ required: match[3] === 'required',
269
+ description: match[4].trim(),
270
+ });
271
+ }
272
+ }
273
+
274
+ return capabilities;
275
+ }
276
+
277
+ /**
278
+ * Parses the ## Event Payloads section.
279
+ * Format:
280
+ * ### event_name
281
+ * - field: type — description
282
+ * Returns: object mapping event_name -> Array<{ field, type, description }>
283
+ */
284
+ function parseEventPayloads(content) {
285
+ const payloads = {};
286
+
287
+ // We need to parse ### sub-sections within ## Event Payloads.
288
+ // Find the section start, then extract up to the next ## heading or end of string.
289
+ // Using manual indexing is more reliable than regex for the "last section" case.
290
+ const sectionStart = content.indexOf('\n## Event Payloads\n');
291
+ if (sectionStart < 0) return payloads;
292
+
293
+ const afterHeading = content.substring(sectionStart + '\n## Event Payloads\n'.length);
294
+ const nextH2 = afterHeading.indexOf('\n## ');
295
+ const sectionContent = nextH2 >= 0 ? afterHeading.substring(0, nextH2) : afterHeading;
296
+ const lines = sectionContent.split('\n');
297
+
298
+ let currentEvent = null;
299
+
300
+ for (const line of lines) {
301
+ // ### event_name header
302
+ const h3Match = line.match(/^###\s+(.+)/);
303
+ if (h3Match) {
304
+ currentEvent = h3Match[1].trim();
305
+ payloads[currentEvent] = [];
306
+ continue;
307
+ }
308
+
309
+ if (!currentEvent) continue;
310
+
311
+ // - field: type — description
312
+ const fieldMatch = line.match(/^-\s+(\w+):\s+([\w\[\]|]+)\s+[—-]+\s+(.+)/);
313
+ if (fieldMatch) {
314
+ payloads[currentEvent].push({
315
+ field: fieldMatch[1],
316
+ type: fieldMatch[2],
317
+ description: fieldMatch[3].trim(),
318
+ });
319
+ }
320
+ }
321
+
322
+ return payloads;
323
+ }
324
+
134
325
  // ---------------------------------------------------------------------------
135
326
  // Per-skill metadata extraction
136
327
  // ---------------------------------------------------------------------------
137
328
 
329
+ /**
330
+ * Required sections for the structured YAML contract.
331
+ * Used to generate warnings when sections are missing.
332
+ */
333
+ const REQUIRED_CONTRACT_SECTIONS = [
334
+ 'Requirements',
335
+ 'Risk',
336
+ 'Approval Gates',
337
+ 'Fallbacks',
338
+ 'Capabilities',
339
+ 'Event Payloads',
340
+ ];
341
+
138
342
  function processSkill({ skillFile, skillDir, triggerFile }) {
139
343
  const raw = fs.readFileSync(skillFile, 'utf8');
140
344
  const stat = fs.statSync(skillFile);
@@ -151,6 +355,24 @@ function processSkill({ skillFile, skillDir, triggerFile }) {
151
355
  const blackboardReads = extractBlackboardPaths(sections['Blackboard Read'] || []);
152
356
  const blackboardWrites = extractBlackboardPaths(sections['Blackboard Write'] || []);
153
357
 
358
+ // New structured contract sections
359
+ const requirements = parseRequirements(sections['Requirements'] || []);
360
+ const risk = parseRisk(sections['Risk'] || []);
361
+ const { gates: approval_gates, complexity_routing } = parseApprovalGates(
362
+ sections['Approval Gates'] || []
363
+ );
364
+ const fallbacks = parseFallbacks(sections['Fallbacks'] || []);
365
+ const capabilities = parseCapabilities(sections['Capabilities'] || []);
366
+ const event_payloads = parseEventPayloads(parsed.content);
367
+
368
+ // Warnings — flag any missing required contract sections
369
+ const warnings = [];
370
+ for (const section of REQUIRED_CONTRACT_SECTIONS) {
371
+ if (!sections[section]) {
372
+ warnings.push(`Missing required section: ## ${section}`);
373
+ }
374
+ }
375
+
154
376
  // References directory
155
377
  const referencesDir = path.join(ROOT, skillDir, 'references');
156
378
  let references = [];
@@ -178,8 +400,16 @@ function processSkill({ skillFile, skillDir, triggerFile }) {
178
400
  events_listens: eventsListens,
179
401
  blackboard_reads: blackboardReads,
180
402
  blackboard_writes: blackboardWrites,
403
+ requirements,
404
+ risk,
405
+ approval_gates,
406
+ complexity_routing,
407
+ fallbacks,
408
+ capabilities,
409
+ event_payloads,
181
410
  references,
182
411
  has_evals: hasEvals,
412
+ warnings,
183
413
  size_bytes: stat.size,
184
414
  enabled: true,
185
415
  };
@@ -196,15 +426,38 @@ function main() {
196
426
  // Sort alphabetically by name
197
427
  skills.sort((a, b) => a.name.localeCompare(b.name));
198
428
 
429
+ // Collect manifest-level warnings for skills with missing sections
430
+ const manifestWarnings = [];
431
+ for (const skill of skills) {
432
+ if (skill.warnings && skill.warnings.length > 0) {
433
+ manifestWarnings.push({
434
+ skill: skill.name,
435
+ warnings: skill.warnings,
436
+ });
437
+ }
438
+ }
439
+
199
440
  const manifest = {
200
441
  generated_at: new Date().toISOString(),
201
442
  skills,
443
+ warnings: manifestWarnings,
202
444
  };
203
445
 
204
446
  const outputPath = path.join(ROOT, 'ftm-manifest.json');
205
447
  fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
206
448
 
207
449
  process.stderr.write(`Generated manifest for ${skills.length} skills\n`);
450
+
451
+ if (manifestWarnings.length > 0) {
452
+ process.stderr.write(
453
+ `Warnings: ${manifestWarnings.length} skill(s) missing required contract sections\n`
454
+ );
455
+ for (const w of manifestWarnings) {
456
+ process.stderr.write(` ${w.skill}: ${w.warnings.join(', ')}\n`);
457
+ }
458
+ } else {
459
+ process.stderr.write(`All ${skills.length} skills have complete contract sections\n`);
460
+ }
208
461
  }
209
462
 
210
463
  main();