@ai-sdlc/orchestrator 0.5.0 → 0.9.0

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 (227) hide show
  1. package/dist/action-enforcement.d.ts +26 -0
  2. package/dist/action-enforcement.js +70 -0
  3. package/dist/admission-composite.d.ts +67 -0
  4. package/dist/admission-composite.js +158 -0
  5. package/dist/admission-enrichment.d.ts +142 -0
  6. package/dist/admission-enrichment.js +331 -0
  7. package/dist/admission-hc.d.ts +62 -0
  8. package/dist/admission-hc.js +83 -0
  9. package/dist/admission-score.d.ts +148 -0
  10. package/dist/admission-score.js +237 -0
  11. package/dist/analysis/analyzer.js +3 -2
  12. package/dist/analysis/convention-detector.d.ts +85 -2
  13. package/dist/analysis/convention-detector.js +375 -70
  14. package/dist/analysis/diff-analyzer.d.ts +33 -0
  15. package/dist/analysis/diff-analyzer.js +122 -0
  16. package/dist/analysis/hotspot-analyzer.js +3 -1
  17. package/dist/analysis/index.d.ts +2 -1
  18. package/dist/analysis/index.js +2 -1
  19. package/dist/artifacts/index.d.ts +65 -0
  20. package/dist/artifacts/index.js +142 -0
  21. package/dist/backlog-adapter.d.ts +121 -0
  22. package/dist/backlog-adapter.js +438 -0
  23. package/dist/calibration.d.ts +43 -0
  24. package/dist/calibration.js +76 -0
  25. package/dist/cli/commands/agents.d.ts +24 -0
  26. package/dist/cli/commands/agents.js +66 -1
  27. package/dist/cli/commands/git-remote.d.ts +49 -0
  28. package/dist/cli/commands/git-remote.js +91 -0
  29. package/dist/cli/commands/health.d.ts +4 -0
  30. package/dist/cli/commands/health.js +13 -1
  31. package/dist/cli/commands/init.d.ts +26 -0
  32. package/dist/cli/commands/init.js +164 -22
  33. package/dist/cli/commands/mcp-setup.d.ts +31 -0
  34. package/dist/cli/commands/mcp-setup.js +78 -8
  35. package/dist/cli/formatters/table.js +15 -2
  36. package/dist/cli/index.d.ts +14 -1
  37. package/dist/cli/index.js +81 -20
  38. package/dist/cli/versions.d.ts +57 -0
  39. package/dist/cli/versions.js +128 -0
  40. package/dist/code-area-classifier.d.ts +21 -0
  41. package/dist/code-area-classifier.js +48 -0
  42. package/dist/config.d.ts +33 -1
  43. package/dist/config.js +78 -8
  44. package/dist/cycle-utils.d.ts +51 -0
  45. package/dist/cycle-utils.js +77 -0
  46. package/dist/database/adapters/external.d.ts +24 -0
  47. package/dist/database/adapters/external.js +80 -0
  48. package/dist/database/adapters/neon.d.ts +41 -0
  49. package/dist/database/adapters/neon.js +98 -0
  50. package/dist/database/adapters/pg-snapshot-restore.d.ts +28 -0
  51. package/dist/database/adapters/pg-snapshot-restore.js +68 -0
  52. package/dist/database/adapters/sqlite-copy.d.ts +32 -0
  53. package/dist/database/adapters/sqlite-copy.js +145 -0
  54. package/dist/database/connection-injection.d.ts +35 -0
  55. package/dist/database/connection-injection.js +93 -0
  56. package/dist/database/index.d.ts +12 -0
  57. package/dist/database/index.js +23 -0
  58. package/dist/database/registry.d.ts +13 -0
  59. package/dist/database/registry.js +27 -0
  60. package/dist/database/topology.d.ts +52 -0
  61. package/dist/database/topology.js +44 -0
  62. package/dist/database/types.d.ts +89 -0
  63. package/dist/database/types.js +26 -0
  64. package/dist/defaults.d.ts +5 -0
  65. package/dist/defaults.js +5 -0
  66. package/dist/design-authority.d.ts +40 -0
  67. package/dist/design-authority.js +71 -0
  68. package/dist/design-lookahead.d.ts +64 -0
  69. package/dist/design-lookahead.js +86 -0
  70. package/dist/design-quality-trend.d.ts +87 -0
  71. package/dist/design-quality-trend.js +190 -0
  72. package/dist/design-system-context.d.ts +46 -0
  73. package/dist/design-system-context.js +80 -0
  74. package/dist/design-system-correction-loop.d.ts +64 -0
  75. package/dist/design-system-correction-loop.js +128 -0
  76. package/dist/design-system-metrics.d.ts +61 -0
  77. package/dist/design-system-metrics.js +104 -0
  78. package/dist/design-system-stewardship.d.ts +22 -0
  79. package/dist/design-system-stewardship.js +85 -0
  80. package/dist/design-system-validation.d.ts +37 -0
  81. package/dist/design-system-validation.js +88 -0
  82. package/dist/dispatch/index.d.ts +4 -0
  83. package/dist/dispatch/index.js +4 -0
  84. package/dist/dispatch/merge-gate.d.ts +46 -0
  85. package/dist/dispatch/merge-gate.js +90 -0
  86. package/dist/dispatch/requeue.d.ts +57 -0
  87. package/dist/dispatch/requeue.js +131 -0
  88. package/dist/dispatch/worker-pool.d.ts +62 -0
  89. package/dist/dispatch/worker-pool.js +60 -0
  90. package/dist/execute.d.ts +40 -0
  91. package/dist/execute.js +295 -30
  92. package/dist/fix-ci.js +40 -3
  93. package/dist/fix-review.d.ts +66 -0
  94. package/dist/fix-review.js +448 -0
  95. package/dist/harness/adapters/claude-code.d.ts +29 -0
  96. package/dist/harness/adapters/claude-code.js +191 -0
  97. package/dist/harness/adapters/codex.d.ts +25 -0
  98. package/dist/harness/adapters/codex.js +61 -0
  99. package/dist/harness/independence.d.ts +51 -0
  100. package/dist/harness/independence.js +67 -0
  101. package/dist/harness/index.d.ts +14 -0
  102. package/dist/harness/index.js +20 -0
  103. package/dist/harness/registry.d.ts +17 -0
  104. package/dist/harness/registry.js +31 -0
  105. package/dist/harness/types.d.ts +123 -0
  106. package/dist/harness/types.js +8 -0
  107. package/dist/harness/version-probe.d.ts +14 -0
  108. package/dist/harness/version-probe.js +123 -0
  109. package/dist/index.d.ts +39 -5
  110. package/dist/index.js +48 -3
  111. package/dist/models/classifier.d.ts +76 -0
  112. package/dist/models/classifier.js +221 -0
  113. package/dist/models/index.d.ts +3 -0
  114. package/dist/models/index.js +3 -0
  115. package/dist/models/registry.d.ts +97 -0
  116. package/dist/models/registry.js +173 -0
  117. package/dist/pillar-breakdown.d.ts +63 -0
  118. package/dist/pillar-breakdown.js +153 -0
  119. package/dist/pipeline-cycle-detector.d.ts +70 -0
  120. package/dist/pipeline-cycle-detector.js +111 -0
  121. package/dist/priority.d.ts +7 -76
  122. package/dist/priority.js +18 -5
  123. package/dist/review-meta.d.ts +65 -0
  124. package/dist/review-meta.js +149 -0
  125. package/dist/review.d.ts +35 -0
  126. package/dist/review.js +84 -0
  127. package/dist/runners/claude-code-sdk.d.ts +31 -0
  128. package/dist/runners/claude-code-sdk.js +238 -0
  129. package/dist/runners/claude-code.d.ts +3 -0
  130. package/dist/runners/claude-code.js +319 -73
  131. package/dist/runners/codex.js +4 -1
  132. package/dist/runners/copilot.js +4 -1
  133. package/dist/runners/cursor.js +4 -1
  134. package/dist/runners/git-utils.d.ts +81 -0
  135. package/dist/runners/git-utils.js +201 -0
  136. package/dist/runners/index.d.ts +5 -1
  137. package/dist/runners/index.js +4 -0
  138. package/dist/runners/review-agent.d.ts +67 -0
  139. package/dist/runners/review-agent.js +350 -0
  140. package/dist/runners/runner-registry.js +10 -0
  141. package/dist/runners/sdk-review-runner.d.ts +65 -0
  142. package/dist/runners/sdk-review-runner.js +185 -0
  143. package/dist/runners/security-triage.d.ts +20 -4
  144. package/dist/runners/security-triage.js +42 -14
  145. package/dist/runners/types.d.ts +25 -0
  146. package/dist/runtime/attestations.d.ts +274 -0
  147. package/dist/runtime/attestations.js +460 -0
  148. package/dist/runtime/git-env.d.ts +53 -0
  149. package/dist/runtime/git-env.js +60 -0
  150. package/dist/runtime/index.d.ts +7 -0
  151. package/dist/runtime/index.js +7 -0
  152. package/dist/runtime/parallelism-flag.d.ts +10 -0
  153. package/dist/runtime/parallelism-flag.js +18 -0
  154. package/dist/runtime/port-allocator.d.ts +21 -0
  155. package/dist/runtime/port-allocator.js +66 -0
  156. package/dist/runtime/worktree-pool.d.ts +86 -0
  157. package/dist/runtime/worktree-pool.js +204 -0
  158. package/dist/runtime/worktree.d.ts +25 -0
  159. package/dist/runtime/worktree.js +111 -0
  160. package/dist/sa-scoring/auto-calibrate.d.ts +69 -0
  161. package/dist/sa-scoring/auto-calibrate.js +107 -0
  162. package/dist/sa-scoring/c1-sa2-computable.d.ts +42 -0
  163. package/dist/sa-scoring/c1-sa2-computable.js +59 -0
  164. package/dist/sa-scoring/composite.d.ts +107 -0
  165. package/dist/sa-scoring/composite.js +139 -0
  166. package/dist/sa-scoring/depparse-client.d.ts +79 -0
  167. package/dist/sa-scoring/depparse-client.js +187 -0
  168. package/dist/sa-scoring/did-compiler.d.ts +122 -0
  169. package/dist/sa-scoring/did-compiler.js +286 -0
  170. package/dist/sa-scoring/drift-monitor.d.ts +84 -0
  171. package/dist/sa-scoring/drift-monitor.js +186 -0
  172. package/dist/sa-scoring/exemplar-bank.d.ts +78 -0
  173. package/dist/sa-scoring/exemplar-bank.js +154 -0
  174. package/dist/sa-scoring/feedback-store.d.ts +100 -0
  175. package/dist/sa-scoring/feedback-store.js +156 -0
  176. package/dist/sa-scoring/index.d.ts +71 -0
  177. package/dist/sa-scoring/index.js +158 -0
  178. package/dist/sa-scoring/layer1-deterministic.d.ts +115 -0
  179. package/dist/sa-scoring/layer1-deterministic.js +298 -0
  180. package/dist/sa-scoring/layer2-structural.d.ts +71 -0
  181. package/dist/sa-scoring/layer2-structural.js +151 -0
  182. package/dist/sa-scoring/layer3-llm.d.ts +86 -0
  183. package/dist/sa-scoring/layer3-llm.js +282 -0
  184. package/dist/sa-scoring/rescore-orchestrator.d.ts +52 -0
  185. package/dist/sa-scoring/rescore-orchestrator.js +47 -0
  186. package/dist/scheduling/burn-down.d.ts +27 -0
  187. package/dist/scheduling/burn-down.js +43 -0
  188. package/dist/scheduling/calibration.d.ts +42 -0
  189. package/dist/scheduling/calibration.js +150 -0
  190. package/dist/scheduling/index.d.ts +8 -0
  191. package/dist/scheduling/index.js +8 -0
  192. package/dist/scheduling/ledger.d.ts +59 -0
  193. package/dist/scheduling/ledger.js +216 -0
  194. package/dist/scheduling/off-peak.d.ts +27 -0
  195. package/dist/scheduling/off-peak.js +112 -0
  196. package/dist/scheduling/schedule-decision.d.ts +39 -0
  197. package/dist/scheduling/schedule-decision.js +89 -0
  198. package/dist/scheduling/tier-analysis.d.ts +47 -0
  199. package/dist/scheduling/tier-analysis.js +81 -0
  200. package/dist/scheduling/types.d.ts +140 -0
  201. package/dist/scheduling/types.js +11 -0
  202. package/dist/shared.d.ts +13 -0
  203. package/dist/shared.js +32 -0
  204. package/dist/state/index.d.ts +1 -1
  205. package/dist/state/schema.d.ts +6 -1
  206. package/dist/state/schema.js +276 -1
  207. package/dist/state/store.d.ts +71 -1
  208. package/dist/state/store.js +464 -7
  209. package/dist/state/types.d.ts +174 -0
  210. package/dist/types.d.ts +1 -1
  211. package/dist/validate-agent-output.js +4 -1
  212. package/dist/watch.js +6 -0
  213. package/dist/workflow-patterns/artifact-writer.d.ts +16 -0
  214. package/dist/workflow-patterns/artifact-writer.js +34 -0
  215. package/dist/workflow-patterns/classifiers.d.ts +10 -0
  216. package/dist/workflow-patterns/classifiers.js +72 -0
  217. package/dist/workflow-patterns/detector.d.ts +27 -0
  218. package/dist/workflow-patterns/detector.js +186 -0
  219. package/dist/workflow-patterns/index.d.ts +8 -0
  220. package/dist/workflow-patterns/index.js +7 -0
  221. package/dist/workflow-patterns/proposal-generator.d.ts +15 -0
  222. package/dist/workflow-patterns/proposal-generator.js +183 -0
  223. package/dist/workflow-patterns/telemetry-ingest.d.ts +27 -0
  224. package/dist/workflow-patterns/telemetry-ingest.js +103 -0
  225. package/dist/workflow-patterns/types.d.ts +61 -0
  226. package/dist/workflow-patterns/types.js +11 -0
  227. package/package.json +2 -2
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Cryptographic review attestations (AISDLC-74).
3
+ *
4
+ * `/ai-sdlc execute` runs three reviewer subagents (code/test/security) locally
5
+ * before pushing. CI then re-ran the same reviewers via `Post Review Results` —
6
+ * burning tokens on duplicate work.
7
+ *
8
+ * This module provides the primitives `/ai-sdlc execute` and the
9
+ * `verify-attestation.yml` workflow share to skip CI review when a valid local
10
+ * attestation exists. The shape is a DSSE envelope (in-toto / SLSA pattern)
11
+ * carrying a versioned predicate that commits to the commit SHA, diff hash,
12
+ * policy hash, and reviewer agent file hashes — so CI can reject envelopes
13
+ * after force-push, after a policy edit, or after a reviewer agent change.
14
+ *
15
+ * ## Threat model (in-scope)
16
+ *
17
+ * - Lazy contributor faking attestation → signature mismatch
18
+ * - Copy-pasted attestation from another PR → subject digest mismatch
19
+ * - Replay after diff changed (force-push) → diffHash mismatch
20
+ * - Attestation issued before a policy edit → policyHash mismatch
21
+ * - Stale reviewer-agent attestation → agentFileHash mismatch
22
+ * - Schema drift / forward-compat smuggling → schemaVersion enforcement
23
+ *
24
+ * Out of scope: compromised dev machine, compromised CI runner, collusion.
25
+ *
26
+ * ## Why ed25519 + Node's built-in crypto (no Sigstore)
27
+ *
28
+ * The keys are project-controlled, committed in `.ai-sdlc/trusted-reviewers.yaml`,
29
+ * and small (32-byte). Sigstore would add Fulcio + Rekor + transparency log
30
+ * infrastructure for no benefit at this scale. ed25519 is what `ssh-keygen
31
+ * -t ed25519` and `git commit -S` already use; Node's `crypto.sign(null, ...)`
32
+ * supports it natively.
33
+ */
34
+ import { createHash, generateKeyPairSync, sign, verify } from 'node:crypto';
35
+ /**
36
+ * The currently-accepted predicate schema versions. CI rejects any envelope
37
+ * whose `payload.schemaVersion` is not in this allowlist — this is the
38
+ * forward-compatibility hatch (we add a new version here when we change the
39
+ * predicate shape, and CI keeps accepting v1 until we explicitly remove it).
40
+ *
41
+ * Exported so the `verify-attestation` workflow can `import`/inline it.
42
+ */
43
+ export const ACCEPTED_SCHEMA_VERSIONS = ['v1'];
44
+ /**
45
+ * The DSSE PAE payload type for our predicate. DSSE spec mandates a payload
46
+ * type URI — we use a project-controlled vendor URI rather than the
47
+ * in-toto Statement format (which would force us to shape the predicate
48
+ * around `_type` + `subject` at the envelope layer instead of the predicate).
49
+ */
50
+ export const DSSE_PAYLOAD_TYPE = 'application/vnd.ai-sdlc.attestation+json';
51
+ // ─── Schema validation ────────────────────────────────────────────
52
+ //
53
+ // Defense-in-depth against GITHUB_OUTPUT injection (and any other
54
+ // downstream consumer that interpolates predicate fields into a
55
+ // structured format). Every field that the verifier ever interpolates
56
+ // into a `reason` string MUST be regex-validated to a known-safe
57
+ // charset BEFORE the rest of `verifyAttestation` runs. If validation
58
+ // fails, we return a FIXED reason string that does not embed the bad
59
+ // value — never give the attacker a way to smuggle their payload past
60
+ // us by burying it in our reason text.
61
+ //
62
+ // Mirror of `.ai-sdlc/schemas/attestation.v1.schema.json` — kept in
63
+ // sync by the `validatePredicateShape` test ('schema mirror in sync').
64
+ /** sha1 git commit (40 lowercase hex chars). */
65
+ const SHA1_HEX = /^[0-9a-f]{40}$/;
66
+ /** sha256 hex (64 lowercase hex chars). */
67
+ const SHA256_HEX = /^[0-9a-f]{64}$/;
68
+ /** ISO 8601 timestamp — permissive enough for `new Date().toISOString()`. */
69
+ const ISO_8601 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
70
+ /** Free-form short identifier — letters, digits, dot, dash, underscore. */
71
+ const SHORT_ID = /^[A-Za-z0-9._-]+$/;
72
+ /**
73
+ * `harnessNote` is the only field the operator can put long-form text
74
+ * in. We allow letters/digits/punctuation/whitespace but reject CR/LF
75
+ * (which is what attackers need to inject newline-key=value pairs).
76
+ */
77
+ const SAFE_TEXT = /^[^\r\n]*$/;
78
+ /**
79
+ * Validate a parsed predicate against the v1 schema regex patterns.
80
+ *
81
+ * Returns `null` when the predicate is shape-valid; otherwise returns
82
+ * a static failure reason that does NOT embed any user-controlled
83
+ * value (just the field path). This is the load-bearing property:
84
+ * the malicious value never reaches the `reason` string, so it can't
85
+ * propagate to GITHUB_OUTPUT or commit-status descriptions.
86
+ */
87
+ export function validatePredicateShape(parsed) {
88
+ if (parsed === null || typeof parsed !== 'object') {
89
+ return 'schema validation failed: predicate must be an object';
90
+ }
91
+ const p = parsed;
92
+ // schemaVersion — string from the accepted enum.
93
+ if (typeof p['schemaVersion'] !== 'string') {
94
+ return 'schema validation failed: schemaVersion must be a string';
95
+ }
96
+ if (!ACCEPTED_SCHEMA_VERSIONS.includes(p['schemaVersion'])) {
97
+ // Bounded set — safe to surface the version. We also re-check this
98
+ // in `verifyAttestation` after shape validation so the error
99
+ // surface is consistent between schema-rejection and allowlist.
100
+ return 'schema validation failed: schemaVersion not in accepted enum';
101
+ }
102
+ // subject.digest.sha1 — 40 hex chars.
103
+ const subject = p['subject'];
104
+ if (subject === null || typeof subject !== 'object') {
105
+ return 'schema validation failed: subject must be an object';
106
+ }
107
+ const digest = subject['digest'];
108
+ if (digest === null || typeof digest !== 'object') {
109
+ return 'schema validation failed: subject.digest must be an object';
110
+ }
111
+ const sha1 = digest['sha1'];
112
+ if (typeof sha1 !== 'string' || !SHA1_HEX.test(sha1)) {
113
+ return 'schema validation failed: subject.digest.sha1 does not match pattern';
114
+ }
115
+ // diffHash + policyHash — 64 hex chars each.
116
+ for (const field of ['diffHash', 'policyHash']) {
117
+ const v = p[field];
118
+ if (typeof v !== 'string' || !SHA256_HEX.test(v)) {
119
+ return `schema validation failed: ${field} does not match pattern`;
120
+ }
121
+ }
122
+ // pluginVersion — short ID (no CR/LF, no `=`).
123
+ const pluginVersion = p['pluginVersion'];
124
+ if (typeof pluginVersion !== 'string' ||
125
+ pluginVersion.length === 0 ||
126
+ !SHORT_ID.test(pluginVersion)) {
127
+ return 'schema validation failed: pluginVersion does not match pattern';
128
+ }
129
+ // iterationCount — positive integer.
130
+ const iterationCount = p['iterationCount'];
131
+ if (typeof iterationCount !== 'number' ||
132
+ !Number.isInteger(iterationCount) ||
133
+ iterationCount < 1) {
134
+ return 'schema validation failed: iterationCount must be a positive integer';
135
+ }
136
+ // harnessNote — free-form, but no CR/LF (else it can inject
137
+ // newlines into a downstream key=value writer).
138
+ const harnessNote = p['harnessNote'];
139
+ if (typeof harnessNote !== 'string' || !SAFE_TEXT.test(harnessNote)) {
140
+ return 'schema validation failed: harnessNote contains forbidden characters';
141
+ }
142
+ // signedAt — ISO 8601.
143
+ const signedAt = p['signedAt'];
144
+ if (typeof signedAt !== 'string' || !ISO_8601.test(signedAt)) {
145
+ return 'schema validation failed: signedAt does not match ISO 8601 pattern';
146
+ }
147
+ // reviewers — array of objects, each with regex-validated fields.
148
+ const reviewers = p['reviewers'];
149
+ if (!Array.isArray(reviewers) || reviewers.length === 0) {
150
+ return 'schema validation failed: reviewers must be a non-empty array';
151
+ }
152
+ for (let i = 0; i < reviewers.length; i++) {
153
+ const r = reviewers[i];
154
+ if (r === null || typeof r !== 'object') {
155
+ return 'schema validation failed: reviewer entry must be an object';
156
+ }
157
+ const rec = r;
158
+ const agentId = rec['agentId'];
159
+ if (typeof agentId !== 'string' || agentId.length === 0 || !SHORT_ID.test(agentId)) {
160
+ return 'schema validation failed: reviewer agentId does not match pattern';
161
+ }
162
+ const agentFileHash = rec['agentFileHash'];
163
+ if (typeof agentFileHash !== 'string' || !SHA256_HEX.test(agentFileHash)) {
164
+ return 'schema validation failed: reviewer agentFileHash does not match pattern';
165
+ }
166
+ const harness = rec['harness'];
167
+ if (typeof harness !== 'string' || harness.length === 0 || !SHORT_ID.test(harness)) {
168
+ return 'schema validation failed: reviewer harness does not match pattern';
169
+ }
170
+ if (typeof rec['approved'] !== 'boolean') {
171
+ return 'schema validation failed: reviewer approved must be a boolean';
172
+ }
173
+ const findings = rec['findings'];
174
+ if (findings === null || typeof findings !== 'object') {
175
+ return 'schema validation failed: reviewer findings must be an object';
176
+ }
177
+ for (const sev of ['critical', 'major', 'minor', 'suggestion']) {
178
+ const n = findings[sev];
179
+ if (typeof n !== 'number' || !Number.isInteger(n) || n < 0) {
180
+ return 'schema validation failed: reviewer findings count must be a non-negative integer';
181
+ }
182
+ }
183
+ }
184
+ return null;
185
+ }
186
+ /**
187
+ * The set of reviewer agent IDs the verifier expects to see in every
188
+ * attestation. Exported so callers (verify-attestation.mjs) can
189
+ * cross-check that all three reviewers are present + match.
190
+ *
191
+ * Frozen to discourage callers from mutating it.
192
+ */
193
+ export const REQUIRED_REVIEWER_AGENT_IDS = Object.freeze([
194
+ 'code-reviewer',
195
+ 'test-reviewer',
196
+ 'security-reviewer',
197
+ ]);
198
+ /**
199
+ * Compute a sha256 hex digest. Single source of truth for the hashing
200
+ * algorithm — every predicate field that ends in `Hash` flows through here.
201
+ */
202
+ export function sha256Hex(input) {
203
+ return createHash('sha256').update(input).digest('hex');
204
+ }
205
+ /** Compute a sha1 hex digest (used for git commit SHAs in the subject). */
206
+ export function sha1Hex(input) {
207
+ return createHash('sha1').update(input).digest('hex');
208
+ }
209
+ /**
210
+ * Build the predicate payload from raw inputs. Pure function — no I/O,
211
+ * no signing. The caller (`/ai-sdlc execute` Step 10) reads files and git
212
+ * output, then hands them here.
213
+ */
214
+ export function buildPredicate(inputs) {
215
+ if (!/^[0-9a-f]{40}$/i.test(inputs.commitSha)) {
216
+ throw new Error(`buildPredicate: commitSha must be a 40-char hex SHA-1, got ${inputs.commitSha}`);
217
+ }
218
+ return {
219
+ schemaVersion: 'v1',
220
+ subject: { digest: { sha1: inputs.commitSha.toLowerCase() } },
221
+ diffHash: sha256Hex(inputs.diff),
222
+ policyHash: sha256Hex(inputs.policy),
223
+ reviewers: inputs.reviewers.map((r) => ({
224
+ agentId: r.agentId,
225
+ agentFileHash: sha256Hex(r.agentFileContent),
226
+ harness: r.harness,
227
+ approved: r.approved,
228
+ findings: { ...r.findings },
229
+ })),
230
+ pluginVersion: inputs.pluginVersion,
231
+ iterationCount: inputs.iterationCount,
232
+ harnessNote: inputs.harnessNote,
233
+ signedAt: inputs.signedAt ?? new Date().toISOString(),
234
+ };
235
+ }
236
+ /**
237
+ * DSSE Pre-Authentication Encoding. Per the spec
238
+ * (https://github.com/secure-systems-lab/dsse/blob/master/protocol.md):
239
+ *
240
+ * PAE(type, body) = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body
241
+ *
242
+ * Lengths are decimal ASCII byte-counts of the UTF-8 encoding. Signing the
243
+ * PAE — not the raw payload — is what gives DSSE its domain separation:
244
+ * a signature over a payload of one `payloadType` cannot be replayed onto
245
+ * a payload of a different type.
246
+ */
247
+ export function paeEncode(payloadType, payload) {
248
+ const typeBuf = Buffer.from(payloadType, 'utf-8');
249
+ const prefix = Buffer.from(`DSSEv1 ${typeBuf.length} ${payloadType} ${payload.length} `, 'utf-8');
250
+ return Buffer.concat([prefix, payload]);
251
+ }
252
+ /** Generate a fresh ed25519 keypair as PEM strings (for `/ai-sdlc init-signing-key`). */
253
+ export function generateSigningKeyPair() {
254
+ const { privateKey, publicKey } = generateKeyPairSync('ed25519');
255
+ return {
256
+ privateKeyPem: privateKey.export({ format: 'pem', type: 'pkcs8' }).toString(),
257
+ publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }).toString(),
258
+ };
259
+ }
260
+ /**
261
+ * Sign a predicate, producing a DSSE envelope.
262
+ *
263
+ * Throws if `predicate.schemaVersion` is not in `ACCEPTED_SCHEMA_VERSIONS` —
264
+ * we don't want to issue an envelope that we'd reject ourselves.
265
+ */
266
+ export function signAttestation(opts) {
267
+ if (!ACCEPTED_SCHEMA_VERSIONS.includes(opts.predicate.schemaVersion)) {
268
+ throw new Error(`signAttestation: schemaVersion '${opts.predicate.schemaVersion}' is not in the accepted allowlist [${ACCEPTED_SCHEMA_VERSIONS.join(', ')}]`);
269
+ }
270
+ const payloadJson = Buffer.from(JSON.stringify(opts.predicate), 'utf-8');
271
+ const pae = paeEncode(DSSE_PAYLOAD_TYPE, payloadJson);
272
+ const signature = sign(null, pae, opts.privateKeyPem);
273
+ return {
274
+ payloadType: DSSE_PAYLOAD_TYPE,
275
+ payload: payloadJson.toString('base64'),
276
+ signatures: [
277
+ {
278
+ keyid: opts.keyid,
279
+ sig: signature.toString('base64'),
280
+ },
281
+ ],
282
+ };
283
+ }
284
+ /**
285
+ * Verify a DSSE envelope. Returns a discriminated union — `{ valid: true }`
286
+ * with the parsed predicate + matched trusted reviewer, or `{ valid: false }`
287
+ * with a single human-readable reason string.
288
+ *
289
+ * The reason string is what gets posted to the commit status
290
+ * (`ai-sdlc/attestation: invalid (<reason>)`), so keep it short and specific.
291
+ */
292
+ export function verifyAttestation(opts) {
293
+ const allowlist = opts.acceptedSchemaVersions ?? ACCEPTED_SCHEMA_VERSIONS;
294
+ // ── Parse the envelope ────────────────────────────────────────
295
+ if (opts.envelope.payloadType !== DSSE_PAYLOAD_TYPE) {
296
+ return {
297
+ valid: false,
298
+ reason: `payloadType mismatch: expected ${DSSE_PAYLOAD_TYPE}, got ${opts.envelope.payloadType}`,
299
+ };
300
+ }
301
+ if (!Array.isArray(opts.envelope.signatures) || opts.envelope.signatures.length === 0) {
302
+ return { valid: false, reason: 'envelope has no signatures' };
303
+ }
304
+ // Node's `Buffer.from(s, 'base64')` does NOT throw on invalid input —
305
+ // it silently drops non-base64 chars. We round-trip through .toString
306
+ // ('base64') below as part of the JSON-parse step; PAE re-encoding then
307
+ // catches any tampering at signature-verify time.
308
+ if (typeof opts.envelope.payload !== 'string' || opts.envelope.payload.length === 0) {
309
+ return { valid: false, reason: 'envelope payload is empty or non-string' };
310
+ }
311
+ const payloadJson = Buffer.from(opts.envelope.payload, 'base64');
312
+ let parsed;
313
+ try {
314
+ parsed = JSON.parse(payloadJson.toString('utf-8'));
315
+ }
316
+ catch {
317
+ return { valid: false, reason: 'payload is not valid JSON' };
318
+ }
319
+ // ── Schema validation (REGEX-BOUND) ───────────────────────────
320
+ // This MUST run before any predicate field is interpolated into a
321
+ // reason string. The shape validator returns a fixed (non-interpolated)
322
+ // reason on failure so a malicious value cannot smuggle CR/LF or
323
+ // `=` into our output. See validatePredicateShape for rationale.
324
+ const shapeError = validatePredicateShape(parsed);
325
+ if (shapeError !== null) {
326
+ return { valid: false, reason: shapeError };
327
+ }
328
+ const predicate = parsed;
329
+ // ── Schema version allowlist (post-shape) ─────────────────────
330
+ // Belt-and-braces: the shape validator already enforced membership in
331
+ // ACCEPTED_SCHEMA_VERSIONS, but callers can override the allowlist via
332
+ // opts.acceptedSchemaVersions for forward-compat tests, so re-check here.
333
+ if (!allowlist.includes(predicate.schemaVersion)) {
334
+ return {
335
+ valid: false,
336
+ reason: `schemaVersion '${predicate.schemaVersion}' not in allowlist [${allowlist.join(', ')}]`,
337
+ };
338
+ }
339
+ // ── Signature (any-of-N pubkeys) ──────────────────────────────
340
+ const pae = paeEncode(DSSE_PAYLOAD_TYPE, payloadJson);
341
+ let matchedReviewer = null;
342
+ for (const sig of opts.envelope.signatures) {
343
+ let sigBytes;
344
+ try {
345
+ sigBytes = Buffer.from(sig.sig, 'base64');
346
+ }
347
+ catch {
348
+ continue;
349
+ }
350
+ for (const reviewer of opts.trustedReviewers) {
351
+ try {
352
+ if (verify(null, pae, reviewer.pubkey, sigBytes)) {
353
+ matchedReviewer = reviewer;
354
+ break;
355
+ }
356
+ }
357
+ catch {
358
+ // Bad pubkey PEM — skip, try next.
359
+ }
360
+ }
361
+ if (matchedReviewer)
362
+ break;
363
+ }
364
+ if (!matchedReviewer) {
365
+ return {
366
+ valid: false,
367
+ reason: 'signature did not match any trusted reviewer pubkey',
368
+ };
369
+ }
370
+ // ── Bind to PR state ──────────────────────────────────────────
371
+ // Note: every field interpolated into a reason below has already been
372
+ // regex-bounded by validatePredicateShape (sha1/sha256 hex, SHORT_ID
373
+ // for agentId), so embedding them in reason strings cannot inject
374
+ // CR/LF or `=` into downstream key=value writers.
375
+ const expectedSha = opts.expected.commitSha.toLowerCase();
376
+ if (predicate.subject.digest.sha1.toLowerCase() !== expectedSha) {
377
+ return {
378
+ valid: false,
379
+ reason: `subject digest mismatch (envelope was signed for a different commit)`,
380
+ };
381
+ }
382
+ if (predicate.diffHash !== opts.expected.diffHash) {
383
+ return { valid: false, reason: 'diffHash mismatch (PR diff changed since attestation)' };
384
+ }
385
+ if (predicate.policyHash !== opts.expected.policyHash) {
386
+ return {
387
+ valid: false,
388
+ reason: 'policyHash mismatch (.ai-sdlc/review-policy.md changed since attestation)',
389
+ };
390
+ }
391
+ for (const r of predicate.reviewers) {
392
+ const expectedHash = opts.expected.expectedAgentFileHashes[r.agentId];
393
+ if (expectedHash && expectedHash !== r.agentFileHash) {
394
+ return {
395
+ valid: false,
396
+ reason: `agentFileHash mismatch for reviewer '${r.agentId}' (agent file changed since attestation)`,
397
+ };
398
+ }
399
+ }
400
+ // ── Reviewer-set completeness ────────────────────────────────
401
+ // Every attestation MUST cover all three required reviewers (code,
402
+ // test, security). Without this, a contributor could ship an
403
+ // attestation containing only `code-reviewer` and bypass the test
404
+ // and security review entirely.
405
+ const present = new Set(predicate.reviewers.map((r) => r.agentId));
406
+ for (const required of REQUIRED_REVIEWER_AGENT_IDS) {
407
+ if (!present.has(required)) {
408
+ return {
409
+ valid: false,
410
+ reason: `reviewer set incomplete: missing required reviewer '${required}'`,
411
+ };
412
+ }
413
+ }
414
+ return { valid: true, predicate, trustedReviewer: matchedReviewer };
415
+ }
416
+ /**
417
+ * Validate the shape of a parsed `.ai-sdlc/trusted-reviewers.yaml` document.
418
+ * Throws on malformed input with a specific reason. Acceptance criterion #4.
419
+ *
420
+ * Accepts the parsed YAML (as `unknown`) and returns the typed array.
421
+ */
422
+ export function validateTrustedReviewers(parsed) {
423
+ if (parsed === null || parsed === undefined)
424
+ return [];
425
+ if (typeof parsed !== 'object') {
426
+ throw new Error('trusted-reviewers.yaml: root must be an object with a `reviewers` list');
427
+ }
428
+ const root = parsed;
429
+ const list = root['reviewers'];
430
+ if (list === undefined || list === null)
431
+ return [];
432
+ if (!Array.isArray(list)) {
433
+ throw new Error('trusted-reviewers.yaml: `reviewers` must be a list');
434
+ }
435
+ const out = [];
436
+ for (let i = 0; i < list.length; i++) {
437
+ const entry = list[i];
438
+ if (typeof entry !== 'object' || entry === null) {
439
+ throw new Error(`trusted-reviewers.yaml: reviewers[${i}] must be an object`);
440
+ }
441
+ const r = entry;
442
+ for (const field of ['identity', 'machine', 'pubkey', 'addedAt', 'addedBy']) {
443
+ if (typeof r[field] !== 'string' || r[field].length === 0) {
444
+ throw new Error(`trusted-reviewers.yaml: reviewers[${i}].${field} must be a non-empty string`);
445
+ }
446
+ }
447
+ if (!r['pubkey'].includes('BEGIN PUBLIC KEY')) {
448
+ throw new Error(`trusted-reviewers.yaml: reviewers[${i}].pubkey must be a PEM-encoded public key`);
449
+ }
450
+ out.push({
451
+ identity: r['identity'],
452
+ machine: r['machine'],
453
+ pubkey: r['pubkey'],
454
+ addedAt: r['addedAt'],
455
+ addedBy: r['addedBy'],
456
+ });
457
+ }
458
+ return out;
459
+ }
460
+ //# sourceMappingURL=attestations.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Helpers for safely invoking `git` subprocesses inside isolated working
3
+ * directories.
4
+ *
5
+ * ## Why this exists (AISDLC-68 → AISDLC-72)
6
+ *
7
+ * When a husky pre-push hook (or any nested git invocation) runs commands
8
+ * from inside `.worktrees/<id>/`, git exports `GIT_DIR`, `GIT_WORK_TREE`,
9
+ * and `GIT_INDEX_FILE` into the child process environment so subprocesses
10
+ * "stay" in the calling worktree's git context. That's the right behavior
11
+ * for normal subprocesses — but it's catastrophic for code that creates
12
+ * its own throwaway repository in a temp directory.
13
+ *
14
+ * Without sanitizing the environment:
15
+ * - `execSync('git init', { cwd: tmpDir })` initializes the *parent*
16
+ * worktree's index, not tmpDir's.
17
+ * - `execSync('git commit', { cwd: tmpDir })` writes a commit onto the
18
+ * *parent's* current branch using files from tmpDir — silently
19
+ * corrupting the feature branch with leaked test artifacts.
20
+ * - The pre-push hook that triggered this whole cascade then sees
21
+ * unexpected commits and either fails or worse, ships them.
22
+ *
23
+ * The fix is uniform: strip `GIT_DIR` / `GIT_WORK_TREE` / `GIT_INDEX_FILE`
24
+ * from the inherited env at every site that runs `git` against an explicit
25
+ * `cwd`. PR #78 patched the tokens-studio adapter; AISDLC-72 sweeps the
26
+ * orchestrator + remaining test sites.
27
+ */
28
+ /**
29
+ * Return a copy of `process.env` with the git-context env vars removed.
30
+ *
31
+ * Use this for any subprocess that runs `git` against an explicit `cwd`
32
+ * different from the calling process's CWD (temp repos, sibling repos,
33
+ * worktree-pool clones).
34
+ */
35
+ export declare function cleanGitEnv(): NodeJS.ProcessEnv;
36
+ /**
37
+ * Promisified `execFile('git', args, opts)` wrapper that strips the git
38
+ * context env vars before invocation. Use whenever the caller passes
39
+ * `cwd` (i.e. running git in a directory other than the parent process's
40
+ * CWD).
41
+ *
42
+ * Caller-supplied `opts.env` takes precedence — it's already explicit
43
+ * about what to inherit. We only inject `cleanGitEnv()` when no env was
44
+ * provided.
45
+ */
46
+ export declare function gitExecFile(args: string[], opts?: {
47
+ cwd?: string;
48
+ env?: NodeJS.ProcessEnv;
49
+ }): Promise<{
50
+ stdout: string;
51
+ stderr: string;
52
+ }>;
53
+ //# sourceMappingURL=git-env.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Helpers for safely invoking `git` subprocesses inside isolated working
3
+ * directories.
4
+ *
5
+ * ## Why this exists (AISDLC-68 → AISDLC-72)
6
+ *
7
+ * When a husky pre-push hook (or any nested git invocation) runs commands
8
+ * from inside `.worktrees/<id>/`, git exports `GIT_DIR`, `GIT_WORK_TREE`,
9
+ * and `GIT_INDEX_FILE` into the child process environment so subprocesses
10
+ * "stay" in the calling worktree's git context. That's the right behavior
11
+ * for normal subprocesses — but it's catastrophic for code that creates
12
+ * its own throwaway repository in a temp directory.
13
+ *
14
+ * Without sanitizing the environment:
15
+ * - `execSync('git init', { cwd: tmpDir })` initializes the *parent*
16
+ * worktree's index, not tmpDir's.
17
+ * - `execSync('git commit', { cwd: tmpDir })` writes a commit onto the
18
+ * *parent's* current branch using files from tmpDir — silently
19
+ * corrupting the feature branch with leaked test artifacts.
20
+ * - The pre-push hook that triggered this whole cascade then sees
21
+ * unexpected commits and either fails or worse, ships them.
22
+ *
23
+ * The fix is uniform: strip `GIT_DIR` / `GIT_WORK_TREE` / `GIT_INDEX_FILE`
24
+ * from the inherited env at every site that runs `git` against an explicit
25
+ * `cwd`. PR #78 patched the tokens-studio adapter; AISDLC-72 sweeps the
26
+ * orchestrator + remaining test sites.
27
+ */
28
+ import { execFile } from 'node:child_process';
29
+ import { promisify } from 'node:util';
30
+ const execFileAsync = promisify(execFile);
31
+ /**
32
+ * Return a copy of `process.env` with the git-context env vars removed.
33
+ *
34
+ * Use this for any subprocess that runs `git` against an explicit `cwd`
35
+ * different from the calling process's CWD (temp repos, sibling repos,
36
+ * worktree-pool clones).
37
+ */
38
+ export function cleanGitEnv() {
39
+ const env = { ...process.env };
40
+ delete env.GIT_DIR;
41
+ delete env.GIT_WORK_TREE;
42
+ delete env.GIT_INDEX_FILE;
43
+ return env;
44
+ }
45
+ /**
46
+ * Promisified `execFile('git', args, opts)` wrapper that strips the git
47
+ * context env vars before invocation. Use whenever the caller passes
48
+ * `cwd` (i.e. running git in a directory other than the parent process's
49
+ * CWD).
50
+ *
51
+ * Caller-supplied `opts.env` takes precedence — it's already explicit
52
+ * about what to inherit. We only inject `cleanGitEnv()` when no env was
53
+ * provided.
54
+ */
55
+ export async function gitExecFile(args, opts = {}) {
56
+ const env = opts.env ?? cleanGitEnv();
57
+ const { stdout, stderr } = await execFileAsync('git', args, { ...opts, env });
58
+ return { stdout, stderr };
59
+ }
60
+ //# sourceMappingURL=git-env.js.map
@@ -0,0 +1,7 @@
1
+ export { deterministicPort, allocatePort, allocateContiguousPorts, isPortFree, PortAllocationError, DEFAULT_BASE_PORT, PORT_RANGE_OFFSET_MIN, PORT_RANGE_OFFSET_MAX, type AllocatePortOptions, type AllocateContiguousOptions, } from './port-allocator.js';
2
+ export { slugifyBranch, worktreePath, verifyOwnership, assertOwnership, isExistingWorktree, WorktreeOwnershipError, type OwnershipResult, } from './worktree.js';
3
+ export { WorktreePoolManager, WorktreePoolError, DEFAULT_POOL_ROOT, DEFAULT_STALE_THRESHOLD_DAYS, type WorktreePoolSpec, type WorktreePoolManagerDeps, type AllocateOptions, type WorktreeHandle, } from './worktree-pool.js';
4
+ export { readParallelismMode, isParallelismEnabled, FLAG_NAME as PARALLELISM_FLAG, type ParallelismMode, } from './parallelism-flag.js';
5
+ export { cleanGitEnv, gitExecFile } from './git-env.js';
6
+ export { ACCEPTED_SCHEMA_VERSIONS, DSSE_PAYLOAD_TYPE, buildPredicate, generateSigningKeyPair, paeEncode, sha1Hex, sha256Hex, signAttestation, validateTrustedReviewers, verifyAttestation, type AttestationPredicate, type DsseEnvelope, type DsseSignature, type ReviewerEntry, type SchemaVersion, type SubjectDigest, type TrustedReviewer, type VerifyResult, } from './attestations.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ export { deterministicPort, allocatePort, allocateContiguousPorts, isPortFree, PortAllocationError, DEFAULT_BASE_PORT, PORT_RANGE_OFFSET_MIN, PORT_RANGE_OFFSET_MAX, } from './port-allocator.js';
2
+ export { slugifyBranch, worktreePath, verifyOwnership, assertOwnership, isExistingWorktree, WorktreeOwnershipError, } from './worktree.js';
3
+ export { WorktreePoolManager, WorktreePoolError, DEFAULT_POOL_ROOT, DEFAULT_STALE_THRESHOLD_DAYS, } from './worktree-pool.js';
4
+ export { readParallelismMode, isParallelismEnabled, FLAG_NAME as PARALLELISM_FLAG, } from './parallelism-flag.js';
5
+ export { cleanGitEnv, gitExecFile } from './git-env.js';
6
+ export { ACCEPTED_SCHEMA_VERSIONS, DSSE_PAYLOAD_TYPE, buildPredicate, generateSigningKeyPair, paeEncode, sha1Hex, sha256Hex, signAttestation, validateTrustedReviewers, verifyAttestation, } from './attestations.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Feature-flag gate for RFC-0010 parallelism. While Phases 1-4 land, the worker-pool
3
+ * conversion in execute.ts is gated behind AI_SDLC_PARALLELISM=experimental. Phase 5
4
+ * (hardening) flips the default to on after a soak window.
5
+ */
6
+ export declare const FLAG_NAME = "AI_SDLC_PARALLELISM";
7
+ export type ParallelismMode = 'off' | 'experimental' | 'on';
8
+ export declare function readParallelismMode(env?: NodeJS.ProcessEnv): ParallelismMode;
9
+ export declare function isParallelismEnabled(env?: NodeJS.ProcessEnv): boolean;
10
+ //# sourceMappingURL=parallelism-flag.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Feature-flag gate for RFC-0010 parallelism. While Phases 1-4 land, the worker-pool
3
+ * conversion in execute.ts is gated behind AI_SDLC_PARALLELISM=experimental. Phase 5
4
+ * (hardening) flips the default to on after a soak window.
5
+ */
6
+ export const FLAG_NAME = 'AI_SDLC_PARALLELISM';
7
+ export function readParallelismMode(env = process.env) {
8
+ const raw = env[FLAG_NAME]?.trim().toLowerCase();
9
+ if (raw === 'experimental')
10
+ return 'experimental';
11
+ if (raw === 'on' || raw === 'true' || raw === '1')
12
+ return 'on';
13
+ return 'off';
14
+ }
15
+ export function isParallelismEnabled(env) {
16
+ return readParallelismMode(env) !== 'off';
17
+ }
18
+ //# sourceMappingURL=parallelism-flag.js.map
@@ -0,0 +1,21 @@
1
+ export declare const DEFAULT_BASE_PORT = 3190;
2
+ export declare const PORT_RANGE_OFFSET_MIN = 100;
3
+ export declare const PORT_RANGE_OFFSET_MAX = 999;
4
+ export interface AllocatePortOptions {
5
+ basePort?: number;
6
+ envOverride?: string | undefined;
7
+ isPortFree?: (port: number) => Promise<boolean>;
8
+ }
9
+ export interface AllocateContiguousOptions extends AllocatePortOptions {
10
+ count: number;
11
+ }
12
+ export declare class PortAllocationError extends Error {
13
+ readonly worktreePath: string;
14
+ readonly attemptedPorts: number[];
15
+ constructor(message: string, worktreePath: string, attemptedPorts: number[]);
16
+ }
17
+ export declare function deterministicPort(worktreePath: string, basePort?: number): number;
18
+ export declare function allocatePort(worktreePath: string, options?: AllocatePortOptions): Promise<number>;
19
+ export declare function allocateContiguousPorts(worktreePath: string, options: AllocateContiguousOptions): Promise<number[]>;
20
+ export declare function isPortFree(port: number): Promise<boolean>;
21
+ //# sourceMappingURL=port-allocator.d.ts.map