@entelligentsia/forgecli 0.8.4 → 0.9.1

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 (170) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +165 -2
  3. package/dist/bin/argv.d.ts +2 -2
  4. package/dist/bin/argv.js +17 -0
  5. package/dist/bin/argv.js.map +1 -1
  6. package/dist/bin/config.d.ts +69 -0
  7. package/dist/bin/config.js +315 -0
  8. package/dist/bin/config.js.map +1 -0
  9. package/dist/bin/doctor.d.ts +1 -0
  10. package/dist/bin/doctor.js +12 -0
  11. package/dist/bin/doctor.js.map +1 -1
  12. package/dist/bin/forge.js +7 -0
  13. package/dist/bin/forge.js.map +1 -1
  14. package/dist/extensions/forgecli/config-command.d.ts +8 -0
  15. package/dist/extensions/forgecli/config-command.js +66 -0
  16. package/dist/extensions/forgecli/config-command.js.map +1 -0
  17. package/dist/extensions/forgecli/config-layer.d.ts +38 -0
  18. package/dist/extensions/forgecli/config-layer.js +68 -0
  19. package/dist/extensions/forgecli/config-layer.js.map +1 -0
  20. package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
  21. package/dist/extensions/forgecli/config-tui/component.js +236 -0
  22. package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
  23. package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
  24. package/dist/extensions/forgecli/config-tui/handler.js +240 -0
  25. package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
  26. package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
  27. package/dist/extensions/forgecli/config-tui/index.js +5 -0
  28. package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
  29. package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
  30. package/dist/extensions/forgecli/config-tui/keys.js +33 -0
  31. package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
  32. package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
  33. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
  34. package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
  35. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
  36. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
  37. package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
  38. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
  39. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
  40. package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
  41. package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
  42. package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
  43. package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
  44. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
  45. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
  46. package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
  47. package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
  48. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
  49. package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
  50. package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
  51. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
  52. package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
  53. package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
  54. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
  55. package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
  56. package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
  57. package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
  58. package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
  59. package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
  60. package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
  61. package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
  62. package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
  63. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
  64. package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
  65. package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
  66. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
  67. package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
  68. package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
  69. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
  70. package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
  71. package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
  72. package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
  73. package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
  74. package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
  75. package/dist/extensions/forgecli/config-tui/screens.js +78 -0
  76. package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
  77. package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
  78. package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
  79. package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
  80. package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
  81. package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
  82. package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
  83. package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
  84. package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
  85. package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
  86. package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
  87. package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
  88. package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
  89. package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
  90. package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
  91. package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
  92. package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
  93. package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
  94. package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
  95. package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
  96. package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
  97. package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
  98. package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
  99. package/dist/extensions/forgecli/config-tui/state.js +11 -0
  100. package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
  101. package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
  102. package/dist/extensions/forgecli/config-tui/theme.js +88 -0
  103. package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
  104. package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
  105. package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
  106. package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
  107. package/dist/extensions/forgecli/config-writer.d.ts +16 -0
  108. package/dist/extensions/forgecli/config-writer.js +63 -0
  109. package/dist/extensions/forgecli/config-writer.js.map +1 -0
  110. package/dist/extensions/forgecli/fix-bug.js +85 -1
  111. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  112. package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
  113. package/dist/extensions/forgecli/forge-commands.js +3 -8
  114. package/dist/extensions/forgecli/forge-commands.js.map +1 -1
  115. package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
  116. package/dist/extensions/forgecli/forge-subagent.js +19 -0
  117. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  118. package/dist/extensions/forgecli/index.js +16 -0
  119. package/dist/extensions/forgecli/index.js.map +1 -1
  120. package/dist/extensions/forgecli/input-router.d.ts +33 -0
  121. package/dist/extensions/forgecli/input-router.js +133 -0
  122. package/dist/extensions/forgecli/input-router.js.map +1 -0
  123. package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
  124. package/dist/extensions/forgecli/model-resolver.js +65 -0
  125. package/dist/extensions/forgecli/model-resolver.js.map +1 -0
  126. package/dist/extensions/forgecli/model-validator.d.ts +29 -0
  127. package/dist/extensions/forgecli/model-validator.js +107 -0
  128. package/dist/extensions/forgecli/model-validator.js.map +1 -0
  129. package/dist/extensions/forgecli/run-sprint.js +59 -0
  130. package/dist/extensions/forgecli/run-sprint.js.map +1 -1
  131. package/dist/extensions/forgecli/run-task.js +93 -1
  132. package/dist/extensions/forgecli/run-task.js.map +1 -1
  133. package/dist/extensions/forgecli/thread-switcher.js +5 -2
  134. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  135. package/dist/extensions/forgecli/whats-new-widget.js +5 -2
  136. package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
  137. package/package.json +11 -3
  138. package/dist/extensions/forgecli/review-command.d.ts +0 -2
  139. package/dist/extensions/forgecli/review-command.js +0 -184
  140. package/dist/extensions/forgecli/review-command.js.map +0 -1
  141. package/dist/forge-payload/.tools/banners.cjs +0 -435
  142. package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
  143. package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
  144. package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
  145. package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
  146. package/dist/forge-payload/.tools/collate.cjs +0 -1041
  147. package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
  148. package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
  149. package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
  150. package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
  151. package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
  152. package/dist/forge-payload/.tools/lib/result.js +0 -40
  153. package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
  154. package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
  155. package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
  156. package/dist/forge-payload/.tools/lib/validate.js +0 -141
  157. package/dist/forge-payload/.tools/manage-config.cjs +0 -340
  158. package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
  159. package/dist/forge-payload/.tools/package.json +0 -3
  160. package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
  161. package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
  162. package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
  163. package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
  164. package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
  165. package/dist/forge-payload/.tools/seed-store.cjs +0 -237
  166. package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
  167. package/dist/forge-payload/.tools/store-query.cjs +0 -319
  168. package/dist/forge-payload/.tools/store.cjs +0 -315
  169. package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
  170. package/dist/forge-payload/.tools/validate-store.cjs +0 -593
@@ -1,319 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- // store-query.cjs — Forge store query engine CLI
4
- // Implements exact-args, keyword, and NLP intent paths.
5
- // Spawned by store-cli.cjs query/nlp/schema dispatch.
6
-
7
- const path = require('path');
8
-
9
- process.on('uncaughtException', (e) => {
10
- process.stderr.write(`Error: ${e.message}\n`);
11
- process.exit(1);
12
- });
13
-
14
- const { StoreFacade, loadForgeConfig, resetConfigCache } = require('./lib/store-facade.cjs');
15
- const { parseIntentNLP, ENTITY_SYNONYMS, STATUS_MAP } = require('./lib/store-nlp.cjs');
16
- const { executeQuery, buildResult, kwMatches } = require('./lib/store-query-exec.cjs');
17
-
18
- // ── Argument parser ───────────────────────────────────────────────────────────
19
-
20
- function parseArgs(argv) {
21
- const args = { _raw: argv.join(' ') };
22
- const intentWords = [];
23
- let i = 0;
24
- while (i < argv.length) {
25
- const a = argv[i];
26
- switch (a) {
27
- case '--sprint': args.sprint = argv[++i]; break;
28
- case '--task': args.task = argv[++i]; break;
29
- case '--bug': args.bug = argv[++i]; break;
30
- case '--feature': args.feature = argv[++i]; break;
31
- case '--status': args.status = argv[++i]; break;
32
- case '--keyword': args.keyword = argv[++i]; break;
33
- case '--type': args.type = argv[++i]; break;
34
- case '--mode': args.mode = argv[++i]; break;
35
- case '--with-blockers': args.withBlockers = true; break;
36
- case '--with-blocked-tasks':args.withBlockedTasks = true; break;
37
- case '--with-sprint': args.withSprint = true; break;
38
- case '--with-feature': args.withFeature = true; break;
39
- case '--no-excerpts': args.noExcerpts = true; break;
40
- case '--list-sprints': args.listSprints = true; break;
41
- case '--help': case '-h': args.help = true; break;
42
- default:
43
- if (!a.startsWith('-')) intentWords.push(a);
44
- break;
45
- }
46
- i++;
47
- }
48
- if (intentWords.length > 0) args.intent = intentWords.join(' ');
49
- return args;
50
- }
51
-
52
- function printHelp() {
53
- process.stdout.write(`Usage: store-query <command> [options]
54
-
55
- Commands:
56
- query Query the Forge store (exact args or NLP intent)
57
- nlp Query the Forge store using NLP intent parser
58
- schema Dump project schema and grammar reference
59
-
60
- Query options:
61
- --sprint <id> List tasks/bugs for a sprint
62
- --task <id> Get task details
63
- --bug <id> Get bug details
64
- --feature <id> Get feature + related tasks
65
- --list-sprints List all sprints
66
- --status <status> Filter by status
67
- --keyword <term> Search entity titles
68
- --type <entity> Limit --keyword to sprints|tasks|bugs|features
69
- --with-blockers Follow blockedBy FK on tasks
70
- --with-blocked-tasks Follow blocksTask FK on bugs
71
- --with-sprint Follow sprintId FK
72
- --with-feature Follow featureId FK
73
- --no-excerpts Omit INDEX.md excerpts
74
- --mode strict|nlp|off Engine mode (default: nlp for intent, strict for flags)
75
- `);
76
- }
77
-
78
- // ── Exact-args query path ─────────────────────────────────────────────────────
79
-
80
- function cmdQueryExact(args, store, cfg) {
81
- const trace = [];
82
- const results = [];
83
- const relatedFiles = [];
84
- const includeExcerpts = !args.noExcerpts;
85
- const filter = {};
86
- if (args.status) filter.status = args.status;
87
-
88
- if (args.sprint === 'all' || args.listSprints) {
89
- const sprints = store.listSprints(filter);
90
- trace.push(`listed all sprints: ${sprints.length}`);
91
- for (const s of sprints) results.push(buildResult(s, 'sprint', store, includeExcerpts));
92
- } else if (args.sprint) {
93
- const sprint = store.getEntity('sprints', args.sprint);
94
- const tasks = store.listTasks({ sprintId: args.sprint, ...filter });
95
- const bugs = store.listBugs({ sprintId: args.sprint, ...filter });
96
- trace.push(`sprint ${args.sprint}: ${tasks.length} tasks, ${bugs.length} bugs`);
97
- if (sprint) results.push(buildResult(sprint, 'sprint', store, includeExcerpts));
98
- for (const t of tasks) {
99
- results.push(buildResult(t, 'task', store, includeExcerpts));
100
- if (args.withBlockers && t.blockedBy) {
101
- for (const bugId of (Array.isArray(t.blockedBy) ? t.blockedBy : [t.blockedBy])) {
102
- const bug = store.getEntity('bugs', bugId);
103
- if (bug) { results.push(buildResult(bug, 'bug', store, includeExcerpts)); trace.push(`followed blockedBy → ${bugId}`); }
104
- }
105
- }
106
- }
107
- for (const b of bugs) results.push(buildResult(b, 'bug', store, includeExcerpts));
108
- } else if (args.task) {
109
- const task = store.getEntity('tasks', args.task);
110
- if (task) {
111
- results.push(buildResult(task, 'task', store, includeExcerpts));
112
- if (args.withSprint && task.sprintId) {
113
- const s = store.getEntity('sprints', task.sprintId);
114
- if (s) { results.push(buildResult(s, 'sprint', store, includeExcerpts)); trace.push(`followed sprintId → ${task.sprintId}`); }
115
- }
116
- if (args.withFeature && (task.featureId || task.feature_id)) {
117
- const fid = task.featureId || task.feature_id;
118
- const f = store.getEntity('features', fid);
119
- if (f) { results.push(buildResult(f, 'feature', store, includeExcerpts)); trace.push(`followed featureId → ${fid}`); }
120
- }
121
- trace.push(`task ${args.task} found`);
122
- } else {
123
- trace.push(`task ${args.task} not found`);
124
- }
125
- } else if (args.bug) {
126
- const bug = store.getEntity('bugs', args.bug);
127
- if (bug) {
128
- results.push(buildResult(bug, 'bug', store, includeExcerpts));
129
- if (args.withBlockedTasks && bug.blocksTask) {
130
- for (const tid of (Array.isArray(bug.blocksTask) ? bug.blocksTask : [bug.blocksTask])) {
131
- const t = store.getEntity('tasks', tid);
132
- if (t) { results.push(buildResult(t, 'task', store, includeExcerpts)); trace.push(`followed blocksTask → ${tid}`); }
133
- }
134
- }
135
- trace.push(`bug ${args.bug} found`);
136
- } else {
137
- trace.push(`bug ${args.bug} not found`);
138
- }
139
- } else if (args.feature) {
140
- const feat = store.getEntity('features', args.feature);
141
- if (feat) {
142
- results.push(buildResult(feat, 'feature', store, includeExcerpts));
143
- const tasks = store.listTasks({ featureId: args.feature, ...filter });
144
- trace.push(`feature ${args.feature}: ${tasks.length} tasks`);
145
- for (const t of tasks) results.push(buildResult(t, 'task', store, includeExcerpts));
146
- } else {
147
- trace.push(`feature ${args.feature} not found`);
148
- }
149
- }
150
-
151
- for (const r of results) {
152
- if (r.fileRefs?.md) relatedFiles.push(r.fileRefs.md);
153
- if (r.fileRefs?.json) relatedFiles.push(r.fileRefs.json);
154
- }
155
-
156
- return { query: args._raw, path: 'exact', traversalTrace: trace, results, relatedFileRefs: [...new Set(relatedFiles)], config: { store: cfg.storePathRel, engineering: cfg.kbPath || 'engineering' } };
157
- }
158
-
159
- // ── Keyword search path ───────────────────────────────────────────────────────
160
-
161
- function cmdKeywordSearch(args, store, cfg) {
162
- const trace = [];
163
- const results = [];
164
- const relatedFiles = [];
165
- const includeExcerpts = !args.noExcerpts;
166
- const keyword = args.keyword;
167
- const targetType = args.type || null;
168
-
169
- const entityTypes = targetType
170
- ? [targetType]
171
- : ['sprints', 'tasks', 'bugs', 'features'];
172
- const singMap = { sprints: 'sprint', tasks: 'task', bugs: 'bug', features: 'feature' };
173
-
174
- for (const plural of entityTypes) {
175
- const singular = singMap[plural];
176
- let entities;
177
- switch (plural) {
178
- case 'sprints': entities = store.listSprints(); break;
179
- case 'tasks': entities = store.listTasks(); break;
180
- case 'bugs': entities = store.listBugs(); break;
181
- case 'features': entities = store.listFeatures(); break;
182
- default: entities = [];
183
- }
184
- const before = entities.length;
185
- const matched = entities.filter(e => kwMatches(e.title || '', keyword));
186
- if (matched.length > 0) {
187
- trace.push(`keyword "${keyword}" in ${plural}: ${before} → ${matched.length}`);
188
- for (const m of matched) results.push(buildResult(m, singular, store, includeExcerpts));
189
- }
190
- }
191
-
192
- for (const r of results) {
193
- if (r.fileRefs?.md) relatedFiles.push(r.fileRefs.md);
194
- if (r.fileRefs?.json) relatedFiles.push(r.fileRefs.json);
195
- }
196
-
197
- return { query: args._raw, path: 'keyword', traversalTrace: trace, results, relatedFileRefs: [...new Set(relatedFiles)], config: { store: cfg.storePathRel, engineering: cfg.kbPath || 'engineering' } };
198
- }
199
-
200
- // ── Schema command ────────────────────────────────────────────────────────────
201
-
202
- function cmdSchema(cfg) {
203
- const { buildFieldValidators } = require('./lib/store-query-exec.cjs');
204
- // build validators inline since not exported — rebuild here
205
- const p = cfg.prefix;
206
- const esc = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
207
- const fv = {
208
- sprints: { sprintId: /^S\d+$/, status: ['planning','active','completed','retrospective-done','blocked','partially-completed','abandoned'] },
209
- tasks: { taskId: new RegExp(`^${esc}-S\\d+-T\\d+$`), sprintId: /^S\d+$/, featureId: /^FEAT-\d+$/, status: ['draft','planned','plan-approved','implementing','implemented','review-approved','approved','committed','plan-revision-required','code-revision-required','blocked','escalated','abandoned'] },
210
- bugs: { bugId: new RegExp(`^${esc}-BUG-\\d+$`), sprintId: /^S\d+$/, severity: ['critical','major','minor'], status: ['reported','triaged','in-progress','fixed','verified'] },
211
- features: { featureId: /^FEAT-\d+$/, status: ['active','draft','shipped','retired'] },
212
- };
213
- const toEnum = spec => Array.isArray(spec) ? spec : (spec instanceof RegExp ? `pattern: ${spec}` : null);
214
-
215
- return {
216
- project: { prefix: cfg.prefix, name: cfg.projectName, kbPath: cfg.kbPath, storePath: cfg.storePathRel },
217
- entities: {
218
- sprints: { idField: 'sprintId', idPattern: fv.sprints.sprintId.toString(), status: toEnum(fv.sprints.status), fks: [] },
219
- tasks: { idField: 'taskId', idPattern: fv.tasks.taskId.toString(), status: toEnum(fv.tasks.status), fks: ['sprintId','featureId','blockedBy'] },
220
- bugs: { idField: 'bugId', idPattern: fv.bugs.bugId.toString(), status: toEnum(fv.bugs.status), severity: toEnum(fv.bugs.severity), fks: ['sprintId','blocksTask'] },
221
- features: { idField: 'featureId', idPattern: fv.features.featureId.toString(),status: toEnum(fv.features.status), fks: [] },
222
- },
223
- entitySynonyms: ENTITY_SYNONYMS,
224
- statusSynonyms: STATUS_MAP,
225
- grammar: {
226
- recency: ['last','latest','newest','recent','most recent','oldest','earliest','first'],
227
- limit: ['top N','first N','last N'],
228
- count: ['how many','count of','number of','count'],
229
- fkPhrases: ['with sprint','with feature','blocking','blocked','which sprint','sprint for','feature for'],
230
- },
231
- };
232
- }
233
-
234
- // ── Main ──────────────────────────────────────────────────────────────────────
235
-
236
- function main() {
237
- const argv = process.argv.slice(2);
238
- const startMs = Date.now();
239
-
240
- if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
241
- printHelp();
242
- process.exit(0);
243
- }
244
-
245
- const command = argv[0];
246
- const rest = argv.slice(1);
247
- const args = parseArgs(rest);
248
-
249
- if (args.help) { printHelp(); process.exit(0); }
250
-
251
- const cfg = loadForgeConfig();
252
- const store = new StoreFacade(cfg.storePathAbs);
253
-
254
- let result;
255
-
256
- switch (command) {
257
- case 'schema': {
258
- result = cmdSchema(cfg);
259
- break;
260
- }
261
- case 'nlp': {
262
- if (!args.intent) {
263
- process.stderr.write('Usage: store-query nlp "<natural language query>"\n');
264
- process.exit(1);
265
- }
266
- const plan = parseIntentNLP(args.intent);
267
- result = executeQuery(plan, store, { query: args.intent, noExcerpts: args.noExcerpts });
268
- result.query = args.intent;
269
- result.path = 'intent-nlp';
270
- result.config = { store: cfg.storePathRel, engineering: cfg.kbPath || 'engineering' };
271
- break;
272
- }
273
- case 'query': {
274
- const mode = args.mode || 'auto';
275
- const hasExactFlags = args.sprint || args.task || args.bug || args.feature || args.listSprints;
276
- const hasKeyword = !!args.keyword;
277
-
278
- if (mode === 'strict' || mode === 'off') {
279
- if (args.intent && !hasExactFlags && !hasKeyword) {
280
- process.stderr.write('Mode is strict — intent strings not accepted. Use --sprint/--task/--bug/--feature flags.\n');
281
- process.exit(1);
282
- }
283
- }
284
-
285
- if (hasExactFlags) {
286
- result = cmdQueryExact(args, store, cfg);
287
- } else if (hasKeyword) {
288
- result = cmdKeywordSearch(args, store, cfg);
289
- } else if (args.intent) {
290
- const plan = parseIntentNLP(args.intent);
291
- result = executeQuery(plan, store, { query: args.intent, noExcerpts: args.noExcerpts });
292
- result.query = args.intent;
293
- result.path = 'intent-nlp';
294
- result.config = { store: cfg.storePathRel, engineering: cfg.kbPath || 'engineering' };
295
- } else {
296
- process.stderr.write('Provide an entity flag (--sprint, --task, --bug, --feature) or an intent string.\n');
297
- process.exit(1);
298
- }
299
- break;
300
- }
301
- default: {
302
- process.stderr.write(`Unknown command: ${command}\n`);
303
- printHelp();
304
- process.exit(1);
305
- }
306
- }
307
-
308
- // Attach meta timing block
309
- const totalMs = Date.now() - startMs;
310
- result.meta = {
311
- mode: args.mode || 'auto',
312
- engineVersion: '1.0.0',
313
- totalTimeMs: totalMs,
314
- };
315
-
316
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
317
- }
318
-
319
- main();
@@ -1,315 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { findProjectRoot } = require('./lib/project-root.cjs');
6
-
7
- /**
8
- * Store Facade for Forge
9
- * Provides a backend-agnostic interface for CRUD operations on core store entities.
10
- */
11
- class Store {
12
- constructor(implementation) {
13
- this.impl = implementation;
14
- }
15
-
16
- // --- Sprints ---
17
- getSprint(id) { return this.impl.getSprint(id); }
18
- listSprints(filter) { return this.impl.listSprints(filter); }
19
- writeSprint(data) { return this.impl.writeSprint(data); }
20
- deleteSprint(id) { return this.impl.deleteSprint(id); }
21
-
22
- // --- Tasks ---
23
- getTask(id) { return this.impl.getTask(id); }
24
- listTasks(filter) { return this.impl.listTasks(filter); }
25
- writeTask(data) { return this.impl.writeTask(data); }
26
- deleteTask(id) { return this.impl.deleteTask(id); }
27
-
28
- // --- Bugs ---
29
- getBug(id) { return this.impl.getBug(id); }
30
- listBugs(filter) { return this.impl.listBugs(filter); }
31
- writeBug(data) { return this.impl.writeBug(data); }
32
- deleteBug(id) { return this.impl.deleteBug(id); }
33
-
34
- // --- Events ---
35
- getEvent(id, sprintId) { return this.impl.getEvent(id, sprintId); }
36
- listEvents(sprintId, filter) { return this.impl.listEvents(sprintId, filter); }
37
- writeEvent(sprintId, data) { return this.impl.writeEvent(sprintId, data); }
38
- deleteEvent(id, sprintId) { return this.impl.deleteEvent(id, sprintId); }
39
- renameEvent(sprintId, oldFilename, newEventId) { return this.impl.renameEvent(sprintId, oldFilename, newEventId); }
40
-
41
- // --- Features ---
42
- getFeature(id) { return this.impl.getFeature(id); }
43
- listFeatures(filter) { return this.impl.listFeatures(filter); }
44
- writeFeature(data) { return this.impl.writeFeature(data); }
45
- deleteFeature(id) { return this.impl.deleteFeature(id); }
46
-
47
- // --- Collation State ---
48
- writeCollationState(data) { return this.impl.writeCollationState(data); }
49
- readCollationState() { return this.impl.readCollationState(); }
50
-
51
- // --- Event Operations (extended) ---
52
- /**
53
- * Purge all event files for a sprint directory.
54
- * @param {string} sprintId
55
- * @param {{ dryRun?: boolean }} opts - dryRun: return file list without deleting
56
- * @returns {{ purged: boolean, fileCount: number, files: string[] }}
57
- */
58
- purgeEvents(sprintId, opts) { return this.impl.purgeEvents(sprintId, opts); }
59
- /**
60
- * List event filenames for a sprint directory.
61
- * @param {string} sprintId
62
- * @returns {{ filename: string, id: string }[]}
63
- */
64
- listEventFilenames(sprintId) { return this.impl.listEventFilenames(sprintId); }
65
- }
66
-
67
- /**
68
- * Filesystem Implementation of the Store facade.
69
- * Manages JSON flat-files in the .forge/store directory.
70
- */
71
- class FSImpl {
72
- constructor(configPath = '.forge/config.json') {
73
- this.configPath = configPath;
74
- this.storeRoot = this._resolveStoreRoot();
75
- }
76
-
77
- _resolveStoreRoot() {
78
- const configPathIsAbsolute = path.isAbsolute(this.configPath);
79
- const projectRoot = configPathIsAbsolute ? null : findProjectRoot();
80
- try {
81
- const resolved = projectRoot ? path.join(projectRoot, this.configPath) : this.configPath;
82
- const config = JSON.parse(fs.readFileSync(resolved, 'utf8'));
83
- const storePath = config.paths.store;
84
- return path.isAbsolute(storePath) ? storePath
85
- : projectRoot ? path.join(projectRoot, storePath) : storePath;
86
- } catch (e) {
87
- // Fallback to default if config is missing or corrupt
88
- return projectRoot ? path.join(projectRoot, '.forge', 'store') : '.forge/store';
89
- }
90
- }
91
-
92
- _getPath(entity, id) {
93
- const entityMap = {
94
- sprint: 'sprints',
95
- task: 'tasks',
96
- bug: 'bugs',
97
- event: 'events',
98
- feature: 'features'
99
- };
100
- const dir = entityMap[entity];
101
- if (!dir) throw new Error(`Unknown entity type: ${entity}`);
102
- return path.join(this.storeRoot, dir, `${id}.json`);
103
- }
104
-
105
- _readJson(filePath) {
106
- if (!fs.existsSync(filePath)) return null;
107
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
108
- }
109
-
110
- _writeJson(filePath, data) {
111
- const dir = path.dirname(filePath);
112
- if (!fs.existsSync(dir)) {
113
- fs.mkdirSync(dir, { recursive: true });
114
- }
115
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
116
- return data;
117
- }
118
-
119
- // Sprints
120
- getSprint(id) { return this._readJson(this._getPath('sprint', id)); }
121
- listSprints(filter) {
122
- const dir = path.join(this.storeRoot, 'sprints');
123
- if (!fs.existsSync(dir)) return [];
124
- return fs.readdirSync(dir)
125
- .filter(f => f.endsWith('.json'))
126
- .map(f => this._readJson(path.join(dir, f)))
127
- .filter(s => !s || (filter ? this._matches(s, filter) : true));
128
- }
129
- writeSprint(data) {
130
- return this._writeJson(this._getPath('sprint', data.sprintId), data);
131
- }
132
- deleteSprint(id) {
133
- const p = this._getPath('sprint', id);
134
- if (fs.existsSync(p)) fs.unlinkSync(p);
135
- }
136
-
137
- // Tasks
138
- getTask(id) { return this._readJson(this._getPath('task', id)); }
139
- listTasks(filter) {
140
- const dir = path.join(this.storeRoot, 'tasks');
141
- if (!fs.existsSync(dir)) return [];
142
- return fs.readdirSync(dir)
143
- .filter(f => f.endsWith('.json'))
144
- .map(f => this._readJson(path.join(dir, f)))
145
- .filter(t => !t || (filter ? this._matches(t, filter) : true));
146
- }
147
- writeTask(data) {
148
- return this._writeJson(this._getPath('task', data.taskId), data);
149
- }
150
- deleteTask(id) {
151
- const p = this._getPath('task', id);
152
- if (fs.existsSync(p)) fs.unlinkSync(p);
153
- }
154
-
155
- // Bugs
156
- getBug(id) { return this._readJson(this._getPath('bug', id)); }
157
- listBugs(filter) {
158
- const dir = path.join(this.storeRoot, 'bugs');
159
- if (!fs.existsSync(dir)) return [];
160
- return fs.readdirSync(dir)
161
- .filter(f => f.endsWith('.json'))
162
- .map(f => this._readJson(path.join(dir, f)))
163
- .filter(b => !b || (filter ? this._matches(b, filter) : true));
164
- }
165
- writeBug(data) {
166
- return this._writeJson(this._getPath('bug', data.bugId), data);
167
- }
168
- deleteBug(id) {
169
- const p = this._getPath('bug', id);
170
- if (fs.existsSync(p)) fs.unlinkSync(p);
171
- }
172
-
173
- // Events
174
- getEvent(id, sprintId) {
175
- const p = path.join(this.storeRoot, 'events', sprintId, `${id}.json`);
176
- return this._readJson(p);
177
- }
178
- listEvents(sprintId, filter) {
179
- const dir = path.join(this.storeRoot, 'events', sprintId);
180
- if (!fs.existsSync(dir)) return [];
181
- return fs.readdirSync(dir)
182
- .filter(f => f.endsWith('.json'))
183
- .map(f => this._readJson(path.join(dir, f)))
184
- .filter(e => !e || (filter ? this._matches(e, filter) : true));
185
- }
186
- /**
187
- * Find an event file whose internal eventId matches the given eventId,
188
- * but whose filename does not. Returns the mismatched filename (without
189
- * .json extension), or null if none found. Skips _-prefixed (ephemeral) files.
190
- */
191
- _findEventFileByContentId(sprintId, eventId) {
192
- const dir = path.join(this.storeRoot, 'events', sprintId);
193
- if (!fs.existsSync(dir)) return null;
194
- const files = fs.readdirSync(dir).filter(f => f.endsWith('.json') && !f.startsWith('_'));
195
- for (const file of files) {
196
- const filename = file.slice(0, -5); // strip .json
197
- if (filename === eventId) continue; // already canonical
198
- const rec = this._readJson(path.join(dir, file));
199
- if (rec && rec.eventId === eventId) {
200
- return filename;
201
- }
202
- }
203
- return null;
204
- }
205
- /**
206
- * Rename an event file from oldFilename to match newEventId.
207
- * Throws if the target file already exists (collision).
208
- */
209
- renameEvent(sprintId, oldFilename, newEventId) {
210
- const dir = path.join(this.storeRoot, 'events', sprintId);
211
- const oldPath = path.join(dir, `${oldFilename}.json`);
212
- const newPath = path.join(dir, `${newEventId}.json`);
213
- if (oldPath === newPath) return; // no-op
214
- if (fs.existsSync(newPath)) {
215
- throw new Error(`Cannot rename event: target file already exists: ${newPath}`);
216
- }
217
- if (fs.existsSync(oldPath)) {
218
- fs.renameSync(oldPath, newPath);
219
- }
220
- }
221
- writeEvent(sprintId, data) {
222
- // Detect ghost file: an existing file whose content eventId matches but
223
- // whose filename does not. Rename it to the canonical name before writing.
224
- const ghostFilename = this._findEventFileByContentId(sprintId, data.eventId);
225
- if (ghostFilename !== null) {
226
- this.renameEvent(sprintId, ghostFilename, data.eventId);
227
- }
228
- const p = path.join(this.storeRoot, 'events', sprintId, `${data.eventId}.json`);
229
- return this._writeJson(p, data);
230
- }
231
- deleteEvent(id, sprintId) {
232
- const p = path.join(this.storeRoot, 'events', sprintId, `${id}.json`);
233
- if (fs.existsSync(p)) fs.unlinkSync(p);
234
- }
235
-
236
- // Features
237
- getFeature(id) { return this._readJson(this._getPath('feature', id)); }
238
- listFeatures(filter) {
239
- const dir = path.join(this.storeRoot, 'features');
240
- if (!fs.existsSync(dir)) return [];
241
- return fs.readdirSync(dir)
242
- .filter(f => f.endsWith('.json'))
243
- .map(f => this._readJson(path.join(dir, f)))
244
- .filter(f => !f || (filter ? this._matches(f, filter) : true));
245
- }
246
- writeFeature(data) {
247
- return this._writeJson(this._getPath('feature', data.feature_id), data);
248
- }
249
- deleteFeature(id) {
250
- const p = this._getPath('feature', id);
251
- if (fs.existsSync(p)) fs.unlinkSync(p);
252
- }
253
-
254
- // Collation State
255
- writeCollationState(data) {
256
- const filePath = path.join(this.storeRoot, 'COLLATION_STATE.json');
257
- return this._writeJson(filePath, data);
258
- }
259
- readCollationState() {
260
- const filePath = path.join(this.storeRoot, 'COLLATION_STATE.json');
261
- return this._readJson(filePath);
262
- }
263
-
264
- // Event Operations (extended)
265
- /**
266
- * Purge the events directory for a given sprint.
267
- * Includes a path-traversal guard: the resolved directory must remain
268
- * within the events base directory. Throws on escape attempt.
269
- * Note: fileCount reflects .json files only, but the directory is removed
270
- * entirely (including any non-.json files).
271
- */
272
- purgeEvents(sprintId, { dryRun = false } = {}) {
273
- const eventsBase = path.resolve(this.storeRoot, 'events');
274
- const eventsDir = path.resolve(eventsBase, sprintId);
275
- // Guard: resolved path must stay within the events base directory.
276
- if (!eventsDir.startsWith(eventsBase + path.sep) && eventsDir !== eventsBase) {
277
- throw new Error(`Resolved events path '${eventsDir}' escapes store root — aborting purge`);
278
- }
279
- if (!fs.existsSync(eventsDir)) {
280
- return { purged: false, fileCount: 0, files: [] };
281
- }
282
- const files = fs.readdirSync(eventsDir).filter(f => f.endsWith('.json'));
283
- if (dryRun) {
284
- return { purged: false, fileCount: files.length, files };
285
- }
286
- fs.rmSync(eventsDir, { recursive: true, force: true });
287
- return { purged: true, fileCount: files.length, files };
288
- }
289
-
290
- /**
291
- * List all event filenames for a sprint directory.
292
- * Returns { filename, id } objects for ALL .json files including
293
- * _-prefixed ephemeral sidecars. Callers filter internally.
294
- */
295
- listEventFilenames(sprintId) {
296
- const dir = path.join(this.storeRoot, 'events', sprintId);
297
- if (!fs.existsSync(dir)) return [];
298
- return fs.readdirSync(dir)
299
- .filter(f => f.endsWith('.json'))
300
- .map(f => ({
301
- filename: f,
302
- id: f.slice(0, -5) // strip .json extension
303
- }));
304
- }
305
-
306
- _matches(record, filter) {
307
- if (!filter) return true;
308
- return Object.entries(filter).every(([key, value]) => record[key] === value);
309
- }
310
- }
311
-
312
- // Export a singleton instance for the plugin, plus classes for testing
313
- module.exports = new Store(new FSImpl());
314
- module.exports.Store = Store;
315
- module.exports.FSImpl = FSImpl;