@entelligentsia/forgecli 0.11.2 → 0.11.3

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 (38) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/CHANGELOG-forge-plugin.md +27 -0
  3. package/dist/bin/forge.js +0 -0
  4. package/dist/extensions/forgecli/ask-user-tool.js +32 -20
  5. package/dist/extensions/forgecli/ask-user-tool.js.map +1 -1
  6. package/dist/extensions/forgecli/config-layer.js +4 -1
  7. package/dist/extensions/forgecli/config-layer.js.map +1 -1
  8. package/dist/extensions/forgecli/config-writer.js +4 -1
  9. package/dist/extensions/forgecli/config-writer.js.map +1 -1
  10. package/dist/extensions/forgecli/fix-bug.js +31 -1
  11. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  12. package/dist/extensions/forgecli/health-check.d.ts +10 -0
  13. package/dist/extensions/forgecli/health-check.js +160 -8
  14. package/dist/extensions/forgecli/health-check.js.map +1 -1
  15. package/dist/extensions/forgecli/hook-dispatcher.js +4 -2
  16. package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
  17. package/dist/extensions/forgecli/hooks/write-guard.js +5 -1
  18. package/dist/extensions/forgecli/hooks/write-guard.js.map +1 -1
  19. package/dist/extensions/forgecli/lib/store-error-remediation.d.ts +65 -0
  20. package/dist/extensions/forgecli/lib/store-error-remediation.js +298 -0
  21. package/dist/extensions/forgecli/lib/store-error-remediation.js.map +1 -0
  22. package/dist/extensions/forgecli/run-sprint.js +16 -1
  23. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  24. package/dist/extensions/forgecli/run-task.js +30 -8
  25. package/dist/extensions/forgecli/run-task.js.map +1 -1
  26. package/dist/extensions/forgecli/store-resolver.d.ts +18 -0
  27. package/dist/extensions/forgecli/store-resolver.js +44 -4
  28. package/dist/extensions/forgecli/store-resolver.js.map +1 -1
  29. package/dist/extensions/forgecli/store-validator.d.ts +3 -0
  30. package/dist/extensions/forgecli/store-validator.js +4 -2
  31. package/dist/extensions/forgecli/store-validator.js.map +1 -1
  32. package/dist/forge-payload/.base-pack/personas/supervisor.md +9 -0
  33. package/dist/forge-payload/.base-pack/workflows/enhance.md +17 -11
  34. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  35. package/dist/forge-payload/.schemas/migrations.json +16 -0
  36. package/dist/forge-payload/meta/workflows/meta-enhance.md +17 -11
  37. package/dist/forge-payload/tools/build-persona-pack.cjs +120 -11
  38. package/package.json +1 -1
@@ -22,6 +22,99 @@ const fs = require('fs');
22
22
  const path = require('path');
23
23
  const crypto = require('crypto');
24
24
 
25
+ // ── Persona parser — two variants: base-pack (no frontmatter) and meta (YAML frontmatter) ──
26
+ //
27
+ // Base-pack personas: first line is a markdown heading, e.g. "# Architect" or
28
+ // "# Architect — subtitle". The rest is the body. id and role are derived from
29
+ // the filename. summary is the first sentence of the first non-heading line.
30
+ // responsibilities and outputs are derived from section headers in the body.
31
+ // file_ref is derived from the file path.
32
+ //
33
+ // Meta personas: YAML frontmatter block (--- ... ---) containing all required
34
+ // fields. The body is ignored.
35
+
36
+ function parsePersona(content, filePath) {
37
+ if (content.startsWith('---')) {
38
+ // Meta format — parse frontmatter then ignore body
39
+ const fm = parseFrontmatter(content, filePath);
40
+ // Re-validate required fields for meta personas
41
+ for (const field of REQUIRED_PERSONA_FIELDS) {
42
+ if (!(field in fm)) {
43
+ throw new Error(`${filePath}: frontmatter missing required field '${field}'`);
44
+ }
45
+ }
46
+ return fm;
47
+ }
48
+
49
+ // Base-pack format — no frontmatter, derive fields from content
50
+ const lines = content.split(/\r?\n/);
51
+ const fileName = path.basename(filePath, '.md'); // e.g. "architect"
52
+
53
+ // Derive id and role from filename (e.g. "meta-architect.md" → "architect", "architect.md" → "architect")
54
+ const id = fileName.replace(/^meta-/, '').replace(/-skills$/, '');
55
+ const role = id;
56
+
57
+ // Extract summary: first non-heading, non-empty line
58
+ let summary = '';
59
+ for (const line of lines) {
60
+ const trimmed = line.trim();
61
+ if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('{{')) {
62
+ // Grab first sentence (up to first period + space or first line break)
63
+ const sentenceEnd = trimmed.search(/\.\s/);
64
+ summary = sentenceEnd >= 0 ? trimmed.slice(0, sentenceEnd + 1) : trimmed;
65
+ break;
66
+ }
67
+ }
68
+
69
+ // Derive responsibilities and outputs from section headers
70
+ const responsibilities = [];
71
+ const outputs = [];
72
+ for (const line of lines) {
73
+ if (/^##\s+(What I Produce|Outputs?)/i.test(line)) {
74
+ // Collect bullet points under this header until next ## or end
75
+ let i = lines.indexOf(line) + 1;
76
+ while (i < lines.length && !lines[i].startsWith('##')) {
77
+ const m = lines[i].match(/^[-*]\s+(.+)/);
78
+ if (m) outputs.push(m[1].trim());
79
+ i++;
80
+ }
81
+ }
82
+ if (/^##\s+(Capabilities?)/i.test(line)) {
83
+ let i = lines.indexOf(line) + 1;
84
+ while (i < lines.length && !lines[i].startsWith('##')) {
85
+ const m = lines[i].match(/^[-*]\s+(.+)/);
86
+ if (m) responsibilities.push(m[1].trim());
87
+ i++;
88
+ }
89
+ }
90
+ }
91
+
92
+ // file_ref: base-pack personas become .forge/personas/<id>.md
93
+ const file_ref = `.forge/personas/${id}.md`;
94
+
95
+ return { id, role, summary, responsibilities, outputs, file_ref };
96
+ }
97
+
98
+ // ── Skill parser — base-pack skills (with frontmatter, no file_ref) and meta skills (with file_ref) ──
99
+
100
+ function parseSkill(content, filePath) {
101
+ const fm = parseFrontmatter(content, filePath);
102
+
103
+ // Skills must have: id, applies_to, summary, capabilities
104
+ for (const field of ['id', 'applies_to', 'summary', 'capabilities']) {
105
+ if (!(field in fm)) {
106
+ throw new Error(`${filePath}: frontmatter missing required field '${field}'`);
107
+ }
108
+ }
109
+
110
+ // file_ref is present in meta skills; derive it for base-pack skills from the path
111
+ if (!fm.file_ref) {
112
+ fm.file_ref = `.forge/skills/${path.basename(filePath)}`;
113
+ }
114
+
115
+ return fm;
116
+ }
117
+
25
118
  // ── YAML frontmatter parser ──────────────────────────────────────────────────
26
119
  // Narrow-scope parser: handles scalars, folded scalars (`>`), block lists
27
120
  // (`- item`) under a key, and inline flow lists (`[a, b]`). Anything else
@@ -123,30 +216,44 @@ function listMarkdown(dir) {
123
216
  .map((f) => path.join(dir, f));
124
217
  }
125
218
 
126
- function loadEntries(dir, requiredFields) {
219
+ function loadPersonas(dir) {
127
220
  const entries = {};
128
221
  for (const filePath of listMarkdown(dir)) {
129
222
  const content = fs.readFileSync(filePath, 'utf8');
130
- let fm;
131
223
  try {
132
- fm = parseFrontmatter(content, filePath);
224
+ const persona = parsePersona(content, filePath);
225
+ entries[persona.id] = persona;
133
226
  } catch (err) {
134
- // Re-throw with original path-bearing message intact.
135
227
  throw err;
136
228
  }
137
- for (const field of requiredFields) {
138
- if (!(field in fm)) {
139
- throw new Error(`${filePath}: frontmatter missing required field '${field}'`);
140
- }
229
+ }
230
+ return entries;
231
+ }
232
+
233
+ function loadSkills(dir) {
234
+ const entries = {};
235
+ for (const filePath of listMarkdown(dir)) {
236
+ const content = fs.readFileSync(filePath, 'utf8');
237
+ try {
238
+ const skill = parseSkill(content, filePath);
239
+ entries[skill.id] = skill;
240
+ } catch (err) {
241
+ throw err;
141
242
  }
142
- entries[fm.id] = fm;
143
243
  }
144
244
  return entries;
145
245
  }
146
246
 
147
247
  function buildPack({ personaDir, skillDir }) {
148
- const personas = loadEntries(personaDir, REQUIRED_PERSONA_FIELDS);
149
- const skills = loadEntries(skillDir, REQUIRED_SKILL_FIELDS);
248
+ const personas = loadPersonas(personaDir);
249
+ const skills = loadSkills(skillDir);
250
+
251
+ const personaCount = Object.keys(personas).length;
252
+ const skillCount = Object.keys(skills).length;
253
+ if (personaCount === 0 && skillCount === 0) {
254
+ throw new Error('no personas or skills found; refusing to write empty pack');
255
+ }
256
+
150
257
  return {
151
258
  version: 1,
152
259
  built_at: new Date().toISOString(),
@@ -220,6 +327,8 @@ if (require.main === module) {
220
327
 
221
328
  module.exports = {
222
329
  parseFrontmatter,
330
+ parsePersona,
331
+ parseSkill,
223
332
  buildPack,
224
333
  computeSourceHash,
225
334
  writePack,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entelligentsia/forgecli",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "Forge SDLC ported onto @earendil-works/pi-coding-agent — production launcher with three bin aliases (forge/forgecli/4ge). Bundles a curated fork of pi-coding-agent vendored under earendil-works names.",
5
5
  "license": "MIT",
6
6
  "author": "Entelligentsia",