@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,150 @@
1
+ 'use strict';
2
+ // bin/attribute-trace-divergence.cjs
3
+ // Root-cause attribution tool for XState conformance divergences.
4
+ // Reads TTrace records from .planning/formal/.divergences.json (produced by validate-traces.cjs).
5
+ // Classifies each divergence as spec-bug or impl-bug with confidence scores.
6
+ // DIAG-02: outputs "fix XState guard X" or "fix hook implementation Y at line Z".
7
+ //
8
+ // CLI usage:
9
+ // node bin/attribute-trace-divergence.cjs [--input <path>] [--batch-size <N>] [--output-json]
10
+ //
11
+ // --input path to TTrace JSON file (default: .planning/formal/.divergences.json)
12
+ // --batch-size analyze first N divergences (default: 10)
13
+ // --output-json emit structured JSON to stdout in addition to plain text summary
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ // ── Attribution heuristics ────────────────────────────────────────────────────
19
+
20
+ // classifyDivergence: given a TTrace record, classify the root cause.
21
+ // Returns { specBugConfidence, implBugConfidence, failingGuard, recommendation, evidence }.
22
+ function classifyDivergence(ttrace) {
23
+ const { event, actualState, expectedState, guardEvaluations, divergenceType } = ttrace;
24
+ let specBugConfidence = 50; // start at 50/50
25
+ let implBugConfidence = 50;
26
+ let failingGuard = null;
27
+ const evidence = [];
28
+
29
+ // Evidence 1: Are there guard evaluations where a guard failed?
30
+ const failedGuards = (guardEvaluations || []).filter(g => !g.passed);
31
+ if (failedGuards.length > 0) {
32
+ failingGuard = failedGuards[0].guardName;
33
+ evidence.push(`Guard "${failingGuard}" evaluated to false`);
34
+ // Guard failure → could be spec-bug (wrong guard logic) or impl-bug (context not set)
35
+ // Check if context values look uninitialized (null, 0, undefined for numeric fields)
36
+ const ctx = failedGuards[0].context || {};
37
+ const suspiciousFields = Object.entries(ctx)
38
+ .filter(([k, v]) => (v === null || v === undefined || v === 0) &&
39
+ (k.includes('Count') || k.includes('Available') || k.includes('Polled')))
40
+ .map(([k]) => k);
41
+ if (suspiciousFields.length > 0) {
42
+ implBugConfidence += 30;
43
+ specBugConfidence -= 30;
44
+ evidence.push(`Context fields may not be initialized: ${suspiciousFields.join(', ')}`);
45
+ } else {
46
+ // Guard failed but context looks populated → more likely spec-bug
47
+ specBugConfidence += 20;
48
+ implBugConfidence -= 20;
49
+ evidence.push(`Context appears populated — guard logic may be incorrect`);
50
+ }
51
+ }
52
+
53
+ // Evidence 2: unmappable_action — always impl-bug (hook sent wrong event type)
54
+ if (divergenceType === 'unmappable_action') {
55
+ implBugConfidence = 90;
56
+ specBugConfidence = 10;
57
+ evidence.push('Action type has no XState mapping — hook emitted unknown event');
58
+ }
59
+
60
+ // Evidence 3: state stayed same when it should have changed (machine ignored event)
61
+ if (actualState === expectedState) {
62
+ // Validation passed — not a divergence; shouldn't be in TTrace
63
+ implBugConfidence = 0;
64
+ specBugConfidence = 0;
65
+ }
66
+
67
+ // Clamp 0-100
68
+ specBugConfidence = Math.max(0, Math.min(100, specBugConfidence));
69
+ implBugConfidence = Math.max(0, Math.min(100, implBugConfidence));
70
+
71
+ const recommendation = implBugConfidence >= specBugConfidence
72
+ ? `impl-bug: check hook implementation — context field initialization or event payload mapping`
73
+ : `spec-bug: review XState guard "${failingGuard || 'unknown'}" in qgsd-workflow.machine.ts`;
74
+
75
+ return { specBugConfidence, implBugConfidence, failingGuard, recommendation, evidence };
76
+ }
77
+
78
+ // analyzeTrace: full attribution for one TTrace record.
79
+ // Returns structured analysis object.
80
+ function analyzeTrace(ttrace, machine, walker) {
81
+ const { event, actualState, expectedState, divergenceType } = ttrace;
82
+ const classification = classifyDivergence(ttrace);
83
+ return {
84
+ event_action: event?.action || 'unknown',
85
+ actual_state: actualState,
86
+ expected_state: expectedState,
87
+ divergence_type: divergenceType,
88
+ failing_guard: classification.failingGuard,
89
+ spec_bug_confidence: classification.specBugConfidence,
90
+ impl_bug_confidence: classification.implBugConfidence,
91
+ recommendation: classification.recommendation,
92
+ evidence: classification.evidence,
93
+ };
94
+ }
95
+
96
+ module.exports = { classifyDivergence, analyzeTrace };
97
+
98
+ if (require.main === module) {
99
+ // Parse args
100
+ const args = process.argv.slice(2);
101
+ const inputIdx = args.indexOf('--input');
102
+ const batchIdx = args.indexOf('--batch-size');
103
+ const outputJson = args.includes('--output-json');
104
+ const inputPath = inputIdx >= 0
105
+ ? args[inputIdx + 1]
106
+ : path.join(process.cwd(), '.planning', 'formal', '.divergences.json');
107
+ const batchSize = batchIdx >= 0 ? (parseInt(args[batchIdx + 1], 10) || 10) : 10;
108
+
109
+ if (!fs.existsSync(inputPath)) {
110
+ process.stderr.write('[attribute-trace-divergence] No divergences file at: ' + inputPath + '\n');
111
+ process.stderr.write(' Run: node bin/validate-traces.cjs to generate .planning/formal/.divergences.json\n');
112
+ process.exit(0);
113
+ }
114
+
115
+ let ttraces;
116
+ try {
117
+ ttraces = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
118
+ } catch (e) {
119
+ process.stderr.write('[attribute-trace-divergence] Failed to parse: ' + inputPath + ' — ' + e.message + '\n');
120
+ process.exit(1);
121
+ }
122
+
123
+ if (!Array.isArray(ttraces) || ttraces.length === 0) {
124
+ process.stdout.write('[attribute-trace-divergence] No divergences to analyze\n');
125
+ process.exit(0);
126
+ }
127
+
128
+ const batch = ttraces.slice(0, batchSize);
129
+ const results = batch.map(t => analyzeTrace(t, null, null));
130
+
131
+ // Plain text summary
132
+ process.stdout.write('[attribute-trace-divergence] Analyzed ' + results.length + ' of ' + ttraces.length + ' divergences\n\n');
133
+ for (const r of results) {
134
+ process.stdout.write('Action: ' + r.event_action + ' | ' + r.actual_state + ' → expected ' + r.expected_state + '\n');
135
+ process.stdout.write(' Guard: ' + (r.failing_guard || 'n/a') + '\n');
136
+ process.stdout.write(' Spec-bug: ' + r.spec_bug_confidence + '% | Impl-bug: ' + r.impl_bug_confidence + '%\n');
137
+ process.stdout.write(' Recommend: ' + r.recommendation + '\n');
138
+ if (r.evidence.length > 0) {
139
+ process.stdout.write(' Evidence: ' + r.evidence.join('; ') + '\n');
140
+ }
141
+ process.stdout.write('\n');
142
+ }
143
+
144
+ if (outputJson) {
145
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
146
+ }
147
+
148
+ // Fail-open exit: non-zero only if attribution itself errored, not just because divergences exist
149
+ process.exit(0);
150
+ }
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * auth-drivers/gh-cli.cjs — GitHub CLI keychain driver
5
+ *
6
+ * Accounts live in the macOS/system keychain managed by gh.
7
+ * There is no credential file to copy — all operations delegate to gh.
8
+ * This is the single source of truth for gh auth status parsing,
9
+ * replacing duplicated regex in qgsd.cjs and gh-account-rotate.cjs.
10
+ *
11
+ * Applies to: copilot-1
12
+ *
13
+ * Required providers.json fields:
14
+ * auth.login — add command (e.g. ["gh", "auth", "login"])
15
+ */
16
+
17
+ const { spawnSync } = require('child_process');
18
+
19
+ /**
20
+ * parseGhStatus() → { accounts: string[], active: string | null }
21
+ * Single authoritative parser for `gh auth status` output.
22
+ * gh writes to stderr; we merge both streams.
23
+ */
24
+ function parseGhStatus() {
25
+ const r = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
26
+ const out = (r.stdout || '') + (r.stderr || '');
27
+
28
+ const accounts = [];
29
+ let active = null;
30
+ let last = null;
31
+
32
+ for (const line of out.split('\n')) {
33
+ const m = line.match(/account (\S+)/);
34
+ if (m) last = m[1];
35
+ if (/Logged in to github\.com account/.test(line) && last && !accounts.includes(last)) {
36
+ accounts.push(last);
37
+ }
38
+ if (/Active account:\s*true/.test(line) && last) {
39
+ active = last;
40
+ }
41
+ }
42
+
43
+ return { accounts, active };
44
+ }
45
+
46
+ /**
47
+ * list(provider) → [{ name, active }]
48
+ */
49
+ function list(_provider) {
50
+ const { accounts, active } = parseGhStatus();
51
+ return accounts.map(name => ({ name, active: name === active }));
52
+ }
53
+
54
+ /**
55
+ * switch(provider, name) — calls gh auth switch (non-interactive, stays in TUI).
56
+ */
57
+ function switchAccount(_provider, name) {
58
+ const r = spawnSync('gh', ['auth', 'switch', '--user', name, '--hostname', 'github.com'], {
59
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
60
+ });
61
+ if (r.status !== 0) {
62
+ throw new Error((r.stderr || r.stdout || 'gh auth switch failed').trim());
63
+ }
64
+ }
65
+
66
+ /**
67
+ * addCredentialFile() → null
68
+ * gh stores credentials in the OS keychain — no file to poll.
69
+ * promptLoginExternal will wait for manual [Enter] confirmation.
70
+ */
71
+ function addCredentialFile(_provider) {
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * extractAccountName() → null
77
+ * Cannot auto-detect from a file. TUI will not prompt for a name either —
78
+ * gh manages account identity internally; the new account will appear in
79
+ * the next `gh auth status` call.
80
+ */
81
+ function extractAccountName(_provider) {
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * add() — no-op: gh auth login handles everything internally.
87
+ * The account appears automatically in parseGhStatus() after login.
88
+ */
89
+ async function add(_provider, _name) {
90
+ // gh manages account storage; nothing to capture.
91
+ }
92
+
93
+ module.exports = { list, switch: switchAccount, addCredentialFile, extractAccountName, add, parseGhStatus };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * auth-drivers/index.cjs — driver loader
5
+ *
6
+ * loadDriver(type) resolves an auth driver by name, validates the 4-method
7
+ * interface contract, and returns the driver module.
8
+ *
9
+ * Driver interface (all methods required):
10
+ * list(provider) → [{ name: string, active: boolean }]
11
+ * switch(provider, name) → void (throws on error)
12
+ * addCredentialFile(provider) → string | null (file to poll for mtime, null = keychain)
13
+ * extractAccountName(provider)→ string | null (auto-detect after add, null = must prompt)
14
+ */
15
+
16
+ const path = require('path');
17
+
18
+ const DRIVER_DIR = __dirname;
19
+
20
+ const REQUIRED_METHODS = ['list', 'switch', 'addCredentialFile', 'extractAccountName'];
21
+
22
+ function loadDriver(type) {
23
+ if (!type) return null;
24
+
25
+ const driverPath = path.join(DRIVER_DIR, type + '.cjs');
26
+ let driver;
27
+ try {
28
+ driver = require(driverPath);
29
+ } catch (err) {
30
+ if (err.code === 'MODULE_NOT_FOUND') {
31
+ throw new Error(`Unknown auth driver "${type}" — expected ${driverPath}`);
32
+ }
33
+ throw err;
34
+ }
35
+
36
+ // Validate interface contract
37
+ for (const method of REQUIRED_METHODS) {
38
+ if (typeof driver[method] !== 'function') {
39
+ throw new Error(`Auth driver "${type}" missing required method: ${method}()`);
40
+ }
41
+ }
42
+
43
+ return driver;
44
+ }
45
+
46
+ module.exports = { loadDriver };
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * auth-drivers/pool.cjs — file-copy credential pool driver
5
+ *
6
+ * For providers that store OAuth credentials as a JSON file that can be
7
+ * copied between a pool directory and an active location.
8
+ * Backed by account-manager.cjs (TLA+-formalized FSM).
9
+ *
10
+ * Applies to: gemini-1, gemini-2, codex-1, codex-2
11
+ *
12
+ * Required providers.json fields:
13
+ * oauth_rotation.creds_dir — pool directory (~/.gemini/accounts)
14
+ * oauth_rotation.active_file — live credential file (~/.gemini/oauth_creds.json)
15
+ * auth.login — add command (e.g. ["gemini", "auth", "login"])
16
+ */
17
+
18
+ const acm = require('../account-manager.cjs');
19
+
20
+ /**
21
+ * list(provider) → [{ name, active }]
22
+ * Reads the pool directory and the .qgsd-active pointer — no subprocess.
23
+ */
24
+ function list(provider) {
25
+ const credsDir = acm.getCredsDir(provider);
26
+ const pool = acm.listPool(credsDir);
27
+ const active = acm.readActivePtr(credsDir);
28
+ return pool.map(name => ({ name, active: name === active }));
29
+ }
30
+
31
+ /**
32
+ * switch(provider, name) — copies pool entry to active_file, updates pointer.
33
+ * Driven by the FSM; throws on invalid transition or I/O error.
34
+ */
35
+ function switchAccount(provider, name) {
36
+ const fsm = new acm.AccountManagerFSM();
37
+ acm.cmdSwitch(fsm, provider, name);
38
+ }
39
+
40
+ /**
41
+ * addCredentialFile(provider) → string
42
+ * Returns the active_file path — promptLoginExternal polls its mtime to
43
+ * auto-detect when sign-in completes.
44
+ */
45
+ function addCredentialFile(provider) {
46
+ return acm.getActiveFile(provider);
47
+ }
48
+
49
+ /**
50
+ * extractAccountName(provider) → string | null
51
+ * For Gemini: decodes the id_token JWT to get the Google email (no network).
52
+ * For Codex (no id_token): returns null → TUI prompts for a name.
53
+ */
54
+ function extractAccountName(provider) {
55
+ return acm.extractEmailFromCreds(acm.getActiveFile(provider));
56
+ }
57
+
58
+ /**
59
+ * add(provider, name) — captures current active_file into pool under `name`.
60
+ * Called after promptLoginExternal resolves (credential file already written by CLI).
61
+ */
62
+ async function add(provider, name) {
63
+ const fsm = new acm.AccountManagerFSM();
64
+ await acm.cmdAdd(fsm, provider, name, false);
65
+ }
66
+
67
+ module.exports = { list, switch: switchAccount, addCredentialFile, extractAccountName, add };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * auth-drivers/simple.cjs — login-only driver (no account pool)
5
+ *
6
+ * For providers where there is a single account / session and no concept
7
+ * of switching between multiple credentials. The only operation is "add"
8
+ * (i.e., log in), which opens a terminal window running auth.login.
9
+ *
10
+ * Applies to: codex-1/2, opencode-1, and any future CLIs before pool support is added.
11
+ *
12
+ * Required providers.json fields:
13
+ * auth.login — login command (e.g. ["opencode", "auth"])
14
+ * oauth_rotation.active_file — optional; if present, JWT email is decoded from it
15
+ */
16
+
17
+ const os = require('os');
18
+ const fs = require('fs');
19
+
20
+ /**
21
+ * list() → []
22
+ * No pool — there are no accounts to enumerate.
23
+ */
24
+ function list(_provider) {
25
+ return [];
26
+ }
27
+
28
+ /**
29
+ * switch() — not supported; simple providers have no multi-account concept.
30
+ */
31
+ function switchAccount(_provider, _name) {
32
+ throw new Error('Account switching is not supported for this provider.');
33
+ }
34
+
35
+ /**
36
+ * addCredentialFile() → null
37
+ * No known credential file to poll; promptLoginExternal waits for manual [Enter].
38
+ */
39
+ function addCredentialFile(_provider) {
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * extractAccountName(provider) → string | null
45
+ *
46
+ * Two strategies, tried in order:
47
+ *
48
+ * 1. identity_detect (regex on a plain text/YAML/JSON file)
49
+ * Configured via providers.json identity_detect.{file, pattern}.
50
+ * Used for providers whose identity lives in a third-party config file,
51
+ * e.g. OpenCode delegates to gh: file=~/.config/gh/hosts.yml.
52
+ *
53
+ * 2. JWT decode from oauth_rotation.active_file
54
+ * Reads the credential JSON and decodes the id_token claim (or
55
+ * tokens.id_token for Codex-style nesting) to extract the email.
56
+ *
57
+ * Returns null if no strategy succeeds or on any I/O / parse error.
58
+ */
59
+ function extractAccountName(provider) {
60
+ // Strategy 1 — identity_detect (data-driven regex on a config file)
61
+ try {
62
+ const det = provider.identity_detect;
63
+ if (det?.file && det?.pattern) {
64
+ const filePath = det.file.replace(/^~/, os.homedir());
65
+ if (fs.existsSync(filePath)) {
66
+ const content = fs.readFileSync(filePath, 'utf8');
67
+ const m = content.match(new RegExp(det.pattern, 'm'));
68
+ if (m) return m[1];
69
+ }
70
+ }
71
+ } catch (_) {}
72
+
73
+ // Strategy 2 — JWT email from oauth_rotation.active_file
74
+ try {
75
+ const activeFile = provider.oauth_rotation?.active_file;
76
+ if (!activeFile) return null;
77
+ const filePath = activeFile.replace(/^~/, os.homedir());
78
+ if (!fs.existsSync(filePath)) return null;
79
+ const creds = JSON.parse(fs.readFileSync(filePath, 'utf8'));
80
+ // Gemini-style: { id_token: "..." } | Codex-style: { tokens: { id_token: "..." } }
81
+ const jwt = creds.id_token ?? creds.tokens?.id_token;
82
+ if (!jwt) return null;
83
+ const payload = Buffer.from(jwt.split('.')[1], 'base64url').toString('utf8');
84
+ return JSON.parse(payload).email ?? null;
85
+ } catch (_) {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * add() — no-op: the terminal already ran auth.login; nothing to capture.
92
+ */
93
+ async function add(_provider, _name) {}
94
+
95
+ module.exports = { list, switch: switchAccount, addCredentialFile, extractAccountName, add };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * P->F auto-close remediation dispatch
3
+ * Dispatches parameter updates via /qgsd:quick for drift entries,
4
+ * flags investigation for issue/invariant entries.
5
+ *
6
+ * Requirements: PF-04, PF-05
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { transitionDebtEntry } = require('./debt-state-machine.cjs');
12
+ const { readDebtLedger, writeDebtLedger } = require('./debt-ledger.cjs');
13
+
14
+ /**
15
+ * Auto-close P->F divergent entries with two-track dispatch
16
+ * @param {object} residual - The p_to_f residual from sweepPtoF
17
+ * @param {object} [options]
18
+ * @param {function} [options.spawnTool] - Mock spawn function for testing
19
+ * @param {function} [options.isNumericThreshold] - Mock isNumericThreshold for testing
20
+ * @param {function} [options.writeDebtLedger] - Mock write function for testing
21
+ * @param {string} [options.ledgerPath] - Path to debt.json
22
+ * @param {string} [options.specDir] - Path to spec directory
23
+ * @returns {{ actions_taken: string[], entries_processed: number }}
24
+ */
25
+ function autoClosePtoF(residual, options = {}) {
26
+ if (!residual || residual.residual === 0) {
27
+ return { actions_taken: [], entries_processed: 0 };
28
+ }
29
+
30
+ const isNumericFn = options.isNumericThreshold || require('./isNumericThreshold.cjs').isNumericThreshold;
31
+ const writeLedgerFn = options.writeDebtLedger || writeDebtLedger;
32
+ const spawnFn = options.spawnTool || null;
33
+ const specDir = options.specDir;
34
+ const ledgerPath = options.ledgerPath;
35
+
36
+ const actions = [];
37
+ const divergent = residual.detail?.divergent_entries || [];
38
+
39
+ if (divergent.length === 0) {
40
+ return { actions_taken: [], entries_processed: 0 };
41
+ }
42
+
43
+ // Read full ledger to get actual entry objects for mutation
44
+ const ledger = ledgerPath ? readDebtLedger(ledgerPath) : { debt_entries: [] };
45
+
46
+ for (const div of divergent) {
47
+ // Find actual entry in ledger
48
+ const entryIdx = (ledger.debt_entries || []).findIndex(e => e.id === div.id);
49
+ if (entryIdx === -1) continue;
50
+
51
+ let entry = ledger.debt_entries[entryIdx];
52
+
53
+ // Freeze: transition acknowledged -> resolving
54
+ const freezeResult = transitionDebtEntry(entry, 'resolving');
55
+ if (!freezeResult.success) {
56
+ actions.push(`Cannot freeze entry ${entry.id}: ${freezeResult.error}`);
57
+ continue;
58
+ }
59
+ entry = freezeResult.entry;
60
+ ledger.debt_entries[entryIdx] = entry;
61
+
62
+ // Determine track: parameter update vs investigation
63
+ const isParameter = entry.issue_type === 'drift' && isNumericFn(entry.formal_ref, { specDir });
64
+
65
+ if (isParameter) {
66
+ // Parameter update track: dispatch /qgsd:quick
67
+ let dispatchOk = false;
68
+
69
+ if (spawnFn) {
70
+ const result = spawnFn('qgsd-quick.cjs', [
71
+ `Update formal parameter ${entry.formal_ref} to match production`,
72
+ `Production measurement: ${entry.meta?.measured_value}`,
73
+ `Current formal: ${div.expected}`,
74
+ ]);
75
+ dispatchOk = result && result.ok;
76
+ }
77
+
78
+ entry.meta = entry.meta || {};
79
+ entry.meta.remediation_log = `${new Date().toISOString()} - Dispatched parameter update: ${entry.formal_ref}`;
80
+
81
+ if (dispatchOk) {
82
+ const resolveResult = transitionDebtEntry(entry, 'resolved');
83
+ if (resolveResult.success) {
84
+ entry = resolveResult.entry;
85
+ ledger.debt_entries[entryIdx] = entry;
86
+ }
87
+ actions.push(`Dispatched parameter update for ${entry.id}`);
88
+ } else {
89
+ // Failed dispatch - entry stays in resolving (state machine does not allow resolving->acknowledged)
90
+ // It will need manual triage to resolve or a future mechanism to revert
91
+ actions.push(`Failed to dispatch parameter update for ${entry.id}`);
92
+ }
93
+ } else {
94
+ // Investigation track: flag only, do NOT auto-remediate
95
+ entry.meta = entry.meta || {};
96
+ entry.meta.investigation_notes = `${new Date().toISOString()} - Production diverged from ${entry.formal_ref || 'unknown'}. Requires manual review.`;
97
+ actions.push(`Flagged ${entry.id} for investigation`);
98
+ // Entry stays in 'resolving' status — investigation needed
99
+ }
100
+ }
101
+
102
+ // Persist updated ledger to disk
103
+ if (ledgerPath) {
104
+ writeLedgerFn(ledgerPath, ledger);
105
+ }
106
+
107
+ return { actions_taken: actions, entries_processed: divergent.length };
108
+ }
109
+
110
+ module.exports = { autoClosePtoF };