@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.
- package/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* quorum-slot-dispatch.cjs — prompt construction + output parsing wrapper
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node quorum-slot-dispatch.cjs \
|
|
9
|
+
* --slot <name> \
|
|
10
|
+
* --mode <A|B> \
|
|
11
|
+
* --round <n> \
|
|
12
|
+
* --question <text> \
|
|
13
|
+
* [--artifact-path <path>] \
|
|
14
|
+
* [--review-context <string>] \
|
|
15
|
+
* [--prior-positions-file <path>] \
|
|
16
|
+
* [--traces-file <path>] \
|
|
17
|
+
* [--request-improvements] \
|
|
18
|
+
* [--timeout <ms>] \
|
|
19
|
+
* [--cwd <dir>]
|
|
20
|
+
*
|
|
21
|
+
* Builds the Mode A or Mode B prompt from deterministic JS templates matching
|
|
22
|
+
* agents/qgsd-quorum-slot-worker.md Step 2, pipes it to call-quorum-slot.cjs via
|
|
23
|
+
* child_process.spawn, parses the output, and emits a structured YAML result block.
|
|
24
|
+
*
|
|
25
|
+
* Exported pure functions (testable without subprocess):
|
|
26
|
+
* buildModeAPrompt, buildModeBPrompt, parseVerdict, parseReasoning,
|
|
27
|
+
* parseCitations, parseImprovements, emitResultBlock, stripQuotes
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const { spawn } = require('child_process');
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
|
|
34
|
+
// ─── Arg parsing (mirrors call-quorum-slot.cjs pattern) ───────────────────────
|
|
35
|
+
const argv = process.argv.slice(2);
|
|
36
|
+
const getArg = (f) => {
|
|
37
|
+
const i = argv.indexOf(f);
|
|
38
|
+
return i !== -1 && argv[i + 1] !== undefined ? argv[i + 1] : null;
|
|
39
|
+
};
|
|
40
|
+
const hasFlag = (f) => argv.includes(f);
|
|
41
|
+
|
|
42
|
+
// ─── Requirements loading and matching functions ──────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Cache for loaded requirements, keyed by projectRoot to avoid re-reading disk.
|
|
46
|
+
* @type {Map<string, Array>}
|
|
47
|
+
*/
|
|
48
|
+
const requirementsCache = new Map();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* loadRequirements — reads `.planning/formal/requirements.json` from projectRoot.
|
|
52
|
+
* Fail-open: returns [] if file missing, malformed, or any error occurs.
|
|
53
|
+
* Caches result keyed by projectRoot to avoid re-reading disk.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} projectRoot
|
|
56
|
+
* @returns {Array} — array of requirement objects, or [] if load failed
|
|
57
|
+
*/
|
|
58
|
+
function loadRequirements(projectRoot) {
|
|
59
|
+
if (requirementsCache.has(projectRoot)) {
|
|
60
|
+
return requirementsCache.get(projectRoot);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const filePath = path.join(projectRoot, '.planning', 'formal', 'requirements.json');
|
|
65
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
66
|
+
const data = JSON.parse(content);
|
|
67
|
+
const reqs = Array.isArray(data.requirements) ? data.requirements : [];
|
|
68
|
+
requirementsCache.set(projectRoot, reqs);
|
|
69
|
+
return reqs;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// Fail-open: return empty array on any error (missing file, malformed JSON, etc.)
|
|
72
|
+
requirementsCache.set(projectRoot, []);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* List of English stopwords to filter out during keyword extraction.
|
|
79
|
+
* @type {Set<string>}
|
|
80
|
+
*/
|
|
81
|
+
const STOPWORDS = new Set([
|
|
82
|
+
'the', 'a', 'is', 'it', 'to', 'of', 'and', 'or', 'in', 'for', 'this', 'that',
|
|
83
|
+
'with', 'from', 'be', 'are', 'was', 'has', 'have', 'do', 'does', 'not', 'but',
|
|
84
|
+
'an', 'on', 'at', 'by', 'we', 'should', 'would', 'could', 'will', 'can',
|
|
85
|
+
'what', 'how', 'why', 'when', 'which', 'these', 'those', 'as', 'if', 'then',
|
|
86
|
+
'there', 'their', 'they', 'them', 'its', 'so', 'some', 'any', 'all', 'each',
|
|
87
|
+
'every', 'both', 'other', 'very', 'just', 'only', 'more', 'most', 'less', 'too'
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Map artifact path segments to category groups for stronger matching.
|
|
92
|
+
* @type {Map<string, string>}
|
|
93
|
+
*/
|
|
94
|
+
const PATH_CATEGORY_MAP = new Map([
|
|
95
|
+
['hook', 'Hooks & Enforcement'],
|
|
96
|
+
['quorum', 'Quorum & Dispatch'],
|
|
97
|
+
['dispatch', 'Quorum & Dispatch'],
|
|
98
|
+
['install', 'Installer & CLI'],
|
|
99
|
+
['mcp', 'MCP & Agents'],
|
|
100
|
+
['agent', 'MCP & Agents'],
|
|
101
|
+
['slot', 'MCP & Agents'],
|
|
102
|
+
['formal', 'Formal Verification'],
|
|
103
|
+
['alloy', 'Formal Verification'],
|
|
104
|
+
['tla', 'Formal Verification'],
|
|
105
|
+
['prism', 'Formal Verification'],
|
|
106
|
+
['config', 'Configuration'],
|
|
107
|
+
['plan', 'Planning & Tracking'],
|
|
108
|
+
['state', 'Planning & Tracking'],
|
|
109
|
+
['test', 'Testing & Quality'],
|
|
110
|
+
['observe', 'Observability & Diagnostics'],
|
|
111
|
+
['telemetry', 'Observability & Diagnostics'],
|
|
112
|
+
['scoreboard', 'Observability & Diagnostics']
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* matchRequirementsByKeywords — filters requirements based on keywords from question and artifact path.
|
|
117
|
+
* Returns max 20 matching requirements, sorted by relevance score (descending).
|
|
118
|
+
*
|
|
119
|
+
* @param {Array} requirements — full requirements array
|
|
120
|
+
* @param {string} question — the question text
|
|
121
|
+
* @param {string|null} artifactPath — optional artifact path (e.g. "hooks/qgsd-stop.js")
|
|
122
|
+
* @returns {Array} — filtered requirements (max 20), sorted by score descending
|
|
123
|
+
*/
|
|
124
|
+
function matchRequirementsByKeywords(requirements, question, artifactPath) {
|
|
125
|
+
if (!requirements || requirements.length === 0) {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Extract keywords from question
|
|
130
|
+
const questionKeywords = extractKeywords(question);
|
|
131
|
+
|
|
132
|
+
// Extract keywords from artifact path
|
|
133
|
+
const pathKeywords = artifactPath ? extractPathKeywords(artifactPath) : new Set();
|
|
134
|
+
const pathCategories = artifactPath ? extractPathCategories(artifactPath) : [];
|
|
135
|
+
|
|
136
|
+
// Score each requirement
|
|
137
|
+
const scored = requirements.map(req => {
|
|
138
|
+
let score = 0;
|
|
139
|
+
|
|
140
|
+
// Match on id prefix (e.g. "DISP" from "DISP-01")
|
|
141
|
+
const idPrefix = req.id ? req.id.split('-')[0].toLowerCase() : '';
|
|
142
|
+
if (idPrefix && (questionKeywords.has(idPrefix) || pathKeywords.has(idPrefix))) {
|
|
143
|
+
score += 2;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Match on category_raw
|
|
147
|
+
if (req.category_raw) {
|
|
148
|
+
const catRaw = req.category_raw.toLowerCase();
|
|
149
|
+
for (const kw of questionKeywords) {
|
|
150
|
+
if (catRaw.includes(kw)) score += 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Match on category (group)
|
|
155
|
+
if (req.category) {
|
|
156
|
+
const cat = req.category.toLowerCase();
|
|
157
|
+
for (const kw of questionKeywords) {
|
|
158
|
+
if (cat.includes(kw)) score += 1;
|
|
159
|
+
}
|
|
160
|
+
// Boost if category matches path-derived categories
|
|
161
|
+
for (const pathCat of pathCategories) {
|
|
162
|
+
if (cat === pathCat.toLowerCase()) {
|
|
163
|
+
score += 3; // Strong signal from artifact path
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Match on text
|
|
169
|
+
if (req.text) {
|
|
170
|
+
const text = req.text.toLowerCase();
|
|
171
|
+
for (const kw of questionKeywords) {
|
|
172
|
+
if (text.includes(kw)) score += 1;
|
|
173
|
+
}
|
|
174
|
+
for (const kw of pathKeywords) {
|
|
175
|
+
if (text.includes(kw)) score += 1;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { req, score };
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Sort by score descending, filter out zero-score entries, cap at 20
|
|
183
|
+
return scored
|
|
184
|
+
.filter(({ score }) => score > 0)
|
|
185
|
+
.sort((a, b) => b.score - a.score)
|
|
186
|
+
.slice(0, 20)
|
|
187
|
+
.map(({ req }) => req);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* extractKeywords — splits text into tokens and filters out stopwords.
|
|
192
|
+
* Splits on spaces, slashes, hyphens, dots, underscores.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} text
|
|
195
|
+
* @returns {Set<string>} — set of meaningful lowercase tokens
|
|
196
|
+
*/
|
|
197
|
+
function extractKeywords(text) {
|
|
198
|
+
if (!text) return new Set();
|
|
199
|
+
|
|
200
|
+
// Split on common delimiters: space, /, -, ., _
|
|
201
|
+
const tokens = text.toLowerCase()
|
|
202
|
+
.split(/[\s\/\-\._]+/)
|
|
203
|
+
.filter(t => t.length > 0 && !STOPWORDS.has(t));
|
|
204
|
+
|
|
205
|
+
return new Set(tokens);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* extractPathKeywords — extracts tokens from artifact path (just filename parts).
|
|
210
|
+
*
|
|
211
|
+
* @param {string} artifactPath
|
|
212
|
+
* @returns {Set<string>} — meaningful tokens from path
|
|
213
|
+
*/
|
|
214
|
+
function extractPathKeywords(artifactPath) {
|
|
215
|
+
if (!artifactPath) return new Set();
|
|
216
|
+
|
|
217
|
+
// Extract just the filename part
|
|
218
|
+
const filename = path.basename(artifactPath);
|
|
219
|
+
const tokens = filename.toLowerCase()
|
|
220
|
+
.split(/[\s\/\-\._]+/)
|
|
221
|
+
.filter(t => t.length > 0 && !STOPWORDS.has(t));
|
|
222
|
+
|
|
223
|
+
return new Set(tokens);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* extractPathCategories — maps artifact path segments to category groups.
|
|
228
|
+
* Checks each segment of the path against PATH_CATEGORY_MAP.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} artifactPath
|
|
231
|
+
* @returns {Array<string>} — matching category groups
|
|
232
|
+
*/
|
|
233
|
+
function extractPathCategories(artifactPath) {
|
|
234
|
+
if (!artifactPath) return [];
|
|
235
|
+
|
|
236
|
+
const categories = new Set();
|
|
237
|
+
const segments = artifactPath.toLowerCase().split(/[\s\/\-\._]+/);
|
|
238
|
+
|
|
239
|
+
for (const segment of segments) {
|
|
240
|
+
if (PATH_CATEGORY_MAP.has(segment)) {
|
|
241
|
+
categories.add(PATH_CATEGORY_MAP.get(segment));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return Array.from(categories);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* formatRequirementsSection — formats an array of requirements into a text block.
|
|
250
|
+
* Returns null if the array is empty (no section should be injected).
|
|
251
|
+
*
|
|
252
|
+
* @param {Array} requirements — array of requirement objects
|
|
253
|
+
* @returns {string|null} — formatted section or null
|
|
254
|
+
*/
|
|
255
|
+
function formatRequirementsSection(requirements) {
|
|
256
|
+
if (!requirements || requirements.length === 0) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const lines = [];
|
|
261
|
+
lines.push('=== APPLICABLE REQUIREMENTS ===');
|
|
262
|
+
lines.push('The following project requirements are relevant to this review.');
|
|
263
|
+
lines.push('Consider whether the proposed change satisfies or violates these:');
|
|
264
|
+
lines.push('');
|
|
265
|
+
|
|
266
|
+
for (const req of requirements) {
|
|
267
|
+
const category = req.category || 'Unknown';
|
|
268
|
+
lines.push(`- [${req.id}] ${req.text} (${category})`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
lines.push('');
|
|
272
|
+
lines.push('================================');
|
|
273
|
+
|
|
274
|
+
return lines.join('\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Pure prompt-construction functions ──────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* buildModeAPrompt — constructs the Mode A question prompt.
|
|
281
|
+
*
|
|
282
|
+
* Matches the EXACT template from agents/qgsd-quorum-slot-worker.md Step 2 Mode A.
|
|
283
|
+
*
|
|
284
|
+
* @param {object} opts
|
|
285
|
+
* @param {number} opts.round
|
|
286
|
+
* @param {string} opts.repoDir
|
|
287
|
+
* @param {string} opts.question
|
|
288
|
+
* @param {string} [opts.artifactPath]
|
|
289
|
+
* @param {string} [opts.artifactContent] - pre-read content (avoids model read failures)
|
|
290
|
+
* @param {string} [opts.reviewContext]
|
|
291
|
+
* @param {string} [opts.priorPositions] - Round 2+ cross-pollination
|
|
292
|
+
* @param {boolean}[opts.requestImprovements]
|
|
293
|
+
* @param {Array} [opts.requirements] - array of requirement objects to inject
|
|
294
|
+
* @returns {string}
|
|
295
|
+
*/
|
|
296
|
+
function buildModeAPrompt({ round, repoDir, question, artifactPath, artifactContent, reviewContext, priorPositions, requestImprovements, requirements }) {
|
|
297
|
+
const lines = [];
|
|
298
|
+
|
|
299
|
+
// Header
|
|
300
|
+
lines.push(`QGSD Quorum — Round ${round}`);
|
|
301
|
+
lines.push('');
|
|
302
|
+
|
|
303
|
+
// Repository + question
|
|
304
|
+
lines.push(`Repository: ${repoDir}`);
|
|
305
|
+
lines.push('');
|
|
306
|
+
lines.push(`Question: ${question}`);
|
|
307
|
+
|
|
308
|
+
// Artifact section (conditional)
|
|
309
|
+
if (artifactPath) {
|
|
310
|
+
lines.push('');
|
|
311
|
+
lines.push('=== Artifact ===');
|
|
312
|
+
lines.push(`Path: ${artifactPath}`);
|
|
313
|
+
if (artifactContent) {
|
|
314
|
+
lines.push('Content:');
|
|
315
|
+
lines.push(artifactContent);
|
|
316
|
+
} else {
|
|
317
|
+
lines.push('(Read this file to obtain its full content before evaluating.)');
|
|
318
|
+
}
|
|
319
|
+
lines.push('================');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Requirements section (conditional — injected right after question/artifact, before review context)
|
|
323
|
+
if (requirements && requirements.length > 0) {
|
|
324
|
+
const reqSection = formatRequirementsSection(requirements);
|
|
325
|
+
if (reqSection) {
|
|
326
|
+
lines.push('');
|
|
327
|
+
lines.push(reqSection);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Review context (conditional — first occurrence)
|
|
332
|
+
if (reviewContext) {
|
|
333
|
+
lines.push('');
|
|
334
|
+
lines.push(`\u26a0 REVIEW CONTEXT: ${reviewContext}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (round >= 2 && priorPositions) {
|
|
338
|
+
// ── Round 2+ path ─────────────────────────────────────────────────────
|
|
339
|
+
lines.push('');
|
|
340
|
+
lines.push('The following positions are from other AI models in this quorum — not human experts.');
|
|
341
|
+
lines.push('Evaluate them as peer AI opinions.');
|
|
342
|
+
lines.push('');
|
|
343
|
+
lines.push('Prior positions:');
|
|
344
|
+
lines.push(priorPositions);
|
|
345
|
+
|
|
346
|
+
// Review context reminder (Round 2+ only, when reviewContext present)
|
|
347
|
+
if (reviewContext) {
|
|
348
|
+
lines.push('');
|
|
349
|
+
lines.push(`\u26a0 REVIEW CONTEXT REMINDER: ${reviewContext}`);
|
|
350
|
+
lines.push('(If any prior position applied evaluation criteria inconsistent with the above — e.g.');
|
|
351
|
+
lines.push('rejected a plan because code was absent, or approved test results without checking');
|
|
352
|
+
lines.push('assertions — reconsider your position in light of the correct evaluation criteria.)');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
lines.push('');
|
|
356
|
+
if (artifactContent) {
|
|
357
|
+
lines.push('Before revising your position, re-review the artifact content provided above and');
|
|
358
|
+
lines.push('use your tools to check any other relevant files if needed.');
|
|
359
|
+
} else {
|
|
360
|
+
lines.push('Before revising your position, use your tools to re-check relevant files. At minimum');
|
|
361
|
+
lines.push('re-read CLAUDE.md and .planning/STATE.md if they exist, and re-read the artifact file if');
|
|
362
|
+
lines.push('one was provided.');
|
|
363
|
+
}
|
|
364
|
+
lines.push('');
|
|
365
|
+
lines.push('Given the above, do you maintain your answer or revise it? State your updated position');
|
|
366
|
+
lines.push('clearly (2\u20134 sentences).');
|
|
367
|
+
|
|
368
|
+
// Improvements block (Round 2+, when requestImprovements)
|
|
369
|
+
if (requestImprovements) {
|
|
370
|
+
lines.push('If you APPROVE and have specific, actionable improvements, append:');
|
|
371
|
+
lines.push('');
|
|
372
|
+
lines.push('Improvements:');
|
|
373
|
+
lines.push('- suggestion: [concise change \u2014 one sentence]');
|
|
374
|
+
lines.push(' rationale: [why this strengthens the plan]');
|
|
375
|
+
lines.push('');
|
|
376
|
+
lines.push('Omit this section entirely if you have no improvements, or if you BLOCK.');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
lines.push('');
|
|
380
|
+
lines.push('If your re-check references specific files, line numbers, or code snippets, record');
|
|
381
|
+
lines.push('them in a citations: field in your response (optional).');
|
|
382
|
+
|
|
383
|
+
} else {
|
|
384
|
+
// ── Round 1 path ──────────────────────────────────────────────────────
|
|
385
|
+
lines.push('');
|
|
386
|
+
if (artifactContent) {
|
|
387
|
+
lines.push('The artifact content is provided above. Use your available tools to read any other');
|
|
388
|
+
lines.push('relevant files from the Repository directory if needed. Your answer must be grounded');
|
|
389
|
+
lines.push('in the artifact content and what you actually find in the repo.');
|
|
390
|
+
} else {
|
|
391
|
+
lines.push('IMPORTANT: Before answering, use your available tools to read files from the');
|
|
392
|
+
lines.push('Repository directory above. At minimum read: CLAUDE.md (if it exists),');
|
|
393
|
+
lines.push('.planning/STATE.md (if it exists), and the artifact file at the path shown in the');
|
|
394
|
+
lines.push('Artifact section above (if present). Then read any other files directly relevant to');
|
|
395
|
+
lines.push('the question. Your answer must be grounded in what you actually find in the repo.');
|
|
396
|
+
}
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push('You are one AI model in a multi-model quorum. Your peer reviewers are other AI language');
|
|
399
|
+
lines.push('models \u2014 not human experts. Give your honest answer with reasoning. Be concise (3\u20136');
|
|
400
|
+
lines.push('sentences). Do not defer to peer models.');
|
|
401
|
+
|
|
402
|
+
// Improvements block (Round 1, when requestImprovements)
|
|
403
|
+
if (requestImprovements) {
|
|
404
|
+
lines.push('If you APPROVE and have specific, actionable improvements, append:');
|
|
405
|
+
lines.push('');
|
|
406
|
+
lines.push('Improvements:');
|
|
407
|
+
lines.push('- suggestion: [concise change \u2014 one sentence]');
|
|
408
|
+
lines.push(' rationale: [why this strengthens the plan]');
|
|
409
|
+
lines.push('');
|
|
410
|
+
lines.push('Omit this section entirely if you have no improvements, or if you BLOCK.');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
lines.push('');
|
|
414
|
+
lines.push('If your answer references specific files, line numbers, or code snippets from the');
|
|
415
|
+
lines.push('repository, record them in a citations: field in your response (optional \u2014 only');
|
|
416
|
+
lines.push('include if you actually cite code).');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return lines.join('\n');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* buildModeBPrompt — constructs the Mode B execution review prompt.
|
|
424
|
+
*
|
|
425
|
+
* Matches the EXACT template from agents/qgsd-quorum-slot-worker.md Step 2 Mode B.
|
|
426
|
+
*
|
|
427
|
+
* @param {object} opts
|
|
428
|
+
* @param {number} opts.round
|
|
429
|
+
* @param {string} opts.repoDir
|
|
430
|
+
* @param {string} opts.question
|
|
431
|
+
* @param {string} opts.traces - execution trace output (required for Mode B)
|
|
432
|
+
* @param {string} [opts.artifactPath]
|
|
433
|
+
* @param {string} [opts.artifactContent] - pre-read content (avoids model read failures)
|
|
434
|
+
* @param {string} [opts.reviewContext]
|
|
435
|
+
* @param {string} [opts.priorPositions] - Round 2+
|
|
436
|
+
* @param {Array} [opts.requirements] - array of requirement objects to inject
|
|
437
|
+
* @returns {string}
|
|
438
|
+
*/
|
|
439
|
+
function buildModeBPrompt({ round, repoDir, question, traces, artifactPath, artifactContent, reviewContext, priorPositions, requirements }) {
|
|
440
|
+
const lines = [];
|
|
441
|
+
|
|
442
|
+
// Header
|
|
443
|
+
lines.push(`QGSD Quorum — Execution Review (Round ${round})`);
|
|
444
|
+
lines.push('');
|
|
445
|
+
|
|
446
|
+
// Repository + question
|
|
447
|
+
lines.push(`Repository: ${repoDir}`);
|
|
448
|
+
lines.push('');
|
|
449
|
+
lines.push(`QUESTION: ${question}`);
|
|
450
|
+
|
|
451
|
+
// Artifact section (conditional)
|
|
452
|
+
if (artifactPath) {
|
|
453
|
+
lines.push('');
|
|
454
|
+
lines.push('=== Artifact ===');
|
|
455
|
+
lines.push(`Path: ${artifactPath}`);
|
|
456
|
+
if (artifactContent) {
|
|
457
|
+
lines.push('Content:');
|
|
458
|
+
lines.push(artifactContent);
|
|
459
|
+
} else {
|
|
460
|
+
lines.push('(Read this file to obtain its full content before evaluating.)');
|
|
461
|
+
}
|
|
462
|
+
lines.push('================');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Requirements section (conditional — injected right after question/artifact, before review context)
|
|
466
|
+
if (requirements && requirements.length > 0) {
|
|
467
|
+
const reqSection = formatRequirementsSection(requirements);
|
|
468
|
+
if (reqSection) {
|
|
469
|
+
lines.push('');
|
|
470
|
+
lines.push(reqSection);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Review context (conditional — first occurrence)
|
|
475
|
+
if (reviewContext) {
|
|
476
|
+
lines.push('');
|
|
477
|
+
lines.push(`\u26a0 REVIEW CONTEXT: ${reviewContext}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Execution traces (always present in Mode B)
|
|
481
|
+
lines.push('');
|
|
482
|
+
lines.push('=== EXECUTION TRACES ===');
|
|
483
|
+
lines.push(traces || '');
|
|
484
|
+
|
|
485
|
+
// Prior positions (Round 2+)
|
|
486
|
+
if (round >= 2 && priorPositions) {
|
|
487
|
+
lines.push('');
|
|
488
|
+
lines.push('Prior positions:');
|
|
489
|
+
lines.push(priorPositions);
|
|
490
|
+
|
|
491
|
+
// Review context reminder (Round 2+ only, when reviewContext present)
|
|
492
|
+
if (reviewContext) {
|
|
493
|
+
lines.push('');
|
|
494
|
+
lines.push(`\u26a0 REVIEW CONTEXT REMINDER: ${reviewContext}`);
|
|
495
|
+
lines.push('(If any prior position applied incorrect evaluation criteria, reconsider in light of the above.)');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
lines.push('');
|
|
500
|
+
if (artifactContent) {
|
|
501
|
+
lines.push('The artifact content is provided above. Use your tools to read any other relevant files');
|
|
502
|
+
lines.push('from the Repository directory if needed.');
|
|
503
|
+
} else {
|
|
504
|
+
lines.push('Before giving your verdict, use your tools to read files from the Repository directory');
|
|
505
|
+
lines.push('above. At minimum read: CLAUDE.md (if it exists), .planning/STATE.md (if it exists), and');
|
|
506
|
+
lines.push('the artifact file at the path shown above (if present).');
|
|
507
|
+
}
|
|
508
|
+
lines.push('');
|
|
509
|
+
lines.push('Note: prior positions are opinions from other AI models \u2014 not human specialists.');
|
|
510
|
+
lines.push('');
|
|
511
|
+
lines.push('Review the execution traces above. Give:');
|
|
512
|
+
lines.push('');
|
|
513
|
+
lines.push('verdict: APPROVE | REJECT | FLAG');
|
|
514
|
+
lines.push('reasoning: [2\u20134 sentences grounded in the actual trace output \u2014 not assumptions]');
|
|
515
|
+
lines.push('');
|
|
516
|
+
lines.push('APPROVE if output clearly shows the question is satisfied.');
|
|
517
|
+
lines.push('REJECT if output shows it is NOT satisfied.');
|
|
518
|
+
lines.push('FLAG if output is ambiguous or requires human judgment.');
|
|
519
|
+
lines.push('If your verdict references specific lines from the execution traces or files, record');
|
|
520
|
+
lines.push('them in a citations: field (optional \u2014 only when you directly cite output lines or');
|
|
521
|
+
lines.push('file content).');
|
|
522
|
+
|
|
523
|
+
return lines.join('\n');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// ─── Output parsing functions ─────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* parseVerdict — extracts verdict from raw CLI output.
|
|
530
|
+
*
|
|
531
|
+
* Mode A: first 500 chars of rawOutput (free-form position summary)
|
|
532
|
+
* Mode B (default): extract APPROVE|REJECT|FLAG from "verdict:" line; default FLAG
|
|
533
|
+
*
|
|
534
|
+
* @param {string} rawOutput
|
|
535
|
+
* @param {string} [mode] 'A' or 'B' (default B)
|
|
536
|
+
* @returns {string}
|
|
537
|
+
*/
|
|
538
|
+
function parseVerdict(rawOutput, mode) {
|
|
539
|
+
if (mode === 'A') {
|
|
540
|
+
return (rawOutput || '').slice(0, 500);
|
|
541
|
+
}
|
|
542
|
+
// Mode B: extract APPROVE|REJECT|FLAG
|
|
543
|
+
const match = (rawOutput || '').match(/verdict:\s*(APPROVE|REJECT|FLAG)/i);
|
|
544
|
+
return match ? match[1].toUpperCase() : 'FLAG';
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* parseReasoning — extracts reasoning from "reasoning: ..." line.
|
|
549
|
+
*
|
|
550
|
+
* @param {string} rawOutput
|
|
551
|
+
* @returns {string|null}
|
|
552
|
+
*/
|
|
553
|
+
function parseReasoning(rawOutput) {
|
|
554
|
+
if (!rawOutput) return null;
|
|
555
|
+
const match = rawOutput.match(/^reasoning:\s*(.+)$/m);
|
|
556
|
+
if (match) return match[1].trim();
|
|
557
|
+
// Fallback: first 400 chars
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* parseCitations — extracts citations block from "citations: |" section.
|
|
563
|
+
*
|
|
564
|
+
* Handles both space-indented and tab-indented YAML block scalar content.
|
|
565
|
+
*
|
|
566
|
+
* @param {string} rawOutput
|
|
567
|
+
* @returns {string|null}
|
|
568
|
+
*/
|
|
569
|
+
function parseCitations(rawOutput) {
|
|
570
|
+
if (!rawOutput) return null;
|
|
571
|
+
|
|
572
|
+
const lines = rawOutput.split('\n');
|
|
573
|
+
let inCitations = false;
|
|
574
|
+
const citationLines = [];
|
|
575
|
+
|
|
576
|
+
for (let i = 0; i < lines.length; i++) {
|
|
577
|
+
const line = lines[i];
|
|
578
|
+
const trimmed = line.trim();
|
|
579
|
+
|
|
580
|
+
if (!inCitations) {
|
|
581
|
+
// Detect "citations: |" or "citations:" line
|
|
582
|
+
if (/^citations:\s*\|?\s*$/.test(trimmed)) {
|
|
583
|
+
inCitations = true;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
// Indented continuation (space or tab)
|
|
588
|
+
if (line.startsWith(' ') || line.startsWith('\t')) {
|
|
589
|
+
citationLines.push(trimmed);
|
|
590
|
+
} else if (trimmed === '') {
|
|
591
|
+
// blank line inside block — keep
|
|
592
|
+
citationLines.push('');
|
|
593
|
+
} else {
|
|
594
|
+
// Non-indented non-empty line — end of block
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (citationLines.length === 0) return null;
|
|
601
|
+
|
|
602
|
+
// Remove trailing empty lines
|
|
603
|
+
while (citationLines.length > 0 && citationLines[citationLines.length - 1] === '') {
|
|
604
|
+
citationLines.pop();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return citationLines.length > 0 ? citationLines.join('\n') : null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* stripQuotes — strips surrounding single or double quotes from a string.
|
|
612
|
+
* @param {string} s
|
|
613
|
+
* @returns {string}
|
|
614
|
+
*/
|
|
615
|
+
function stripQuotes(s) {
|
|
616
|
+
if (!s) return s;
|
|
617
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
618
|
+
return s.slice(1, -1);
|
|
619
|
+
}
|
|
620
|
+
return s;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* parseImprovements — scans rawOutput for "Improvements:" section, parses list entries.
|
|
625
|
+
*
|
|
626
|
+
* Migrated from bin/gsd-quorum-slot-worker-improvements.test.cjs (canonical location).
|
|
627
|
+
* Never throws — improvements are additive, not required.
|
|
628
|
+
*
|
|
629
|
+
* @param {string} rawOutput
|
|
630
|
+
* @returns {Array<{suggestion: string, rationale: string}>}
|
|
631
|
+
*/
|
|
632
|
+
function parseImprovements(rawOutput) {
|
|
633
|
+
if (!rawOutput || typeof rawOutput !== 'string') return [];
|
|
634
|
+
|
|
635
|
+
const lines = rawOutput.split('\n');
|
|
636
|
+
let inSection = false;
|
|
637
|
+
const results = [];
|
|
638
|
+
let currentEntry = null;
|
|
639
|
+
|
|
640
|
+
for (let i = 0; i < lines.length; i++) {
|
|
641
|
+
const line = lines[i];
|
|
642
|
+
|
|
643
|
+
// Detect section start
|
|
644
|
+
if (!inSection && line.trimStart().startsWith('Improvements:')) {
|
|
645
|
+
inSection = true;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (!inSection) continue;
|
|
650
|
+
|
|
651
|
+
// Detect section end: non-indented non-empty line that isn't a list item
|
|
652
|
+
const trimmed = line.trim();
|
|
653
|
+
if (trimmed === '') continue; // blank lines: skip, stay in section
|
|
654
|
+
|
|
655
|
+
// Check if this is a new top-level key (non-indented, non-list-item) — section ends
|
|
656
|
+
if (!line.startsWith(' ') && !line.startsWith('\t') && !trimmed.startsWith('-') && trimmed !== '') {
|
|
657
|
+
// End of improvements section
|
|
658
|
+
if (currentEntry && currentEntry.suggestion && currentEntry.rationale) {
|
|
659
|
+
results.push({ suggestion: currentEntry.suggestion, rationale: currentEntry.rationale });
|
|
660
|
+
}
|
|
661
|
+
currentEntry = null;
|
|
662
|
+
inSection = false;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Match `- suggestion:` line — starts a new entry
|
|
667
|
+
const suggestionMatch = trimmed.match(/^-\s+suggestion:\s*(.*)$/);
|
|
668
|
+
if (suggestionMatch) {
|
|
669
|
+
// Save previous entry if complete
|
|
670
|
+
if (currentEntry && currentEntry.suggestion && currentEntry.rationale) {
|
|
671
|
+
results.push({ suggestion: currentEntry.suggestion, rationale: currentEntry.rationale });
|
|
672
|
+
}
|
|
673
|
+
const val = stripQuotes(suggestionMatch[1].trim());
|
|
674
|
+
currentEntry = { suggestion: val, rationale: null };
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Match `rationale:` line (indented continuation)
|
|
679
|
+
const rationaleMatch = trimmed.match(/^rationale:\s*(.*)$/);
|
|
680
|
+
if (rationaleMatch && currentEntry) {
|
|
681
|
+
const val = stripQuotes(rationaleMatch[1].trim());
|
|
682
|
+
currentEntry.rationale = val;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Flush last entry
|
|
688
|
+
if (currentEntry && currentEntry.suggestion && currentEntry.rationale) {
|
|
689
|
+
results.push({ suggestion: currentEntry.suggestion, rationale: currentEntry.rationale });
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return results;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* emitResultBlock — produces the YAML-formatted result block matching the agent spec Step 4.
|
|
697
|
+
*
|
|
698
|
+
* Returns a string (does NOT write to stdout — main() handles that).
|
|
699
|
+
*
|
|
700
|
+
* @param {object} opts
|
|
701
|
+
* @param {string} opts.slot
|
|
702
|
+
* @param {number} opts.round
|
|
703
|
+
* @param {string} opts.verdict
|
|
704
|
+
* @param {string} opts.reasoning
|
|
705
|
+
* @param {string} [opts.citations]
|
|
706
|
+
* @param {Array} [opts.improvements]
|
|
707
|
+
* @param {string} [opts.rawOutput]
|
|
708
|
+
* @param {boolean}[opts.isUnavail]
|
|
709
|
+
* @param {string} [opts.unavailMessage]
|
|
710
|
+
* @returns {string}
|
|
711
|
+
*/
|
|
712
|
+
function emitResultBlock({ slot, round, verdict, reasoning, citations, improvements, rawOutput, isUnavail, unavailMessage }) {
|
|
713
|
+
const lines = [];
|
|
714
|
+
|
|
715
|
+
lines.push(`slot: ${slot}`);
|
|
716
|
+
lines.push(`round: ${round}`);
|
|
717
|
+
lines.push(`verdict: ${verdict}`);
|
|
718
|
+
|
|
719
|
+
if (reasoning) {
|
|
720
|
+
lines.push(`reasoning: ${reasoning}`);
|
|
721
|
+
} else {
|
|
722
|
+
lines.push('reasoning:');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (citations) {
|
|
726
|
+
lines.push('citations: |');
|
|
727
|
+
const citLines = citations.split('\n');
|
|
728
|
+
for (const cl of citLines) {
|
|
729
|
+
lines.push(` ${cl}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (improvements && improvements.length > 0) {
|
|
734
|
+
lines.push('improvements:');
|
|
735
|
+
for (const imp of improvements) {
|
|
736
|
+
lines.push(` - suggestion: "${imp.suggestion}"`);
|
|
737
|
+
lines.push(` rationale: "${imp.rationale}"`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (isUnavail && unavailMessage) {
|
|
742
|
+
lines.push('unavail_message: |');
|
|
743
|
+
const msgLines = unavailMessage.slice(0, 500).split('\n');
|
|
744
|
+
for (const ml of msgLines) {
|
|
745
|
+
lines.push(` ${ml}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
lines.push('raw: |');
|
|
750
|
+
const rawTruncated = (rawOutput || '').slice(0, 5000);
|
|
751
|
+
const rawLines = rawTruncated.split('\n');
|
|
752
|
+
for (const rl of rawLines) {
|
|
753
|
+
lines.push(` ${rl}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return lines.join('\n') + '\n';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ─── Main (CLI entry point) ───────────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
async function main() {
|
|
762
|
+
const slot = getArg('--slot');
|
|
763
|
+
const mode = getArg('--mode') || 'A';
|
|
764
|
+
const roundArg = getArg('--round');
|
|
765
|
+
const question = getArg('--question') || '';
|
|
766
|
+
const artifactPath = getArg('--artifact-path') || null;
|
|
767
|
+
const reviewContext = getArg('--review-context') || null;
|
|
768
|
+
const priorPositionsFile = getArg('--prior-positions-file') || null;
|
|
769
|
+
const tracesFile = getArg('--traces-file') || null;
|
|
770
|
+
const requestImprovements = hasFlag('--request-improvements');
|
|
771
|
+
const timeoutArg = getArg('--timeout');
|
|
772
|
+
const cwd = getArg('--cwd') || process.cwd();
|
|
773
|
+
|
|
774
|
+
if (!slot) {
|
|
775
|
+
process.stderr.write('[quorum-slot-dispatch] --slot is required\n');
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
if (!roundArg) {
|
|
779
|
+
process.stderr.write('[quorum-slot-dispatch] --round is required\n');
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const round = parseInt(roundArg, 10);
|
|
784
|
+
const timeout = timeoutArg ? parseInt(timeoutArg, 10) : 30000;
|
|
785
|
+
|
|
786
|
+
// Read optional temp files
|
|
787
|
+
let priorPositions = null;
|
|
788
|
+
if (priorPositionsFile) {
|
|
789
|
+
try {
|
|
790
|
+
priorPositions = fs.readFileSync(priorPositionsFile, 'utf8');
|
|
791
|
+
} catch (e) {
|
|
792
|
+
process.stderr.write(`[quorum-slot-dispatch] Could not read prior-positions-file: ${e.message}\n`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
let traces = null;
|
|
797
|
+
if (tracesFile) {
|
|
798
|
+
try {
|
|
799
|
+
traces = fs.readFileSync(tracesFile, 'utf8');
|
|
800
|
+
} catch (e) {
|
|
801
|
+
process.stderr.write(`[quorum-slot-dispatch] Could not read traces-file: ${e.message}\n`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Build prompt
|
|
806
|
+
const repoDir = cwd;
|
|
807
|
+
|
|
808
|
+
// Pre-read artifact file content to inline in prompt (prevents read failures in models)
|
|
809
|
+
const ARTIFACT_MAX_BYTES = 50 * 1024; // 50KB cap
|
|
810
|
+
let artifactContent = null;
|
|
811
|
+
if (artifactPath) {
|
|
812
|
+
try {
|
|
813
|
+
const resolvedPath = path.isAbsolute(artifactPath) ? artifactPath : path.join(cwd, artifactPath);
|
|
814
|
+
const stat = fs.statSync(resolvedPath);
|
|
815
|
+
if (stat.size <= ARTIFACT_MAX_BYTES) {
|
|
816
|
+
artifactContent = fs.readFileSync(resolvedPath, 'utf8');
|
|
817
|
+
} else {
|
|
818
|
+
process.stderr.write(`[quorum-slot-dispatch] artifact too large (${stat.size} bytes > ${ARTIFACT_MAX_BYTES}), models must read it themselves\n`);
|
|
819
|
+
}
|
|
820
|
+
} catch (e) {
|
|
821
|
+
process.stderr.write(`[quorum-slot-dispatch] could not pre-read artifact: ${e.message}\n`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Load and match requirements (fail-open: if loading fails, requirements will be empty)
|
|
826
|
+
const allRequirements = loadRequirements(repoDir);
|
|
827
|
+
const matchedRequirements = matchRequirementsByKeywords(allRequirements, question, artifactPath);
|
|
828
|
+
|
|
829
|
+
let prompt;
|
|
830
|
+
if (mode === 'B') {
|
|
831
|
+
prompt = buildModeBPrompt({ round, repoDir, question, artifactPath, artifactContent, reviewContext, priorPositions, traces: traces || '', requirements: matchedRequirements });
|
|
832
|
+
} else {
|
|
833
|
+
prompt = buildModeAPrompt({ round, repoDir, question, artifactPath, artifactContent, reviewContext, priorPositions, requestImprovements, requirements: matchedRequirements });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Locate call-quorum-slot.cjs relative to this script
|
|
837
|
+
const cqsPath = path.join(__dirname, 'call-quorum-slot.cjs');
|
|
838
|
+
|
|
839
|
+
// Spawn call-quorum-slot.cjs as child process with stdin pipe
|
|
840
|
+
const rawOutput = await new Promise((resolve) => {
|
|
841
|
+
let child;
|
|
842
|
+
try {
|
|
843
|
+
child = spawn(process.execPath, [cqsPath, '--slot', slot, '--timeout', String(timeout), '--cwd', cwd], {
|
|
844
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
845
|
+
});
|
|
846
|
+
} catch (err) {
|
|
847
|
+
resolve({ exitCode: 1, output: `[spawn error: ${err.message}]` });
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Write prompt to child stdin and close
|
|
852
|
+
child.stdin.write(prompt, 'utf8');
|
|
853
|
+
child.stdin.end();
|
|
854
|
+
|
|
855
|
+
let stdout = '';
|
|
856
|
+
let stderr = '';
|
|
857
|
+
const MAX_BUF = 10 * 1024 * 1024;
|
|
858
|
+
|
|
859
|
+
child.stdout.on('data', d => {
|
|
860
|
+
if (stdout.length < MAX_BUF) stdout += d.toString().slice(0, MAX_BUF - stdout.length);
|
|
861
|
+
});
|
|
862
|
+
child.stderr.on('data', d => {
|
|
863
|
+
stderr += d.toString().slice(0, 4096);
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
child.on('close', (code) => {
|
|
867
|
+
resolve({ exitCode: code, output: stdout || stderr || '(no output)' });
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
child.on('error', (err) => {
|
|
871
|
+
resolve({ exitCode: 1, output: `[spawn error: ${err.message}]` });
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
const { exitCode, output } = rawOutput;
|
|
876
|
+
const isUnavail = exitCode !== 0 || output.includes('TIMEOUT');
|
|
877
|
+
|
|
878
|
+
let result;
|
|
879
|
+
if (isUnavail) {
|
|
880
|
+
result = emitResultBlock({
|
|
881
|
+
slot,
|
|
882
|
+
round,
|
|
883
|
+
verdict: 'UNAVAIL',
|
|
884
|
+
reasoning: 'Bash call failed or timed out.',
|
|
885
|
+
rawOutput: output,
|
|
886
|
+
isUnavail: true,
|
|
887
|
+
unavailMessage: output.slice(0, 500)
|
|
888
|
+
});
|
|
889
|
+
} else {
|
|
890
|
+
const verdict = parseVerdict(output, mode);
|
|
891
|
+
const reasoning = parseReasoning(output) || output.slice(0, 400);
|
|
892
|
+
const citations = parseCitations(output);
|
|
893
|
+
const improvements = requestImprovements ? parseImprovements(output) : [];
|
|
894
|
+
|
|
895
|
+
result = emitResultBlock({
|
|
896
|
+
slot,
|
|
897
|
+
round,
|
|
898
|
+
verdict,
|
|
899
|
+
reasoning,
|
|
900
|
+
citations,
|
|
901
|
+
improvements: improvements.length > 0 ? improvements : undefined,
|
|
902
|
+
rawOutput: output
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
process.stdout.write(result);
|
|
907
|
+
if (!result.endsWith('\n')) process.stdout.write('\n');
|
|
908
|
+
process.exit(0);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ─── Module exports ───────────────────────────────────────────────────────────
|
|
912
|
+
if (typeof module !== 'undefined') {
|
|
913
|
+
module.exports = {
|
|
914
|
+
buildModeAPrompt,
|
|
915
|
+
buildModeBPrompt,
|
|
916
|
+
parseVerdict,
|
|
917
|
+
parseReasoning,
|
|
918
|
+
parseCitations,
|
|
919
|
+
parseImprovements,
|
|
920
|
+
emitResultBlock,
|
|
921
|
+
stripQuotes,
|
|
922
|
+
loadRequirements,
|
|
923
|
+
matchRequirementsByKeywords,
|
|
924
|
+
formatRequirementsSection,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ─── Entry point guard ────────────────────────────────────────────────────────
|
|
929
|
+
if (require.main === module) {
|
|
930
|
+
main().catch(err => {
|
|
931
|
+
process.stderr.write(`[quorum-slot-dispatch] Fatal: ${err.message}\n`);
|
|
932
|
+
process.exit(1);
|
|
933
|
+
});
|
|
934
|
+
}
|