@nforma.ai/nforma 0.2.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 (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. package/templates/qgsd.json +49 -0
@@ -0,0 +1,560 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+
8
+ const TAG = '[qgsd-polyrepo]';
9
+ const POLYREPOS_DIR = path.join(os.homedir(), '.claude', 'polyrepos');
10
+ const MARKER_FILE = 'polyrepo.json';
11
+
12
+ /**
13
+ * Ensure ~/.claude/polyrepos/ directory exists
14
+ */
15
+ function ensurePolyreposDir() {
16
+ try {
17
+ fs.mkdirSync(POLYREPOS_DIR, { recursive: true });
18
+ } catch (err) {
19
+ console.error(`${TAG} Failed to create polyrepos directory:`, err.message);
20
+ throw err;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Load a group config from ~/.claude/polyrepos/<name>.json
26
+ * Returns parsed object or null if not found.
27
+ * Fail-open: if malformed JSON, log warning and return null.
28
+ */
29
+ function loadGroup(name) {
30
+ const filePath = path.join(POLYREPOS_DIR, `${name}.json`);
31
+ try {
32
+ if (!fs.existsSync(filePath)) {
33
+ return null;
34
+ }
35
+ const content = fs.readFileSync(filePath, 'utf8');
36
+ return JSON.parse(content);
37
+ } catch (err) {
38
+ if (err instanceof SyntaxError) {
39
+ console.error(`${TAG} Warning: malformed JSON in ${filePath}, returning null`);
40
+ return null;
41
+ }
42
+ throw err;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Save a group config to ~/.claude/polyrepos/<group.name>.json
48
+ */
49
+ function saveGroup(group) {
50
+ ensurePolyreposDir();
51
+ const filePath = path.join(POLYREPOS_DIR, `${group.name}.json`);
52
+ try {
53
+ fs.writeFileSync(filePath, JSON.stringify(group, null, 2), 'utf8');
54
+ } catch (err) {
55
+ console.error(`${TAG} Failed to save group:`, err.message);
56
+ throw err;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Write per-repo marker at <repoPath>/.planning/polyrepo.json
62
+ * Optional docs: { user?, developer?, examples? } — relative paths within the repo
63
+ */
64
+ function writeMarker(repoPath, name, role, docs) {
65
+ try {
66
+ const markerDir = path.join(repoPath, '.planning');
67
+ fs.mkdirSync(markerDir, { recursive: true });
68
+ const markerPath = path.join(markerDir, MARKER_FILE);
69
+ const marker = { name, role };
70
+ if (docs && Object.keys(docs).length > 0) {
71
+ marker.docs = docs;
72
+ }
73
+ fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2), 'utf8');
74
+ } catch (err) {
75
+ console.error(`${TAG} Failed to write marker:`, err.message);
76
+ throw err;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Read per-repo marker from <repoPath>/.planning/polyrepo.json
82
+ * Returns parsed object or null if not found/malformed.
83
+ */
84
+ function readMarker(repoPath) {
85
+ try {
86
+ const markerPath = path.join(repoPath, '.planning', MARKER_FILE);
87
+ if (!fs.existsSync(markerPath)) return null;
88
+ return JSON.parse(fs.readFileSync(markerPath, 'utf8'));
89
+ } catch (err) {
90
+ if (err instanceof SyntaxError) {
91
+ console.error(`${TAG} Warning: malformed marker at ${repoPath}`);
92
+ return null;
93
+ }
94
+ throw err;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Set docs paths on an existing per-repo marker.
100
+ * docs: { user?, developer?, examples? } — relative paths within the repo.
101
+ * Merges with existing docs (pass null value to remove a key).
102
+ */
103
+ function setDocs(repoPath, docs) {
104
+ const marker = readMarker(repoPath);
105
+ if (!marker) {
106
+ return { ok: false, error: `No polyrepo marker found at ${repoPath}/.planning/polyrepo.json` };
107
+ }
108
+ const merged = { ...(marker.docs || {}) };
109
+ for (const [key, val] of Object.entries(docs)) {
110
+ if (val === null) {
111
+ delete merged[key];
112
+ } else {
113
+ merged[key] = val;
114
+ }
115
+ }
116
+ marker.docs = Object.keys(merged).length > 0 ? merged : undefined;
117
+ try {
118
+ const markerPath = path.join(repoPath, '.planning', MARKER_FILE);
119
+ fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2), 'utf8');
120
+ return { ok: true, docs: merged };
121
+ } catch (err) {
122
+ return { ok: false, error: err.message };
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Remove per-repo marker at <repoPath>/.planning/polyrepo.json
128
+ * Fail-open: no error if file doesn't exist
129
+ */
130
+ function removeMarker(repoPath) {
131
+ try {
132
+ const markerPath = path.join(repoPath, '.planning', MARKER_FILE);
133
+ if (fs.existsSync(markerPath)) {
134
+ fs.unlinkSync(markerPath);
135
+ }
136
+ } catch (err) {
137
+ console.error(`${TAG} Warning: failed to remove marker:`, err.message);
138
+ // Fail-open: don't throw
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Validate group name: alphanumeric + hyphens, 1-50 chars, lowercase, start with alphanumeric
144
+ */
145
+ function validateGroupName(name) {
146
+ const nameRegex = /^[a-z0-9][a-z0-9-]*$/;
147
+ if (!name || typeof name !== 'string') {
148
+ return { ok: false, error: 'Name must be a non-empty string' };
149
+ }
150
+ if (name.length > 50) {
151
+ return { ok: false, error: 'Name must be 1-50 characters' };
152
+ }
153
+ if (!nameRegex.test(name)) {
154
+ return { ok: false, error: 'Name must start with alphanumeric and contain only lowercase alphanumeric and hyphens' };
155
+ }
156
+ return { ok: true };
157
+ }
158
+
159
+ /**
160
+ * Create a new polyrepo group with optional initial repos
161
+ * repos: array of { role, path, planning? }
162
+ * Empty repos array is valid (used by interactive create flow).
163
+ */
164
+ function createGroup(name, repos = []) {
165
+ // Validate name
166
+ const nameVal = validateGroupName(name);
167
+ if (!nameVal.ok) {
168
+ return nameVal;
169
+ }
170
+
171
+ // Check if group already exists
172
+ if (loadGroup(name) !== null) {
173
+ return { ok: false, error: `Group '${name}' already exists` };
174
+ }
175
+
176
+ // Validate repos
177
+ if (!Array.isArray(repos)) {
178
+ return { ok: false, error: 'Repos must be an array' };
179
+ }
180
+
181
+ const seenPaths = new Set();
182
+ for (const repo of repos) {
183
+ if (typeof repo.role !== 'string' || repo.role.length === 0) {
184
+ return { ok: false, error: 'Each repo must have a non-empty role string' };
185
+ }
186
+ if (typeof repo.path !== 'string' || !path.isAbsolute(repo.path)) {
187
+ return { ok: false, error: `Repo path must be absolute: ${repo.path}` };
188
+ }
189
+ if (!fs.existsSync(repo.path)) {
190
+ return { ok: false, error: `Repo path does not exist: ${repo.path}` };
191
+ }
192
+ if (!fs.statSync(repo.path).isDirectory()) {
193
+ return { ok: false, error: `Repo path is not a directory: ${repo.path}` };
194
+ }
195
+ if (seenPaths.has(repo.path)) {
196
+ return { ok: false, error: `Duplicate path in group: ${repo.path}` };
197
+ }
198
+ seenPaths.add(repo.path);
199
+ }
200
+
201
+ // Create group
202
+ const group = {
203
+ name,
204
+ repos: repos.map(r => ({
205
+ role: r.role,
206
+ path: r.path,
207
+ planning: r.planning !== false ? true : false
208
+ }))
209
+ };
210
+
211
+ try {
212
+ saveGroup(group);
213
+ // Write markers for repos with planning: true
214
+ for (const repo of group.repos) {
215
+ if (repo.planning) {
216
+ writeMarker(repo.path, name, repo.role);
217
+ }
218
+ }
219
+ return { ok: true, group };
220
+ } catch (err) {
221
+ return { ok: false, error: `Failed to create group: ${err.message}` };
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Add a repo to an existing group
227
+ */
228
+ function addRepo(groupName, repoPath, role, planning = true) {
229
+ // Load group
230
+ const group = loadGroup(groupName);
231
+ if (!group) {
232
+ return { ok: false, error: `Group '${groupName}' does not exist` };
233
+ }
234
+
235
+ // Validate repo path
236
+ if (typeof repoPath !== 'string' || !path.isAbsolute(repoPath)) {
237
+ return { ok: false, error: `Repo path must be absolute: ${repoPath}` };
238
+ }
239
+ if (!fs.existsSync(repoPath)) {
240
+ return { ok: false, error: `Repo path does not exist: ${repoPath}` };
241
+ }
242
+ if (!fs.statSync(repoPath).isDirectory()) {
243
+ return { ok: false, error: `Repo path is not a directory: ${repoPath}` };
244
+ }
245
+
246
+ // Check for duplicates
247
+ if (group.repos.some(r => r.path === repoPath)) {
248
+ return { ok: false, error: `Repo path already in group: ${repoPath}` };
249
+ }
250
+
251
+ // Validate role
252
+ if (typeof role !== 'string' || role.length === 0) {
253
+ return { ok: false, error: 'Role must be a non-empty string' };
254
+ }
255
+
256
+ // Add repo
257
+ try {
258
+ group.repos.push({
259
+ role,
260
+ path: repoPath,
261
+ planning: planning !== false ? true : false
262
+ });
263
+ saveGroup(group);
264
+ if (planning !== false) {
265
+ writeMarker(repoPath, groupName, role);
266
+ }
267
+ return { ok: true };
268
+ } catch (err) {
269
+ return { ok: false, error: `Failed to add repo: ${err.message}` };
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Remove a repo from a group
275
+ * If group becomes empty, delete the group config file entirely.
276
+ */
277
+ function removeRepo(groupName, repoPath) {
278
+ // Load group
279
+ const group = loadGroup(groupName);
280
+ if (!group) {
281
+ return { ok: false, error: `Group '${groupName}' does not exist` };
282
+ }
283
+
284
+ // Find and remove repo
285
+ const initialLength = group.repos.length;
286
+ group.repos = group.repos.filter(r => r.path !== repoPath);
287
+
288
+ if (group.repos.length === initialLength) {
289
+ return { ok: false, error: `Repo not found in group: ${repoPath}` };
290
+ }
291
+
292
+ try {
293
+ removeMarker(repoPath);
294
+
295
+ // If group is now empty, delete the group config file
296
+ if (group.repos.length === 0) {
297
+ const filePath = path.join(POLYREPOS_DIR, `${groupName}.json`);
298
+ fs.unlinkSync(filePath);
299
+ return { ok: true, deleted_group: true };
300
+ } else {
301
+ saveGroup(group);
302
+ return { ok: true };
303
+ }
304
+ } catch (err) {
305
+ return { ok: false, error: `Failed to remove repo: ${err.message}` };
306
+ }
307
+ }
308
+
309
+ /**
310
+ * List all polyrepo groups
311
+ */
312
+ function listGroups() {
313
+ try {
314
+ ensurePolyreposDir();
315
+ if (!fs.existsSync(POLYREPOS_DIR)) {
316
+ return [];
317
+ }
318
+ const files = fs.readdirSync(POLYREPOS_DIR);
319
+ const groups = [];
320
+ for (const file of files) {
321
+ if (!file.endsWith('.json')) continue;
322
+ const group = loadGroup(file.replace(/\.json$/, ''));
323
+ if (group) {
324
+ groups.push(group);
325
+ }
326
+ }
327
+ return groups;
328
+ } catch (err) {
329
+ console.error(`${TAG} Failed to list groups:`, err.message);
330
+ return [];
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Load a single group by name
336
+ */
337
+ function listGroup(name) {
338
+ return loadGroup(name);
339
+ }
340
+
341
+ /**
342
+ * CLI Subcommand Handler
343
+ */
344
+ function handleCLI() {
345
+ const args = process.argv.slice(2);
346
+ const cmd = args[0];
347
+
348
+ if (!cmd || cmd === '--help' || cmd === '-h') {
349
+ process.stdout.write(`${TAG} Polyrepo Config Management
350
+
351
+ Usage:
352
+ node polyrepo.cjs create <name>
353
+ node polyrepo.cjs add <group> <path> [role] [--no-planning]
354
+ node polyrepo.cjs remove <group> <path>
355
+ node polyrepo.cjs list [group]
356
+ node polyrepo.cjs info
357
+ node polyrepo.cjs docs [show|set|remove]
358
+ node polyrepo.cjs --help
359
+
360
+ Commands:
361
+ create <name> Create an empty polyrepo group
362
+ add <group> <path> [role] [--no-planning]
363
+ Add a repo to a group (role defaults to basename)
364
+ remove <group> <path> Remove a repo from a group
365
+ list [group] List all groups or repos in a specific group
366
+ info Show this repo's polyrepo group membership
367
+ docs show Show doc paths for current repo
368
+ docs set <key> <path> Set a doc path (user, developer, examples, or custom)
369
+ docs remove <key> Remove a doc path
370
+ --help Show this help message
371
+ `);
372
+ return;
373
+ }
374
+
375
+ if (cmd === 'create') {
376
+ const name = args[1];
377
+ if (!name) {
378
+ console.error(`${TAG} create: missing name argument`);
379
+ process.exit(1);
380
+ }
381
+ const result = createGroup(name, []);
382
+ if (result.ok) {
383
+ process.stdout.write(`${TAG} Created group '${name}' at ${POLYREPOS_DIR}/${name}.json\n`);
384
+ } else {
385
+ console.error(`${TAG} Error: ${result.error}`);
386
+ process.exit(1);
387
+ }
388
+ } else if (cmd === 'add') {
389
+ const group = args[1];
390
+ const repoPath = args[2];
391
+ let role = args[3];
392
+ const noPlanning = args.includes('--no-planning');
393
+
394
+ if (!group || !repoPath) {
395
+ console.error(`${TAG} add: missing group or path argument`);
396
+ process.exit(1);
397
+ }
398
+
399
+ // Default role to basename if not provided
400
+ if (!role || role.startsWith('--')) {
401
+ role = path.basename(repoPath);
402
+ }
403
+
404
+ const result = addRepo(group, repoPath, role, !noPlanning);
405
+ if (result.ok) {
406
+ process.stdout.write(`${TAG} Added '${role}' (${repoPath}) to group '${group}'\n`);
407
+ } else {
408
+ console.error(`${TAG} Error: ${result.error}`);
409
+ process.exit(1);
410
+ }
411
+ } else if (cmd === 'remove') {
412
+ const group = args[1];
413
+ const repoPath = args[2];
414
+ if (!group || !repoPath) {
415
+ console.error(`${TAG} remove: missing group or path argument`);
416
+ process.exit(1);
417
+ }
418
+ const result = removeRepo(group, repoPath);
419
+ if (result.ok) {
420
+ if (result.deleted_group) {
421
+ process.stdout.write(`${TAG} Removed repo from group '${group}'. Group was empty, so config deleted.\n`);
422
+ } else {
423
+ process.stdout.write(`${TAG} Removed repo from group '${group}'\n`);
424
+ }
425
+ } else {
426
+ console.error(`${TAG} Error: ${result.error}`);
427
+ process.exit(1);
428
+ }
429
+ } else if (cmd === 'list') {
430
+ const groupName = args[1];
431
+ if (groupName) {
432
+ const group = listGroup(groupName);
433
+ if (!group) {
434
+ console.error(`${TAG} Group '${groupName}' not found`);
435
+ process.exit(1);
436
+ }
437
+ process.stdout.write(`\nPolyrepo Group: ${group.name}\n`);
438
+ for (const repo of group.repos) {
439
+ const planning = repo.planning ? '[planning]' : '[no planning]';
440
+ process.stdout.write(` ${repo.role.padEnd(15)} ${repo.path.padEnd(30)} ${planning}\n`);
441
+ }
442
+ process.stdout.write('\n');
443
+ } else {
444
+ const groups = listGroups();
445
+ if (groups.length === 0) {
446
+ process.stdout.write(`${TAG} No polyrepo groups defined\n`);
447
+ } else {
448
+ process.stdout.write('\nPolyrepo Groups:\n');
449
+ for (const group of groups) {
450
+ process.stdout.write(` ${group.name} (${group.repos.length} repos)\n`);
451
+ for (const repo of group.repos) {
452
+ const planning = repo.planning ? '[planning]' : '[no planning]';
453
+ process.stdout.write(` ${repo.role.padEnd(15)} ${repo.path.padEnd(30)} ${planning}\n`);
454
+ }
455
+ }
456
+ process.stdout.write('\n');
457
+ }
458
+ }
459
+ } else if (cmd === 'info') {
460
+ const cwd = process.cwd();
461
+ const marker = readMarker(cwd);
462
+ if (!marker) {
463
+ process.stdout.write(`${TAG} This repo is not part of any polyrepo group\n`);
464
+ process.exit(1);
465
+ }
466
+ process.stdout.write(`\nThis repo belongs to polyrepo group: ${marker.name}\n`);
467
+ process.stdout.write(`Role: ${marker.role}\n`);
468
+ const group = listGroup(marker.name);
469
+ if (group) {
470
+ const repo = group.repos.find(r => r.path === cwd);
471
+ process.stdout.write(`Planning: ${(repo ? repo.planning : true) ? 'yes' : 'no'}\n`);
472
+ } else {
473
+ process.stdout.write(`Planning: yes\n`);
474
+ }
475
+ if (marker.docs) {
476
+ process.stdout.write(`Docs:\n`);
477
+ for (const [key, val] of Object.entries(marker.docs)) {
478
+ process.stdout.write(` ${key.padEnd(12)} ${val}\n`);
479
+ }
480
+ }
481
+ process.stdout.write('\n');
482
+ } else if (cmd === 'docs') {
483
+ const subcmd = args[1];
484
+ const cwd = process.cwd();
485
+ if (!subcmd || subcmd === 'show') {
486
+ const marker = readMarker(cwd);
487
+ if (!marker) {
488
+ console.error(`${TAG} No polyrepo marker found in current directory`);
489
+ process.exit(1);
490
+ }
491
+ if (!marker.docs || Object.keys(marker.docs).length === 0) {
492
+ process.stdout.write(`${TAG} No docs paths configured for this repo\n`);
493
+ } else {
494
+ process.stdout.write(`\nDocs paths for ${marker.name} (${marker.role}):\n`);
495
+ for (const [key, val] of Object.entries(marker.docs)) {
496
+ process.stdout.write(` ${key.padEnd(12)} ${val}\n`);
497
+ }
498
+ process.stdout.write('\n');
499
+ }
500
+ } else if (subcmd === 'set') {
501
+ // docs set <key> <path>
502
+ const key = args[2];
503
+ const docPath = args[3];
504
+ if (!key || !docPath) {
505
+ console.error(`${TAG} docs set: usage: docs set <key> <path>`);
506
+ console.error(` Keys: user, developer, examples (or any custom key)`);
507
+ process.exit(1);
508
+ }
509
+ const result = setDocs(cwd, { [key]: docPath });
510
+ if (result.ok) {
511
+ process.stdout.write(`${TAG} Set docs.${key} = ${docPath}\n`);
512
+ } else {
513
+ console.error(`${TAG} Error: ${result.error}`);
514
+ process.exit(1);
515
+ }
516
+ } else if (subcmd === 'remove') {
517
+ const key = args[2];
518
+ if (!key) {
519
+ console.error(`${TAG} docs remove: usage: docs remove <key>`);
520
+ process.exit(1);
521
+ }
522
+ const result = setDocs(cwd, { [key]: null });
523
+ if (result.ok) {
524
+ process.stdout.write(`${TAG} Removed docs.${key}\n`);
525
+ } else {
526
+ console.error(`${TAG} Error: ${result.error}`);
527
+ process.exit(1);
528
+ }
529
+ } else {
530
+ console.error(`${TAG} docs: unknown subcommand '${subcmd}'. Use: show, set, remove`);
531
+ process.exit(1);
532
+ }
533
+ } else {
534
+ console.error(`${TAG} Unknown command: ${cmd}`);
535
+ process.exit(1);
536
+ }
537
+ }
538
+
539
+ // Export for testability
540
+ module.exports = {
541
+ createGroup,
542
+ addRepo,
543
+ removeRepo,
544
+ listGroups,
545
+ listGroup,
546
+ loadGroup,
547
+ saveGroup,
548
+ writeMarker,
549
+ readMarker,
550
+ removeMarker,
551
+ setDocs,
552
+ ensurePolyreposDir,
553
+ POLYREPOS_DIR,
554
+ MARKER_FILE
555
+ };
556
+
557
+ // Run CLI if executed directly
558
+ if (require.main === module) {
559
+ handleCLI();
560
+ }
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/prism-priority.cjs
4
+ // PRISM failure probability priority ranker for roadmap signal injection.
5
+ // Requirements: SIG-03
6
+ //
7
+ // Usage:
8
+ // node bin/prism-priority.cjs [--path=check-results.ndjson]
9
+ //
10
+ // Reads check-results.ndjson, extracts PRISM failure entries, ranks by
11
+ // P(failure) x impact, and outputs a formatted priority signal block
12
+ // for injection into plan-phase.md quorum context.
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ // ── Impact score map ─────────────────────────────────────────────────────────
18
+ const IMPACT_SCORES = {
19
+ 'prism:quorum': 10,
20
+ 'prism:mcp-availability': 7,
21
+ };
22
+ const DEFAULT_IMPACT = 5;
23
+
24
+ /**
25
+ * readPrismResults(ndjsonPath) — reads check-results.ndjson and filters PRISM entries.
26
+ * @param {string} [ndjsonPath] - path to check-results.ndjson
27
+ * @returns {Array<{ check_id: string, result: string, summary: string, timestamp: string, metadata: object }>}
28
+ */
29
+ function readPrismResults(ndjsonPath) {
30
+ const p = ndjsonPath || path.join(process.cwd(), '.planning', 'formal', 'check-results.ndjson');
31
+ try {
32
+ if (!fs.existsSync(p)) return [];
33
+ const raw = fs.readFileSync(p, 'utf8');
34
+ const lines = raw.split('\n').filter(l => l.trim().length > 0);
35
+
36
+ // Parse and filter for PRISM entries
37
+ const prismEntries = [];
38
+ for (const line of lines) {
39
+ try {
40
+ const entry = JSON.parse(line);
41
+ if (entry.formalism === 'prism') {
42
+ prismEntries.push({
43
+ check_id: entry.check_id || 'unknown',
44
+ result: entry.result || 'unknown',
45
+ summary: entry.summary || '',
46
+ timestamp: entry.timestamp || '',
47
+ metadata: entry.metadata || {},
48
+ });
49
+ }
50
+ } catch (_) { /* skip malformed lines */ }
51
+ }
52
+
53
+ // Group by check_id, keep only most recent entry per check_id
54
+ const byCheckId = new Map();
55
+ for (const entry of prismEntries) {
56
+ const existing = byCheckId.get(entry.check_id);
57
+ if (!existing || entry.timestamp > existing.timestamp) {
58
+ byCheckId.set(entry.check_id, entry);
59
+ }
60
+ }
61
+
62
+ return Array.from(byCheckId.values());
63
+ } catch (_) {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ /**
69
+ * rankFailureModes(prismResults) — ranks failure modes by P(failure) x impact.
70
+ * @param {Array<{ check_id: string, result: string, summary: string, timestamp: string, metadata: object }>} prismResults
71
+ * @returns {Array<{ check_id: string, priority: number, p_failure: number, impact: number, summary: string }>}
72
+ */
73
+ function rankFailureModes(prismResults) {
74
+ const failures = prismResults.filter(e => e.result === 'fail' || e.result === 'warn');
75
+ if (failures.length === 0) return [];
76
+
77
+ const ranked = failures.map(entry => {
78
+ // Determine P(failure)
79
+ let p_failure;
80
+ if (entry.result === 'fail') {
81
+ p_failure = 1.0;
82
+ } else {
83
+ // warn results — uncertain, use 0.5
84
+ p_failure = 0.5;
85
+ }
86
+
87
+ // Determine impact
88
+ const impact = IMPACT_SCORES[entry.check_id] || DEFAULT_IMPACT;
89
+
90
+ // Compute priority
91
+ const priority = Math.round(p_failure * impact * 100) / 100;
92
+
93
+ return {
94
+ check_id: entry.check_id,
95
+ priority,
96
+ p_failure,
97
+ impact,
98
+ summary: entry.summary,
99
+ };
100
+ });
101
+
102
+ // Sort descending by priority
103
+ ranked.sort((a, b) => b.priority - a.priority);
104
+
105
+ return ranked;
106
+ }
107
+
108
+ /**
109
+ * formatPrioritySignal(rankedModes) — produces formatted text block for quorum injection.
110
+ * @param {Array<{ check_id: string, priority: number, p_failure: number, impact: number, summary: string }>} rankedModes
111
+ * @returns {string|null} - formatted text block, or null if no failures
112
+ */
113
+ function formatPrioritySignal(rankedModes) {
114
+ if (!rankedModes || rankedModes.length === 0) return null;
115
+
116
+ const lines = [
117
+ '=== PRISM Priority Signal ===',
118
+ 'Failure modes ranked by P(failure) x impact:',
119
+ ];
120
+
121
+ for (let i = 0; i < rankedModes.length; i++) {
122
+ const mode = rankedModes[i];
123
+ lines.push(
124
+ (i + 1) + '. [' + mode.check_id + '] priority=' + mode.priority.toFixed(1) +
125
+ ' -- P(fail)=' + mode.p_failure.toFixed(1) +
126
+ ', impact=' + mode.impact +
127
+ ' -- "' + mode.summary + '"'
128
+ );
129
+ }
130
+
131
+ lines.push('=============================');
132
+
133
+ return lines.join('\n');
134
+ }
135
+
136
+ // ── CLI entrypoint ───────────────────────────────────────────────────────────
137
+ if (require.main === module) {
138
+ const args = process.argv.slice(2);
139
+ const pathArg = args.find(a => a.startsWith('--path='));
140
+ const ndjsonPath = pathArg ? pathArg.split('=')[1] : undefined;
141
+
142
+ const results = readPrismResults(ndjsonPath);
143
+ const ranked = rankFailureModes(results);
144
+ const signal = formatPrioritySignal(ranked);
145
+
146
+ if (signal) {
147
+ process.stdout.write(signal + '\n');
148
+ } else {
149
+ process.stderr.write('No PRISM failures found\n');
150
+ }
151
+ }
152
+
153
+ module.exports = { readPrismResults, rankFailureModes, formatPrioritySignal };