@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,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Internal helper: merge baseline into existing requirements.
|
|
10
|
+
* Handles the core sync logic (steps 2-7 from original implementation).
|
|
11
|
+
*/
|
|
12
|
+
function _syncFromBaseline(baseline, projectRoot) {
|
|
13
|
+
const root = projectRoot || process.cwd();
|
|
14
|
+
|
|
15
|
+
// 2. Read existing requirements
|
|
16
|
+
const reqPath = path.join(root, '.planning', 'formal', 'requirements.json');
|
|
17
|
+
let rawEnvelope;
|
|
18
|
+
let requirements;
|
|
19
|
+
|
|
20
|
+
if (fs.existsSync(reqPath)) {
|
|
21
|
+
try {
|
|
22
|
+
rawEnvelope = JSON.parse(fs.readFileSync(reqPath, 'utf8'));
|
|
23
|
+
requirements = rawEnvelope.requirements || [];
|
|
24
|
+
} catch (_) {
|
|
25
|
+
rawEnvelope = {};
|
|
26
|
+
requirements = [];
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
// Create .formal directory if needed
|
|
30
|
+
fs.mkdirSync(path.join(root, '.planning', 'formal'), { recursive: true });
|
|
31
|
+
rawEnvelope = {};
|
|
32
|
+
requirements = [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 3. Build lookup of existing requirement texts -> id
|
|
36
|
+
const existingTexts = new Map(requirements.map(r => [r.text, r.id]));
|
|
37
|
+
|
|
38
|
+
// 4. Build map of highest existing ID number per prefix
|
|
39
|
+
const maxId = {};
|
|
40
|
+
for (const r of requirements) {
|
|
41
|
+
if (!r.id) continue;
|
|
42
|
+
const dashIdx = r.id.lastIndexOf('-');
|
|
43
|
+
if (dashIdx === -1) continue;
|
|
44
|
+
const prefix = r.id.substring(0, dashIdx);
|
|
45
|
+
const num = parseInt(r.id.substring(dashIdx + 1), 10);
|
|
46
|
+
if (!isNaN(num)) {
|
|
47
|
+
maxId[prefix] = Math.max(maxId[prefix] || 0, num);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const added = [];
|
|
52
|
+
const skipped = [];
|
|
53
|
+
const totalBefore = requirements.length;
|
|
54
|
+
|
|
55
|
+
// 5-6. Process each baseline category
|
|
56
|
+
for (const cat of baseline.categories) {
|
|
57
|
+
if (!cat.requirements || cat.requirements.length === 0) continue;
|
|
58
|
+
|
|
59
|
+
// Derive prefix from first requirement's ID (e.g., "UX-01" -> "UX")
|
|
60
|
+
const firstId = cat.requirements[0].id;
|
|
61
|
+
const dashIdx = firstId.lastIndexOf('-');
|
|
62
|
+
const prefix = firstId.substring(0, dashIdx);
|
|
63
|
+
|
|
64
|
+
for (const req of cat.requirements) {
|
|
65
|
+
if (existingTexts.has(req.text)) {
|
|
66
|
+
// 6a. Skip duplicate
|
|
67
|
+
skipped.push({
|
|
68
|
+
id: req.id,
|
|
69
|
+
text: req.text,
|
|
70
|
+
existingId: existingTexts.get(req.text),
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
// 6c. Assign next available ID
|
|
74
|
+
maxId[prefix] = (maxId[prefix] || 0) + 1;
|
|
75
|
+
const padLen = maxId[prefix] > 99 ? String(maxId[prefix]).length : 2;
|
|
76
|
+
const newId = prefix + '-' + String(maxId[prefix]).padStart(padLen, '0');
|
|
77
|
+
|
|
78
|
+
const newReq = {
|
|
79
|
+
id: newId,
|
|
80
|
+
text: req.text,
|
|
81
|
+
category: cat.name,
|
|
82
|
+
phase: 'baseline',
|
|
83
|
+
status: 'Pending',
|
|
84
|
+
provenance: {
|
|
85
|
+
source_file: 'qgsd-baseline',
|
|
86
|
+
milestone: 'baseline',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
requirements.push(newReq);
|
|
91
|
+
existingTexts.set(req.text, newId);
|
|
92
|
+
added.push({ id: newId, text: req.text });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 7. Write if anything was added
|
|
98
|
+
if (added.length > 0) {
|
|
99
|
+
const contentHash = 'sha256:' + crypto
|
|
100
|
+
.createHash('sha256')
|
|
101
|
+
.update(JSON.stringify(requirements, null, 2))
|
|
102
|
+
.digest('hex');
|
|
103
|
+
|
|
104
|
+
// Defensive: only write if hash actually changed
|
|
105
|
+
const existingHash = rawEnvelope.content_hash || null;
|
|
106
|
+
if (existingHash !== contentHash) {
|
|
107
|
+
const envelope = {
|
|
108
|
+
aggregated_at: new Date().toISOString(),
|
|
109
|
+
content_hash: contentHash,
|
|
110
|
+
frozen_at: rawEnvelope.frozen_at || null,
|
|
111
|
+
schema_version: rawEnvelope.schema_version || undefined,
|
|
112
|
+
requirements,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Remove undefined keys
|
|
116
|
+
if (envelope.schema_version === undefined) delete envelope.schema_version;
|
|
117
|
+
|
|
118
|
+
fs.writeFileSync(reqPath, JSON.stringify(envelope, null, 2) + '\n');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
added,
|
|
124
|
+
skipped,
|
|
125
|
+
total_before: totalBefore,
|
|
126
|
+
total_after: requirements.length,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Merge baseline requirements into .planning/formal/requirements.json.
|
|
132
|
+
* Idempotent: matches on exact `text` field to skip duplicates.
|
|
133
|
+
* Assigns next-available IDs per category prefix for new entries.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} profile - One of: web, mobile, desktop, api, cli, library
|
|
136
|
+
* @param {string} [projectRoot] - Path to project root, defaults to process.cwd()
|
|
137
|
+
* @returns {{ added: Array, skipped: Array, total_before: number, total_after: number }}
|
|
138
|
+
*/
|
|
139
|
+
function syncBaselineRequirements(profile, projectRoot) {
|
|
140
|
+
const root = projectRoot || process.cwd();
|
|
141
|
+
|
|
142
|
+
// 1. Load baseline requirements
|
|
143
|
+
let baseline;
|
|
144
|
+
try {
|
|
145
|
+
const { loadBaselineRequirements } = require('./load-baseline-requirements.cjs');
|
|
146
|
+
baseline = loadBaselineRequirements(profile);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error(`Error loading baseline requirements: ${err.message}`);
|
|
149
|
+
process.exit(2);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return _syncFromBaseline(baseline, root);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Merge intent-based baseline requirements into .planning/formal/requirements.json.
|
|
157
|
+
* Idempotent: matches on exact `text` field to skip duplicates.
|
|
158
|
+
* Assigns next-available IDs per category prefix for new entries.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object} intent - Intent object with base_profile and optional dimensions
|
|
161
|
+
* @param {string} [projectRoot] - Path to project root, defaults to process.cwd()
|
|
162
|
+
* @returns {{ added: Array, skipped: Array, total_before: number, total_after: number }}
|
|
163
|
+
*/
|
|
164
|
+
function syncBaselineRequirementsFromIntent(intent, projectRoot) {
|
|
165
|
+
const root = projectRoot || process.cwd();
|
|
166
|
+
|
|
167
|
+
// 1. Load baseline requirements from intent
|
|
168
|
+
let baseline;
|
|
169
|
+
try {
|
|
170
|
+
const { loadBaselineRequirementsFromIntent } = require('./load-baseline-requirements.cjs');
|
|
171
|
+
baseline = loadBaselineRequirementsFromIntent(intent);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(`Error loading baseline requirements: ${err.message}`);
|
|
174
|
+
process.exit(2);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return _syncFromBaseline(baseline, root);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// CLI
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
function printReport(result, profile) {
|
|
185
|
+
console.log(`Baseline sync: ${profile} profile`);
|
|
186
|
+
console.log(` Before: ${result.total_before} requirements`);
|
|
187
|
+
console.log(` Added: ${result.added.length} new requirements`);
|
|
188
|
+
console.log(` Skipped: ${result.skipped.length} (already present by text match)`);
|
|
189
|
+
console.log(` After: ${result.total_after} requirements`);
|
|
190
|
+
|
|
191
|
+
if (result.added.length > 0) {
|
|
192
|
+
console.log('\nAdded:');
|
|
193
|
+
for (const a of result.added) {
|
|
194
|
+
console.log(` + [${a.id}] ${a.text}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (result.skipped.length > 0) {
|
|
199
|
+
console.log('\nSkipped:');
|
|
200
|
+
for (const s of result.skipped) {
|
|
201
|
+
console.log(` ~ [${s.id}] matched existing [${s.existingId}]`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (require.main === module) {
|
|
207
|
+
const args = process.argv.slice(2);
|
|
208
|
+
|
|
209
|
+
// --json flag
|
|
210
|
+
const jsonOutput = args.includes('--json');
|
|
211
|
+
|
|
212
|
+
// --detect is deprecated (auto-detect is now the default behavior) — silently strip it
|
|
213
|
+
const cleanArgs = args.filter(arg => arg !== '--detect');
|
|
214
|
+
|
|
215
|
+
// Priority: --intent-file > --profile > config.json intent > config.json profile > AUTO-DETECT
|
|
216
|
+
|
|
217
|
+
// Check --intent-file
|
|
218
|
+
const intentFileIdx = cleanArgs.indexOf('--intent-file');
|
|
219
|
+
if (intentFileIdx !== -1 && cleanArgs[intentFileIdx + 1]) {
|
|
220
|
+
try {
|
|
221
|
+
const intentFilePath = cleanArgs[intentFileIdx + 1];
|
|
222
|
+
const intentContent = fs.readFileSync(intentFilePath, 'utf8');
|
|
223
|
+
const intent = JSON.parse(intentContent);
|
|
224
|
+
const result = syncBaselineRequirementsFromIntent(intent);
|
|
225
|
+
if (jsonOutput) {
|
|
226
|
+
console.log(JSON.stringify(result, null, 2));
|
|
227
|
+
} else {
|
|
228
|
+
printReport(result, `intent (base_profile: ${intent.base_profile})`);
|
|
229
|
+
}
|
|
230
|
+
process.exit(0);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error(`Error loading intent file: ${err.message}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Parse --profile
|
|
238
|
+
const profileIdx = cleanArgs.indexOf('--profile');
|
|
239
|
+
let profile = null;
|
|
240
|
+
|
|
241
|
+
if (profileIdx !== -1 && cleanArgs[profileIdx + 1]) {
|
|
242
|
+
profile = cleanArgs[profileIdx + 1];
|
|
243
|
+
} else {
|
|
244
|
+
// Try reading from .planning/config.json (intent first, then profile)
|
|
245
|
+
try {
|
|
246
|
+
const config = JSON.parse(fs.readFileSync(
|
|
247
|
+
path.join(process.cwd(), '.planning/config.json'), 'utf8'
|
|
248
|
+
));
|
|
249
|
+
if (config.intent) {
|
|
250
|
+
const result = syncBaselineRequirementsFromIntent(config.intent);
|
|
251
|
+
if (jsonOutput) {
|
|
252
|
+
console.log(JSON.stringify(result, null, 2));
|
|
253
|
+
} else {
|
|
254
|
+
printReport(result, `config intent (base_profile: ${config.intent.base_profile})`);
|
|
255
|
+
}
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
profile = config.profile;
|
|
259
|
+
} catch (_) {}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// If no profile found, auto-detect (new default behavior)
|
|
263
|
+
if (!profile) {
|
|
264
|
+
try {
|
|
265
|
+
const { detectProjectIntent } = require('./detect-project-intent.cjs');
|
|
266
|
+
const detectionResult = detectProjectIntent(process.cwd());
|
|
267
|
+
const intent = detectionResult.suggested;
|
|
268
|
+
const result = syncBaselineRequirementsFromIntent(intent);
|
|
269
|
+
if (jsonOutput) {
|
|
270
|
+
console.log(JSON.stringify({ ...result, detection: detectionResult }, null, 2));
|
|
271
|
+
} else {
|
|
272
|
+
printReport(result, `auto-detected intent (base_profile: ${intent.base_profile})`);
|
|
273
|
+
}
|
|
274
|
+
process.exit(0);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error(`Error auto-detecting project intent: ${err.message}`);
|
|
277
|
+
console.error('Hint: use --profile <web|mobile|desktop|api|cli|library> to specify manually');
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const result = syncBaselineRequirements(profile);
|
|
283
|
+
if (jsonOutput) {
|
|
284
|
+
console.log(JSON.stringify(result, null, 2));
|
|
285
|
+
} else {
|
|
286
|
+
printReport(result, profile);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = { syncBaselineRequirements, syncBaselineRequirementsFromIntent };
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/task-envelope.cjs
|
|
3
|
+
// Task envelope CLI tool for structured research→plan→quorum handoff
|
|
4
|
+
// Provides: init, update, read, validate commands + exported validation functions
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
// Schema definition
|
|
12
|
+
const ENVELOPE_SCHEMA = {
|
|
13
|
+
schema_version: 'string (must be "1")',
|
|
14
|
+
phase: 'string (must match v\\d+\\.\\d+-\\d{2})',
|
|
15
|
+
created_at: 'string (ISO 8601, auto-generated)',
|
|
16
|
+
risk_level: 'string (low|medium|high)',
|
|
17
|
+
research: 'object (optional)',
|
|
18
|
+
plan: 'object (optional)',
|
|
19
|
+
quorum: 'object (optional)'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Validate envelope against schema
|
|
23
|
+
function validateEnvelope(obj) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
|
|
26
|
+
if (!obj || typeof obj !== 'object') {
|
|
27
|
+
errors.push('Envelope must be an object');
|
|
28
|
+
return { valid: false, errors };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check schema_version
|
|
32
|
+
if (typeof obj.schema_version !== 'string' || obj.schema_version !== '1') {
|
|
33
|
+
errors.push('schema_version must be string "1"');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check phase format: v\d+\.\d+-\d{2}
|
|
37
|
+
if (!obj.phase || typeof obj.phase !== 'string' || !/^v\d+\.\d+-\d{2}/.test(obj.phase)) {
|
|
38
|
+
errors.push('phase must match format v\\d+\\.\\d+-\\d{2} (e.g., v0.18-03)');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check risk_level
|
|
42
|
+
if (!obj.risk_level || !['low', 'medium', 'high'].includes(obj.risk_level)) {
|
|
43
|
+
errors.push('risk_level must be one of: low, medium, high');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
valid: errors.length === 0,
|
|
48
|
+
errors
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Find phase directory in .planning/phases/
|
|
53
|
+
function findPhaseDir(phaseId) {
|
|
54
|
+
const phasesDir = path.join(process.cwd(), '.planning', 'phases');
|
|
55
|
+
|
|
56
|
+
if (!fs.existsSync(phasesDir)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Look for directory matching phaseId or phaseId-*
|
|
61
|
+
const dirs = fs.readdirSync(phasesDir);
|
|
62
|
+
const match = dirs.find(d => d === phaseId || d.startsWith(phaseId + '-'));
|
|
63
|
+
|
|
64
|
+
if (!match) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return path.join(phasesDir, match);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get envelope path for a phase
|
|
72
|
+
function getEnvelopePath(phaseId) {
|
|
73
|
+
const phaseDir = findPhaseDir(phaseId);
|
|
74
|
+
if (!phaseDir) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return path.join(phaseDir, 'task-envelope.json');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Write JSON atomically: tmpPath + renameSync
|
|
81
|
+
function writeAtomicJson(targetPath, obj) {
|
|
82
|
+
const dir = path.dirname(targetPath);
|
|
83
|
+
|
|
84
|
+
// Ensure directory exists
|
|
85
|
+
if (!fs.existsSync(dir)) {
|
|
86
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tmpPath = `${targetPath}.${Date.now()}.tmp`;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
fs.writeFileSync(tmpPath, JSON.stringify(obj, null, 2), 'utf8');
|
|
93
|
+
fs.renameSync(tmpPath, targetPath);
|
|
94
|
+
return true;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
// Clean up temp file if rename failed
|
|
97
|
+
try {
|
|
98
|
+
fs.unlinkSync(tmpPath);
|
|
99
|
+
} catch {}
|
|
100
|
+
throw e;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Parse comma-separated arguments into array
|
|
105
|
+
function parseCommaList(str) {
|
|
106
|
+
if (!str) return [];
|
|
107
|
+
return str.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// main: handle CLI commands
|
|
111
|
+
function main() {
|
|
112
|
+
const args = process.argv.slice(2);
|
|
113
|
+
|
|
114
|
+
if (args.length === 0) {
|
|
115
|
+
process.stderr.write('[task-envelope] ERROR: no command specified\n');
|
|
116
|
+
process.stderr.write('Usage: task-envelope.cjs <init|update|read|validate> [options]\n');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const command = args[0];
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
if (command === 'init') {
|
|
124
|
+
commandInit(args);
|
|
125
|
+
} else if (command === 'update') {
|
|
126
|
+
commandUpdate(args);
|
|
127
|
+
} else if (command === 'read') {
|
|
128
|
+
commandRead(args);
|
|
129
|
+
} else if (command === 'validate') {
|
|
130
|
+
commandValidate(args);
|
|
131
|
+
} else {
|
|
132
|
+
process.stderr.write(`[task-envelope] ERROR: unknown command "${command}"\n`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
process.stderr.write(`[task-envelope] ERROR: ${e.message}\n`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Command: init
|
|
142
|
+
function commandInit(args) {
|
|
143
|
+
// Parse arguments
|
|
144
|
+
let phase, objective = '', constraints = '', targetFiles = [], confidence = 'HIGH', riskLevel = 'medium';
|
|
145
|
+
|
|
146
|
+
for (let i = 1; i < args.length; i++) {
|
|
147
|
+
if (args[i] === '--phase' && i + 1 < args.length) {
|
|
148
|
+
phase = args[++i];
|
|
149
|
+
} else if (args[i] === '--objective' && i + 1 < args.length) {
|
|
150
|
+
objective = args[++i];
|
|
151
|
+
} else if (args[i] === '--constraints' && i + 1 < args.length) {
|
|
152
|
+
constraints = args[++i];
|
|
153
|
+
} else if (args[i] === '--target-files' && i + 1 < args.length) {
|
|
154
|
+
targetFiles = parseCommaList(args[++i]);
|
|
155
|
+
} else if (args[i] === '--confidence' && i + 1 < args.length) {
|
|
156
|
+
confidence = args[++i];
|
|
157
|
+
} else if (args[i] === '--risk-level' && i + 1 < args.length) {
|
|
158
|
+
riskLevel = args[++i];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!phase) {
|
|
163
|
+
process.stderr.write('[task-envelope] ERROR: --phase is required\n');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Validate risk_level
|
|
168
|
+
const validRiskLevels = ['low', 'medium', 'high'];
|
|
169
|
+
let finalRiskLevel = riskLevel;
|
|
170
|
+
if (!validRiskLevels.includes(riskLevel)) {
|
|
171
|
+
process.stderr.write(`[task-envelope] WARNING: invalid risk_level "${riskLevel}"; using "medium"\n`);
|
|
172
|
+
finalRiskLevel = 'medium';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Find phase directory
|
|
176
|
+
const phaseDir = findPhaseDir(phase);
|
|
177
|
+
if (!phaseDir) {
|
|
178
|
+
process.stderr.write(`[task-envelope] ERROR: phase directory not found for "${phase}"\n`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Create envelope object
|
|
183
|
+
const envelope = {
|
|
184
|
+
schema_version: '1',
|
|
185
|
+
phase,
|
|
186
|
+
created_at: new Date().toISOString(),
|
|
187
|
+
risk_level: finalRiskLevel,
|
|
188
|
+
research: {
|
|
189
|
+
objective,
|
|
190
|
+
constraints,
|
|
191
|
+
target_files: targetFiles,
|
|
192
|
+
confidence
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Validate and write
|
|
197
|
+
const validation = validateEnvelope(envelope);
|
|
198
|
+
if (!validation.valid) {
|
|
199
|
+
process.stderr.write(`[task-envelope] ERROR: invalid envelope: ${validation.errors.join('; ')}\n`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const envelopePath = path.join(phaseDir, 'task-envelope.json');
|
|
204
|
+
writeAtomicJson(envelopePath, envelope);
|
|
205
|
+
console.log(`[task-envelope] initialized: ${envelopePath}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Command: update
|
|
209
|
+
function commandUpdate(args) {
|
|
210
|
+
// Parse arguments
|
|
211
|
+
let section = null;
|
|
212
|
+
let phase, planPath, keyDecisions = [], waveCount = null;
|
|
213
|
+
|
|
214
|
+
for (let i = 1; i < args.length; i++) {
|
|
215
|
+
if (args[i] === '--section' && i + 1 < args.length) {
|
|
216
|
+
section = args[++i];
|
|
217
|
+
} else if (args[i] === '--phase' && i + 1 < args.length) {
|
|
218
|
+
phase = args[++i];
|
|
219
|
+
} else if (args[i] === '--plan-path' && i + 1 < args.length) {
|
|
220
|
+
planPath = args[++i];
|
|
221
|
+
} else if (args[i] === '--key-decisions' && i + 1 < args.length) {
|
|
222
|
+
keyDecisions = parseCommaList(args[++i]);
|
|
223
|
+
} else if (args[i] === '--wave-count' && i + 1 < args.length) {
|
|
224
|
+
waveCount = parseInt(args[++i], 10);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!section) {
|
|
229
|
+
process.stderr.write('[task-envelope] ERROR: --section is required for update\n');
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!phase) {
|
|
234
|
+
process.stderr.write('[task-envelope] ERROR: --phase is required\n');
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (section === 'plan' && !planPath) {
|
|
239
|
+
process.stderr.write('[task-envelope] ERROR: --plan-path is required for --section plan\n');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Find phase directory
|
|
244
|
+
const phaseDir = findPhaseDir(phase);
|
|
245
|
+
if (!phaseDir) {
|
|
246
|
+
process.stderr.write(`[task-envelope] ERROR: phase directory not found for "${phase}"\n`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const envelopePath = path.join(phaseDir, 'task-envelope.json');
|
|
251
|
+
|
|
252
|
+
// Read existing envelope or create minimal one
|
|
253
|
+
let envelope;
|
|
254
|
+
if (fs.existsSync(envelopePath)) {
|
|
255
|
+
const content = fs.readFileSync(envelopePath, 'utf8');
|
|
256
|
+
envelope = JSON.parse(content);
|
|
257
|
+
} else {
|
|
258
|
+
// Create minimal envelope structure
|
|
259
|
+
envelope = {
|
|
260
|
+
schema_version: '1',
|
|
261
|
+
phase,
|
|
262
|
+
created_at: new Date().toISOString(),
|
|
263
|
+
risk_level: 'medium'
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Update section
|
|
268
|
+
if (section === 'plan') {
|
|
269
|
+
envelope.plan = {
|
|
270
|
+
plan_path: planPath,
|
|
271
|
+
key_decisions: keyDecisions
|
|
272
|
+
};
|
|
273
|
+
if (waveCount !== null && !isNaN(waveCount)) {
|
|
274
|
+
envelope.plan.wave_count = waveCount;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Validate and write
|
|
279
|
+
const validation = validateEnvelope(envelope);
|
|
280
|
+
if (!validation.valid) {
|
|
281
|
+
process.stderr.write(`[task-envelope] ERROR: invalid envelope: ${validation.errors.join('; ')}\n`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
writeAtomicJson(envelopePath, envelope);
|
|
286
|
+
console.log(`[task-envelope] updated: ${envelopePath}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Command: read
|
|
290
|
+
function commandRead(args) {
|
|
291
|
+
// Parse arguments
|
|
292
|
+
let phase;
|
|
293
|
+
|
|
294
|
+
for (let i = 1; i < args.length; i++) {
|
|
295
|
+
if (args[i] === '--phase' && i + 1 < args.length) {
|
|
296
|
+
phase = args[++i];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!phase) {
|
|
301
|
+
process.stderr.write('[task-envelope] ERROR: --phase is required\n');
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const envelopePath = getEnvelopePath(phase);
|
|
306
|
+
if (!envelopePath || !fs.existsSync(envelopePath)) {
|
|
307
|
+
process.stderr.write(`[task-envelope] ERROR: envelope not found for "${phase}"\n`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const content = fs.readFileSync(envelopePath, 'utf8');
|
|
312
|
+
const envelope = JSON.parse(content);
|
|
313
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Command: validate
|
|
317
|
+
function commandValidate(args) {
|
|
318
|
+
// Parse arguments
|
|
319
|
+
let phase;
|
|
320
|
+
|
|
321
|
+
for (let i = 1; i < args.length; i++) {
|
|
322
|
+
if (args[i] === '--phase' && i + 1 < args.length) {
|
|
323
|
+
phase = args[++i];
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!phase) {
|
|
328
|
+
process.stderr.write('[task-envelope] ERROR: --phase is required\n');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const envelopePath = getEnvelopePath(phase);
|
|
333
|
+
if (!envelopePath || !fs.existsSync(envelopePath)) {
|
|
334
|
+
console.log(JSON.stringify({ valid: false, errors: ['envelope not found'] }));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const content = fs.readFileSync(envelopePath, 'utf8');
|
|
339
|
+
let envelope;
|
|
340
|
+
try {
|
|
341
|
+
envelope = JSON.parse(content);
|
|
342
|
+
} catch (e) {
|
|
343
|
+
console.log(JSON.stringify({ valid: false, errors: ['malformed JSON'] }));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const result = validateEnvelope(envelope);
|
|
348
|
+
console.log(JSON.stringify(result));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Exports for testing
|
|
352
|
+
module.exports = {
|
|
353
|
+
validateEnvelope,
|
|
354
|
+
ENVELOPE_SCHEMA
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Run main if executed directly
|
|
358
|
+
if (require.main === module) {
|
|
359
|
+
main();
|
|
360
|
+
}
|