@ai-sdlc/orchestrator 0.10.0 → 0.14.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 (168) hide show
  1. package/dist/adapters.d.ts +42 -3
  2. package/dist/adapters.js +133 -3
  3. package/dist/admission-composite.d.ts +112 -1
  4. package/dist/admission-composite.js +85 -4
  5. package/dist/admission-enrichment.d.ts +12 -3
  6. package/dist/admission-enrichment.js +64 -13
  7. package/dist/admission-score.d.ts +30 -0
  8. package/dist/admission-score.js +4 -1
  9. package/dist/analysis/file-walker.js +5 -0
  10. package/dist/backlog-adapter.d.ts +45 -0
  11. package/dist/backlog-adapter.js +145 -1
  12. package/dist/calibration.d.ts +81 -0
  13. package/dist/calibration.js +76 -0
  14. package/dist/cli/commands/git-remote.js +1 -1
  15. package/dist/cli/commands/init-features.d.ts +301 -2
  16. package/dist/cli/commands/init-features.js +647 -7
  17. package/dist/cli/commands/init-templates.d.ts +198 -1
  18. package/dist/cli/commands/init-templates.js +943 -1
  19. package/dist/cli/commands/init.d.ts +45 -0
  20. package/dist/cli/commands/init.js +147 -5
  21. package/dist/cli/commands/run.js +9 -1
  22. package/dist/cli/index.d.ts +11 -0
  23. package/dist/cli/index.js +42 -8
  24. package/dist/compliance/composer.d.ts +79 -0
  25. package/dist/compliance/composer.js +258 -0
  26. package/dist/compliance/errors.d.ts +64 -0
  27. package/dist/compliance/errors.js +85 -0
  28. package/dist/compliance/loader.d.ts +52 -0
  29. package/dist/compliance/loader.js +124 -0
  30. package/dist/compliance/types.d.ts +184 -0
  31. package/dist/compliance/types.js +41 -0
  32. package/dist/compliance-clearance.d.ts +269 -0
  33. package/dist/compliance-clearance.js +269 -0
  34. package/dist/config.js +17 -0
  35. package/dist/cost-tracker.d.ts +22 -0
  36. package/dist/cost-tracker.js +41 -0
  37. package/dist/cycle-utils.js +14 -1
  38. package/dist/database/adapters/external.js +5 -1
  39. package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
  40. package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
  41. package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
  42. package/dist/embedding/consumers/tessellation-drift.js +76 -0
  43. package/dist/embedding/cross-provider.d.ts +78 -0
  44. package/dist/embedding/cross-provider.js +75 -0
  45. package/dist/embedding/deprecation.d.ts +151 -0
  46. package/dist/embedding/deprecation.js +229 -0
  47. package/dist/embedding/errors.d.ts +90 -0
  48. package/dist/embedding/errors.js +150 -0
  49. package/dist/embedding/index.d.ts +29 -0
  50. package/dist/embedding/index.js +24 -0
  51. package/dist/embedding/pipeline-load.d.ts +146 -0
  52. package/dist/embedding/pipeline-load.js +178 -0
  53. package/dist/embedding/registry.d.ts +45 -0
  54. package/dist/embedding/registry.js +61 -0
  55. package/dist/embedding/stale-vector.d.ts +110 -0
  56. package/dist/embedding/stale-vector.js +92 -0
  57. package/dist/embedding/storage/index.d.ts +51 -0
  58. package/dist/embedding/storage/index.js +43 -0
  59. package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
  60. package/dist/embedding/storage/jsonl-backend.js +332 -0
  61. package/dist/embedding/storage/types.d.ts +135 -0
  62. package/dist/embedding/storage/types.js +13 -0
  63. package/dist/embedding/types.d.ts +180 -0
  64. package/dist/embedding/types.js +10 -0
  65. package/dist/execute.d.ts +29 -2
  66. package/dist/execute.js +200 -38
  67. package/dist/fix-review.js +1 -1
  68. package/dist/index.d.ts +14 -2
  69. package/dist/index.js +19 -2
  70. package/dist/journey/inheritance-validator.d.ts +396 -0
  71. package/dist/journey/inheritance-validator.js +370 -0
  72. package/dist/journey/metric-snapshot.d.ts +403 -0
  73. package/dist/journey/metric-snapshot.js +370 -0
  74. package/dist/journey/state-id-drift-rule.d.ts +137 -0
  75. package/dist/journey/state-id-drift-rule.js +245 -0
  76. package/dist/journey-sa2-router.d.ts +395 -0
  77. package/dist/journey-sa2-router.js +308 -0
  78. package/dist/otel-exporter.js +0 -3
  79. package/dist/runners/claude-code.js +3 -3
  80. package/dist/runners/review-agent.js +6 -2
  81. package/dist/runners/runner-registry.d.ts +36 -0
  82. package/dist/runners/runner-registry.js +90 -0
  83. package/dist/runtime/attestations.d.ts +173 -13
  84. package/dist/runtime/attestations.js +265 -43
  85. package/dist/runtime/index.d.ts +1 -1
  86. package/dist/runtime/index.js +1 -1
  87. package/dist/sa-scoring/exemplar-bank.js +3 -1
  88. package/dist/sa-scoring/layer3-llm.js +6 -1
  89. package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
  90. package/dist/sa-scoring/revision-proposal-config.js +198 -0
  91. package/dist/sa-scoring/revision-proposal.d.ts +285 -0
  92. package/dist/sa-scoring/revision-proposal.js +417 -0
  93. package/dist/shared.d.ts +28 -0
  94. package/dist/shared.js +37 -0
  95. package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
  96. package/dist/signal-ingestion/adapters/community-thread.js +55 -0
  97. package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
  98. package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
  99. package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
  100. package/dist/signal-ingestion/adapters/manual.js +112 -0
  101. package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
  102. package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
  103. package/dist/signal-ingestion/classifier.d.ts +205 -0
  104. package/dist/signal-ingestion/classifier.js +494 -0
  105. package/dist/signal-ingestion/clustering-types.d.ts +36 -0
  106. package/dist/signal-ingestion/clustering-types.js +14 -0
  107. package/dist/signal-ingestion/clustering.d.ts +200 -0
  108. package/dist/signal-ingestion/clustering.js +413 -0
  109. package/dist/signal-ingestion/config.d.ts +351 -0
  110. package/dist/signal-ingestion/config.js +587 -0
  111. package/dist/signal-ingestion/d1.d.ts +252 -0
  112. package/dist/signal-ingestion/d1.js +235 -0
  113. package/dist/signal-ingestion/errors.d.ts +73 -0
  114. package/dist/signal-ingestion/errors.js +108 -0
  115. package/dist/signal-ingestion/governance-events.d.ts +181 -0
  116. package/dist/signal-ingestion/governance-events.js +189 -0
  117. package/dist/signal-ingestion/index.d.ts +35 -0
  118. package/dist/signal-ingestion/index.js +53 -0
  119. package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
  120. package/dist/signal-ingestion/manual-share-metric.js +106 -0
  121. package/dist/signal-ingestion/registry.d.ts +40 -0
  122. package/dist/signal-ingestion/registry.js +137 -0
  123. package/dist/signal-ingestion/residency.d.ts +227 -0
  124. package/dist/signal-ingestion/residency.js +238 -0
  125. package/dist/signal-ingestion/significance.d.ts +554 -0
  126. package/dist/signal-ingestion/significance.js +555 -0
  127. package/dist/signal-ingestion/types.d.ts +191 -0
  128. package/dist/signal-ingestion/types.js +8 -0
  129. package/dist/substrate/drift-composition.d.ts +270 -0
  130. package/dist/substrate/drift-composition.js +306 -0
  131. package/dist/substrate/drift-tui-surface.d.ts +61 -0
  132. package/dist/substrate/drift-tui-surface.js +102 -0
  133. package/dist/substrate/identity-class.d.ts +176 -0
  134. package/dist/substrate/identity-class.js +201 -0
  135. package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
  136. package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
  137. package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
  138. package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
  139. package/dist/tessellation/rule-registry.d.ts +269 -0
  140. package/dist/tessellation/rule-registry.js +92 -0
  141. package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
  142. package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
  143. package/dist/tessellation-admission.d.ts +162 -0
  144. package/dist/tessellation-admission.js +146 -0
  145. package/dist/tessellation-drift.d.ts +246 -0
  146. package/dist/tessellation-drift.js +250 -0
  147. package/dist/validate-config.js +13 -0
  148. package/dist/validate-issue.js +2 -2
  149. package/dist/variant/cardinality-activation.d.ts +126 -0
  150. package/dist/variant/cardinality-activation.js +101 -0
  151. package/dist/variant/deprecation-lifecycle.d.ts +184 -0
  152. package/dist/variant/deprecation-lifecycle.js +208 -0
  153. package/dist/variant/drift-extension.d.ts +136 -0
  154. package/dist/variant/drift-extension.js +164 -0
  155. package/dist/variant/engineering-review.d.ts +185 -0
  156. package/dist/variant/engineering-review.js +142 -0
  157. package/dist/variant/index.d.ts +32 -0
  158. package/dist/variant/index.js +32 -0
  159. package/dist/variant/inheritance-validator.d.ts +165 -0
  160. package/dist/variant/inheritance-validator.js +139 -0
  161. package/dist/variant/internal-adopter/index.d.ts +11 -0
  162. package/dist/variant/internal-adopter/index.js +10 -0
  163. package/dist/variant/internal-adopter/products.d.ts +156 -0
  164. package/dist/variant/internal-adopter/products.js +366 -0
  165. package/dist/variant-admission.d.ts +316 -0
  166. package/dist/variant-admission.js +247 -0
  167. package/dist/webhook-manager.js +7 -1
  168. package/package.json +15 -12
@@ -0,0 +1,180 @@
1
+ /**
2
+ * EmbeddingAdapter interface per RFC-0019 §5.
3
+ *
4
+ * Every embedding adapter MUST implement this interface. Adapters are registered
5
+ * in orchestrator/src/embedding/registry.ts and resolved by name at pipeline-load.
6
+ *
7
+ * The pattern mirrors HarnessAdapter (RFC-0010 §13) and DatabaseBranchAdapter (RFC-0010 §15).
8
+ */
9
+ /**
10
+ * Result of an adapter liveness probe (isAvailable()).
11
+ */
12
+ export interface EmbeddingAvailability {
13
+ available: boolean;
14
+ /** Structured reason — helps operators understand what to fix. */
15
+ reason?: 'env-var-missing' | 'health-check-failed' | 'rate-limited' | 'unknown';
16
+ /** Human-readable detail naming the missing env var or failing probe. */
17
+ detail?: string;
18
+ }
19
+ /**
20
+ * Runtime dependency declaration for an embedding adapter.
21
+ * Parallel to HarnessRequires in RFC-0010 §13.8.
22
+ */
23
+ export interface EmbeddingRequires {
24
+ /** Environment variable required by this adapter (e.g., 'OPENAI_API_KEY'). */
25
+ envVar?: string;
26
+ /** Optional npm package or binary name required (for local/ONNX adapters). */
27
+ binary?: string;
28
+ /** Semver range for the binary, if applicable. */
29
+ versionRange?: string;
30
+ /** Path to model file (e.g., for ONNX-based adapters). */
31
+ modelFile?: string;
32
+ }
33
+ /**
34
+ * Billing model for the adapter — drives cost-tracker routing.
35
+ *
36
+ * - 'pay-per-token': tokens are billed via provider invoice (e.g., OpenAI).
37
+ * embeddingTokens are recorded in cost-tracker but do NOT consume SubscriptionLedger
38
+ * window quota (OQ-7 re-walkthrough).
39
+ * - 'subscription-quota': tokens are billed via the operator's subscription
40
+ * (e.g., future Anthropic embeddings). Routes through SubscriptionLedger via
41
+ * the inputTokens/outputTokens mechanism.
42
+ */
43
+ export type EmbeddingBillingModel = 'pay-per-token' | 'subscription-quota';
44
+ /**
45
+ * Capability matrix for an embedding adapter (RFC-0019 §6.2).
46
+ * Normative for in-tree adapters; adopter-registered adapters SHOULD declare these.
47
+ */
48
+ export interface EmbeddingCapabilities {
49
+ /** Output vector length. */
50
+ dimensions: number;
51
+ /** Maximum tokens per single embed() call. */
52
+ maxInputTokens: number;
53
+ /** Whether the adapter implements embedBatch(). */
54
+ supportsBatching: boolean;
55
+ /** Whether the adapter runs locally (no external API call). */
56
+ selfHosted: boolean;
57
+ /** Billing model — drives cost-tracker routing (OQ-7 re-walkthrough). */
58
+ billingModel: EmbeddingBillingModel;
59
+ /** Approximate cost per 1M tokens in USD (for display / cost estimation). Null for local. */
60
+ approxCostPer1MTokens?: number;
61
+ /**
62
+ * Adapter-declared default deprecation grace period in days per RFC-0019
63
+ * §9.1 OQ-4 re-walkthrough. When set, this overrides the framework default
64
+ * (90d) but is itself overridden by per-org `gracePeriodDays`. Fast-moving
65
+ * providers (e.g., Cohere with 6-month deprecation cycles) declare a smaller
66
+ * value here so the warning window scales to the provider's actual lifecycle.
67
+ *
68
+ * The three-layer precedence is:
69
+ * per-org `gracePeriodDays` (highest)
70
+ * adapter `defaultGracePeriodDays` (this field)
71
+ * framework `FRAMEWORK_DEFAULT_GRACE_PERIOD_DAYS` (90, lowest)
72
+ */
73
+ defaultGracePeriodDays?: number;
74
+ }
75
+ /**
76
+ * The EmbeddingAdapter interface per RFC-0019 §5.
77
+ * Implemented by every embedding adapter registered in the registry.
78
+ */
79
+ export interface EmbeddingAdapter {
80
+ /**
81
+ * Canonical adapter alias — the value adopters set in Pipeline.spec.embedding.provider.
82
+ * MUST be unique across the registry. Convention: '<vendor>-<model-family>-<size>'.
83
+ * Examples: 'openai-text-embedding-3-small', 'cohere-embed-v3-multilingual'.
84
+ */
85
+ readonly name: string;
86
+ /**
87
+ * Provider-specific model identifier — passed to the upstream API.
88
+ * Example: 'text-embedding-3-small' (posted to OpenAI's /embeddings endpoint).
89
+ */
90
+ readonly modelId: string;
91
+ /**
92
+ * Snapshot identifier — ISO date for date-pinned snapshots, semver for versioned models.
93
+ * Used as part of the storage key so vectors don't collide across model snapshots.
94
+ * Example: '2024-01-25' for OpenAI's 2024-01-25 text-embedding-3-small snapshot.
95
+ */
96
+ readonly modelVersion: string;
97
+ /**
98
+ * Vector length the adapter emits. Validated against storage on first write.
99
+ * Redundant with capabilities.dimensions but declared at the top level for
100
+ * fast dimension-mismatch detection without unpacking capabilities.
101
+ */
102
+ readonly dimensions: number;
103
+ /** Static capability matrix. */
104
+ readonly capabilities: EmbeddingCapabilities;
105
+ /** Runtime dependency declaration. */
106
+ readonly requires: EmbeddingRequires;
107
+ /**
108
+ * ISO date when the deprecation warning period starts.
109
+ * When set, pipeline-load emits EmbeddingModelDeprecating warning 90d before
110
+ * and EmbeddingModelDeprecated error at/after this date.
111
+ */
112
+ readonly deprecatedAt?: string;
113
+ /**
114
+ * ISO date when the adapter is removed.
115
+ * When set AND today >= removedAt, pipeline-load FAILS with EmbeddingModelRemoved.
116
+ */
117
+ readonly removedAt?: string;
118
+ /**
119
+ * Canonical name of the adapter operators should migrate to.
120
+ * Included in deprecation warnings/errors.
121
+ */
122
+ readonly replacementAlias?: string;
123
+ /**
124
+ * Embed a single text string. Returns the vector as number[].
125
+ *
126
+ * Implementations MUST:
127
+ * - throw on empty input (no silent zero-vector emission)
128
+ * - throw on input exceeding the provider's per-call token limit
129
+ * - return a vector of length === this.dimensions (orchestrator validates post-hoc)
130
+ *
131
+ * @param text - Source text to embed. MUST be non-empty.
132
+ * @param consumerLabel - Optional label for cost attribution per OQ-6 re-walkthrough.
133
+ * Default: 'unspecified'. Examples: 'rfc-0009-tessellation-drift', 'rfc-0008-ppa-similarity'.
134
+ */
135
+ embed(text: string, consumerLabel?: string): Promise<number[]>;
136
+ /**
137
+ * Optional batch interface. Adapters MAY implement for efficiency; orchestrator
138
+ * calls embed() in a loop when this is undefined.
139
+ * Implementations MUST preserve input order in the returned array.
140
+ *
141
+ * @param texts - Array of source texts to embed. Each MUST be non-empty.
142
+ * @param consumerLabel - Optional label for cost attribution. Applies to all texts in batch.
143
+ */
144
+ embedBatch?(texts: string[], consumerLabel?: string): Promise<number[][]>;
145
+ /**
146
+ * Cheap liveness probe. Combines env-var presence (e.g., OPENAI_API_KEY)
147
+ * + lightweight provider health check (optional).
148
+ * Result MAY be cached for the orchestrator's lifetime.
149
+ */
150
+ isAvailable(): Promise<EmbeddingAvailability>;
151
+ /**
152
+ * Stable identifier for the credential / account in scope. Used by cost-tracker
153
+ * to attribute spend per credential. MUST be a one-way derivation (e.g., SHA-256
154
+ * of the API key + adapter name) and MUST NOT leak the credential itself.
155
+ * Returns null when the adapter cannot derive an account identity (e.g., self-hosted).
156
+ */
157
+ getAccountId(): Promise<string | null>;
158
+ }
159
+ /**
160
+ * Record of a single embedding cost event, passed to the cost-tracker.
161
+ * Captures the (provider, modelVersion, accountId, consumerLabel) dimensions
162
+ * per OQ-6 re-walkthrough.
163
+ */
164
+ export interface EmbeddingCostRecord {
165
+ /** Adapter name (e.g., 'openai-text-embedding-3-small'). */
166
+ provider: string;
167
+ /** Adapter model version (e.g., '2024-01-25'). */
168
+ modelVersion: string;
169
+ /** One-way hash of the API credential, or null for self-hosted. */
170
+ accountId: string | null;
171
+ /** Consumer attribution label for per-consumer cost breakdown. Default: 'unspecified'. */
172
+ consumerLabel: string;
173
+ /** Total tokens consumed by this embed() call. */
174
+ tokens: number;
175
+ /** USD cost for this call. */
176
+ costUsd: number;
177
+ /** Billing model — used to decide whether to decrement SubscriptionLedger. */
178
+ billingModel: EmbeddingBillingModel;
179
+ }
180
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * EmbeddingAdapter interface per RFC-0019 §5.
3
+ *
4
+ * Every embedding adapter MUST implement this interface. Adapters are registered
5
+ * in orchestrator/src/embedding/registry.ts and resolved by name at pipeline-load.
6
+ *
7
+ * The pattern mirrors HarnessAdapter (RFC-0010 §13) and DatabaseBranchAdapter (RFC-0010 §15).
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
package/dist/execute.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * create branch -> invoke agent -> authorize -> validate -> push -> create PR -> comment
7
7
  */
8
8
  import { type IssueTracker, type SourceControl, type AuditLog, type MetricStore, type AgentMemory, type ExpressionEvaluator, type LLMEvaluator, type SecretStore } from '@ai-sdlc/reference';
9
+ import { type AiSdlcConfig } from './config.js';
9
10
  import { type Logger } from './logger.js';
10
11
  import type { AgentRunner } from './runners/types.js';
11
12
  import { type SecurityContext } from './security.js';
@@ -30,8 +31,14 @@ export interface ExecuteOptions {
30
31
  configDir?: string;
31
32
  /** Override the working directory (defaults to `process.cwd()`). */
32
33
  workDir?: string;
33
- /** Inject a custom runner (for testing). */
34
+ /** Inject a custom runner (for testing). Wins over runnerName and registry. */
34
35
  runner?: AgentRunner;
36
+ /**
37
+ * Select a runner by name from the registry (maps to `--runner <name>` CLI flag).
38
+ * Must be registered (built-in, env-discovered, or loaded via AI_SDLC_RUNNER_PLUGIN).
39
+ * Throws when the name is unknown — no silent fallback.
40
+ */
41
+ runnerName?: string;
35
42
  /** Inject a custom issue tracker (for testing). */
36
43
  tracker?: IssueTracker;
37
44
  /** Inject a custom source control adapter (for testing). */
@@ -85,6 +92,20 @@ export interface ExecuteOptions {
85
92
  */
86
93
  pipelineOverride?: import('@ai-sdlc/reference').Pipeline;
87
94
  }
95
+ /**
96
+ * Build an actionable error message when a required resource kind is absent.
97
+ *
98
+ * The loader silently drops resource-shaped YAML files that fail schema
99
+ * validation (recording them as `config.warnings`). Without this helper, the
100
+ * caller would only see "No <Kind> resource found in .ai-sdlc/" with no pointer
101
+ * to the real cause. This helper attaches any relevant warnings so the adopter
102
+ * knows *which* file to fix and *why* it was dropped.
103
+ *
104
+ * Non-resource YAMLs (no apiVersion+kind) are silently skipped by design
105
+ * (AISDLC-722 guard); this helper is only called when the resource is truly
106
+ * absent — i.e. no file with the right kind and a passing schema exists.
107
+ */
108
+ export declare function buildMissingResourceError(kind: string, config: AiSdlcConfig, configDir: string): string;
88
109
  /**
89
110
  * Execute the full AI-SDLC pipeline for a given issue ID.
90
111
  */
@@ -116,9 +137,15 @@ export declare function restoreOriginalBranch(workDir: string, originalHead: str
116
137
  * rebase HEAD onto origin/<branch>, retry once. Rebase conflicts surface as
117
138
  * the original error so the operator can resolve manually.
118
139
  *
140
+ * Local-only mode (AISDLC-530 / AISDLC-527): when there is no 'origin' remote,
141
+ * push is skipped gracefully (returns true to signal the skip). This mirrors the
142
+ * AISDLC-527 fetch guard at step 7 — we detect the "no such remote" pattern in
143
+ * the push error and swallow it exactly as the fetch step does.
144
+ *
145
+ * @returns `true` when push was skipped (local-only repo), `false` when pushed successfully.
119
146
  * @internal — exported for unit tests.
120
147
  */
121
148
  export declare function pushBranchWithRebase(workDir: string, branchName: string, log: {
122
149
  info: (msg: string) => void;
123
- } | undefined): Promise<void>;
150
+ } | undefined): Promise<boolean>;
124
151
  //# sourceMappingURL=execute.d.ts.map
package/dist/execute.js CHANGED
@@ -5,14 +5,14 @@
5
5
  * load config -> fetch issue -> validate -> check autonomy ->
6
6
  * create branch -> invoke agent -> authorize -> validate -> push -> create PR -> comment
7
7
  */
8
- import { createGitHubSourceControl, routeByComplexity, evaluatePromotion, evaluateComplexity, selectModel, withSpan, getMeter, SPAN_NAMES, METRIC_NAMES, ATTRIBUTE_KEYS, } from '@ai-sdlc/reference';
8
+ import { routeByComplexity, evaluatePromotion, evaluateComplexity, selectModel, withSpan, getMeter, SPAN_NAMES, METRIC_NAMES, ATTRIBUTE_KEYS, } from '@ai-sdlc/reference';
9
9
  import { loadConfigAsync } from './config.js';
10
10
  import { validateIssue, validateIssueWithExtensions, parseComplexity } from './validate-issue.js';
11
11
  import { validateAgentOutput } from './validate-agent-output.js';
12
12
  import { createLogger } from './logger.js';
13
13
  import { createStructuredConsoleLogger, createStructuredBufferLogger, } from './structured-logger.js';
14
- import { ClaudeCodeRunner } from './runners/claude-code.js';
15
- import { execFileAsync, getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, isAutonomousStrategy, recordMetric, evaluatePipelineCompliance, authorizeFilesChanged, interpolateBranchPattern, interpolatePRTitle, issueIdToNumber, formatIssueRef, buildIssueTemplateVars, } from './shared.js';
14
+ import { createRunnerRegistry, resolveRunner } from './runners/runner-registry.js';
15
+ import { execFileAsync, getGitHubConfig, resolveRepoRoot, createDefaultAuditLog, resolveAutonomyLevel, resolveConstraints, isAutonomousStrategy, recordMetric, evaluatePipelineCompliance, authorizeFilesChanged, interpolateBranchPattern, interpolatePRTitle, issueIdToNumber, formatIssueRef, buildIssueTemplateVars, validateBranchName, } from './shared.js';
16
16
  import { cleanGitEnv } from './runtime/git-env.js';
17
17
  import { checkKillSwitch, issueAgentCredentials, revokeAgentCredentials, classifyAndSubmitApproval, createPipelineSecurity, } from './security.js';
18
18
  import { createPipelineOrchestration, executePipelineOrchestration, validatePipelineHandoffs, } from './orchestration.js';
@@ -23,7 +23,7 @@ import { createPipelineExpressionEvaluator, createPipelineLLMEvaluator, createPi
23
23
  import { verifyAuditIntegrity, createFileAuditLog, loadAuditEntries, computeAuditHash, } from './audit-extended.js';
24
24
  import { renderTemplate } from './notifications.js';
25
25
  import { admitIssueResource, createPipelineAdmission, } from './admission.js';
26
- import { createPipelineAdapterRegistry, createPipelineWebhookBridge, resolveAdapterFromGit, scanPipelineAdapters, resolveIssueTrackerFromConfig, } from './adapters.js';
26
+ import { createPipelineAdapterRegistry, createPipelineWebhookBridge, resolveAdapterFromGit, scanPipelineAdapters, resolveIssueTrackerFromConfig, resolveSourceControlFromConfig, } from './adapters.js';
27
27
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
28
28
  import { join } from 'node:path';
29
29
  import { defaultSandboxConstraints, DEFAULT_CONFIG_DIR_NAME, DEFAULT_PR_FOOTER, DEFAULT_LINT_COMMAND, DEFAULT_FORMAT_COMMAND, DEFAULT_COMMIT_MESSAGE_TEMPLATE, DEFAULT_COMMIT_CO_AUTHOR, NOTIFICATION_TITLES, } from './defaults.js';
@@ -35,6 +35,33 @@ import { CostTracker } from './cost-tracker.js';
35
35
  import { enrichAgentContext } from './context-enrichment.js';
36
36
  import { reportGateCheckRuns } from './check-runs.js';
37
37
  import { WorktreePoolManager, readParallelismMode } from './runtime/index.js';
38
+ /**
39
+ * Build an actionable error message when a required resource kind is absent.
40
+ *
41
+ * The loader silently drops resource-shaped YAML files that fail schema
42
+ * validation (recording them as `config.warnings`). Without this helper, the
43
+ * caller would only see "No <Kind> resource found in .ai-sdlc/" with no pointer
44
+ * to the real cause. This helper attaches any relevant warnings so the adopter
45
+ * knows *which* file to fix and *why* it was dropped.
46
+ *
47
+ * Non-resource YAMLs (no apiVersion+kind) are silently skipped by design
48
+ * (AISDLC-722 guard); this helper is only called when the resource is truly
49
+ * absent — i.e. no file with the right kind and a passing schema exists.
50
+ */
51
+ export function buildMissingResourceError(kind, config, configDir) {
52
+ const base = `No ${kind} resource found in ${configDir}`;
53
+ // Find any warnings that mention this kind — these are resource-shaped files
54
+ // that had `apiVersion`+`kind` but failed schema validation and were dropped.
55
+ const relevant = (config.warnings ?? []).filter((w) => w.error.includes('validation failed') || w.error.includes('unknown kind'));
56
+ if (relevant.length === 0) {
57
+ return `${base}. Add a ${kind} YAML file to your .ai-sdlc/ directory. Run \`ai-sdlc init\` to scaffold a minimal working configuration.`;
58
+ }
59
+ const details = relevant.map((w) => ` - ${w.file}: ${w.error}`).join('\n');
60
+ return (`${base}.\n` +
61
+ `The following file(s) declared a resource but failed schema validation and were dropped:\n` +
62
+ `${details}\n` +
63
+ `Fix the validation error(s) above, or run \`ai-sdlc init\` to scaffold a minimal working configuration.`);
64
+ }
38
65
  /**
39
66
  * Execute the full AI-SDLC pipeline for a given issue ID.
40
67
  */
@@ -89,13 +116,13 @@ export async function executePipeline(issueId, options = {}) {
89
116
  });
90
117
  }
91
118
  if (!config.qualityGate) {
92
- throw new Error('No QualityGate resource found in .ai-sdlc/');
119
+ throw new Error(buildMissingResourceError('QualityGate', config, configDir));
93
120
  }
94
121
  if (!config.agentRole) {
95
- throw new Error('No AgentRole resource found in .ai-sdlc/');
122
+ throw new Error(buildMissingResourceError('AgentRole', config, configDir));
96
123
  }
97
124
  if (!config.autonomyPolicy) {
98
- throw new Error('No AutonomyPolicy resource found in .ai-sdlc/');
125
+ throw new Error(buildMissingResourceError('AutonomyPolicy', config, configDir));
99
126
  }
100
127
  // Capture narrowed types for use in closures
101
128
  const qualityGate = config.qualityGate;
@@ -114,7 +141,11 @@ export async function executePipeline(issueId, options = {}) {
114
141
  const { org, repo } = getGitHubConfig(options.secretStore);
115
142
  const ghConfig = { org, repo, token: { secretRef: 'github-token' } };
116
143
  const tracker = options.tracker ?? resolveIssueTrackerFromConfig(config, ghConfig);
117
- const sc = options.sourceControl ?? createGitHubSourceControl(ghConfig);
144
+ // Resolve the source-control adapter from AdapterBinding config.
145
+ // options.sourceControl injection still wins (programmatic/testing use).
146
+ // When no SourceControl AdapterBinding is present, defaults to GitHub exactly
147
+ // as before — no regression for existing GitHub adopters (AC #2, AISDLC-530).
148
+ const sc = options.sourceControl ?? resolveSourceControlFromConfig(config, ghConfig);
118
149
  // 3. Fetch issue
119
150
  log.stage('validate-issue');
120
151
  const issue = await tracker.getIssue(issueId);
@@ -233,6 +264,12 @@ export async function restoreOriginalBranch(workDir, originalHead, log) {
233
264
  * rebase HEAD onto origin/<branch>, retry once. Rebase conflicts surface as
234
265
  * the original error so the operator can resolve manually.
235
266
  *
267
+ * Local-only mode (AISDLC-530 / AISDLC-527): when there is no 'origin' remote,
268
+ * push is skipped gracefully (returns true to signal the skip). This mirrors the
269
+ * AISDLC-527 fetch guard at step 7 — we detect the "no such remote" pattern in
270
+ * the push error and swallow it exactly as the fetch step does.
271
+ *
272
+ * @returns `true` when push was skipped (local-only repo), `false` when pushed successfully.
236
273
  * @internal — exported for unit tests.
237
274
  */
238
275
  export async function pushBranchWithRebase(workDir, branchName, log) {
@@ -241,10 +278,16 @@ export async function pushBranchWithRebase(workDir, branchName, log) {
241
278
  const env = cleanGitEnv();
242
279
  try {
243
280
  await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir, env });
244
- return;
281
+ return false;
245
282
  }
246
283
  catch (err) {
247
284
  const stderr = err.stderr ?? err.message;
285
+ // Local-only repos: no 'origin' remote configured — skip push gracefully.
286
+ // Mirror the AISDLC-527 fetch guard pattern exactly.
287
+ if (/no such remote|does not appear to be a git repository/i.test(stderr)) {
288
+ log?.info(`[pipeline] git push skipped: no 'origin' remote configured (local-only repo)`);
289
+ return true;
290
+ }
248
291
  if (!/non-fast-forward|rejected/i.test(stderr)) {
249
292
  throw err;
250
293
  }
@@ -264,9 +307,10 @@ export async function pushBranchWithRebase(workDir, branchName, log) {
264
307
  /* nothing to abort */
265
308
  }
266
309
  throw new Error(`Push rebase failed for branch ${branchName}: ${rebaseErr.message}. ` +
267
- `Resolve conflicts manually and re-run the pipeline.`);
310
+ `Resolve conflicts manually and re-run the pipeline.`, { cause: rebaseErr });
268
311
  }
269
312
  await execFileAsync('git', ['push', 'origin', branchName], { cwd: workDir, env });
313
+ return false;
270
314
  }
271
315
  async function executePipelineBody(issueId, issue, config, qualityGate, agentRole, autonomyPolicy, tracker, sc, auditLog, metricStore, options, log, workDir) {
272
316
  const issueNumber = issueIdToNumber(issueId);
@@ -409,13 +453,59 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
409
453
  // 7. Create branch and checkout locally (read pattern from pipeline config)
410
454
  const branchVars = buildIssueTemplateVars(issueId, issue.title);
411
455
  const branchName = interpolateBranchPattern(config.pipeline?.spec.branching?.pattern, branchVars);
456
+ // Defense against second-order command injection: reject branch names that
457
+ // start with '-' (git flag injection) or contain chars outside the safe ref
458
+ // charset. CodeQL alert #167 (js/second-order-command-line-injection).
459
+ validateBranchName(branchName);
412
460
  await sc.createBranch({ name: branchName });
413
461
  // cleanGitEnv() prevents leaked GIT_DIR from corrupting these calls (AISDLC-72).
414
- await execFileAsync('git', ['fetch', 'origin', branchName], {
415
- cwd: workDir,
416
- env: cleanGitEnv(),
417
- });
418
- await execFileAsync('git', ['checkout', branchName], { cwd: workDir, env: cleanGitEnv() });
462
+ // Guard: skip fetch when no 'origin' remote is configured (local-only repos).
463
+ // The push step already degrades gracefully for local repos; fetch must too.
464
+ //
465
+ // AISDLC-530 review fix: track whether we are in local-only mode so the checkout
466
+ // step can use `git checkout -b` (create) instead of `git checkout` (switch to
467
+ // existing). Remote paths create the branch via the SC adapter API THEN fetch it
468
+ // locally — so the branch already exists locally after the fetch, and plain
469
+ // `git checkout <name>` is correct. Local-only paths never have a remote, the
470
+ // SC adapter's createBranch is a no-op, so the branch does NOT exist yet and we
471
+ // MUST use `-b` to create it in one step.
472
+ let localOnlyMode = false;
473
+ try {
474
+ await execFileAsync('git', ['fetch', 'origin', branchName], {
475
+ cwd: workDir,
476
+ env: cleanGitEnv(),
477
+ });
478
+ }
479
+ catch (fetchErr) {
480
+ const fetchMsg = fetchErr.stderr ??
481
+ fetchErr.message ??
482
+ '';
483
+ // Only swallow the "origin remote is not configured at all" shape (local-only
484
+ // repos). Do NOT match a bare "not found" — that also matches "repository not
485
+ // found" (origin IS configured but the URL is wrong/deleted/inaccessible), which
486
+ // must propagate as a real config error rather than be silently skipped
487
+ // (AISDLC-527 code-review finding).
488
+ if (/no such remote|does not appear to be a git repository/i.test(fetchMsg)) {
489
+ log.info(`[pipeline] git fetch skipped: no 'origin' remote configured (local-only repo)`);
490
+ localOnlyMode = true;
491
+ }
492
+ else {
493
+ throw fetchErr;
494
+ }
495
+ }
496
+ // Local-only: branch was never pushed to a remote, so we must CREATE it with
497
+ // `git checkout -b <name>`. Remote paths: branch was fetched from origin, so
498
+ // it exists locally as FETCH_HEAD / refs/remotes/origin/<name> — plain
499
+ // `git checkout <name>` switches to the already-created tracking branch.
500
+ if (localOnlyMode) {
501
+ await execFileAsync('git', ['checkout', '-b', branchName], {
502
+ cwd: workDir,
503
+ env: cleanGitEnv(),
504
+ });
505
+ }
506
+ else {
507
+ await execFileAsync('git', ['checkout', branchName], { cwd: workDir, env: cleanGitEnv() });
508
+ }
419
509
  // 8. Resolve agent constraints
420
510
  const resolved = resolveConstraints(agentRole.spec.constraints, currentLevel);
421
511
  // 8b. Model selection: choose model based on complexity and budget pressure
@@ -444,7 +534,18 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
444
534
  `| Complexity | ${complexity} |\n` +
445
535
  `| Strategy | ${strategy} |\n`);
446
536
  await notifySlack(`:hammer_and_wrench: Agent working on \`${branchName}\` (model: ${selectedModel ?? 'default'}, complexity: ${complexity})`);
447
- const runner = options.runner ?? new ClaudeCodeRunner();
537
+ // Resolve runner via registry (after discoverFromEnv):
538
+ // 1. options.runner injection (tests / programmatic override) — wins
539
+ // 2. options.runnerName (--runner flag) — must be registered; throws on unknown
540
+ // 3. AI_SDLC_RUNNER_PLUGIN env — dynamically loaded plugin
541
+ // 4. ClaudeCodeRunner built-in default
542
+ // NOTE: env-discovered runners (openai, copilot, etc.) are registered + selectable by
543
+ // name via --runner, but do NOT auto-win on env-var presence (AISDLC-529 code review).
544
+ const runnerRegistry = createRunnerRegistry();
545
+ const runner = await resolveRunner(runnerRegistry, {
546
+ injectedRunner: options.runner,
547
+ runnerName: options.runnerName,
548
+ });
448
549
  // Set up progress tracking — collects streaming events for the activity log
449
550
  const progressLog = [];
450
551
  const onProgress = (event) => {
@@ -535,7 +636,13 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
535
636
  await tracker.addComment(issueId, `## ${agentFailComment.title}\n\n${agentFailComment.body}`);
536
637
  throw new Error(`Agent failed on issue ${formatIssueRef(issueId)}: ${err}`);
537
638
  }
538
- result = stepOutput;
639
+ // Guard: ensure filesChanged is always an array even when a runner returns
640
+ // a partial AgentResult. Downstream consumers (.length, .map, etc.) crash
641
+ // on undefined — defaulting here keeps all reads safe (AISDLC-527).
642
+ result = {
643
+ ...stepOutput,
644
+ filesChanged: stepOutput.filesChanged ?? [],
645
+ };
539
646
  log.stageEnd('agent');
540
647
  auditLog.record({
541
648
  actor: 'system',
@@ -561,9 +668,29 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
561
668
  }
562
669
  }
563
670
  // 10. ABAC authorization check (if write permissions are defined)
564
- if (currentLevel.permissions.write.length > 0) {
671
+ // Guard: default to [] when permissions.write is undefined (minimal/default autonomy configs
672
+ // may omit the write list, causing .length to throw on undefined — AISDLC-527).
673
+ //
674
+ // NOTE on the empty/undefined case (AISDLC-527 security-review finding): per the
675
+ // framework's existing autonomy-policy semantics, an empty write allowlist means
676
+ // "no per-file write restriction at this level" (the default autonomy level ships
677
+ // `write: []`), so this block intentionally SKIPS per-file authorization when the
678
+ // list is empty. The `?? []` makes a *missing* list behave the same as an explicit
679
+ // empty one rather than crashing. Because skipping authorization is a permissive
680
+ // (fail-open) path, we log it so it is auditable rather than silent. Tightening the
681
+ // empty-allowlist semantics to fail-closed is a framework-wide policy change (it would
682
+ // change the meaning of the default `write: []` level) and is tracked separately, NOT
683
+ // resolved in this crash-guard task. Downstream `validateAgentOutput` blocked-paths +
684
+ // branch protection + human merge remain as defense-in-depth.
685
+ const writePermissions = currentLevel.permissions.write ?? [];
686
+ if (writePermissions.length > 0) {
565
687
  authorizeFilesChanged(result.filesChanged, currentLevel.permissions, agentRole.spec.constraints, auditLog, agentRole.metadata.name);
566
688
  }
689
+ else {
690
+ log.info(`[pipeline] ABAC per-file write authorization skipped: autonomy level '${currentLevel.name ?? 'unknown'}' ` +
691
+ `has an empty/undefined permissions.write allowlist (framework default semantics). ` +
692
+ `Downstream blocked-paths validation + branch protection still apply.`);
693
+ }
567
694
  // 11. Post activity log so users can see what the agent did
568
695
  const activitySection = progressLog.length > 0
569
696
  ? `### Activity Log\n\n\`\`\`\n${progressLog.slice(-40).join('\n')}\n\`\`\``
@@ -577,15 +704,20 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
577
704
  // cleanly. Without this, re-runs against an existing remote branch (e.g. when
578
705
  // the issue branch was updated by another pipeline run or by a hand-edit)
579
706
  // reject with non-fast-forward and the agent's work is stranded locally.
707
+ //
708
+ // Local-only mode (AISDLC-530): when there's no 'origin' remote, the push
709
+ // is skipped just like the fetch step at step 7. We detect this by attempting
710
+ // the push and checking for the "no such remote" pattern — same approach
711
+ // AISDLC-527 uses for the fetch guard above (build on, not duplicate).
580
712
  log.stage('push');
581
- await pushBranchWithRebase(workDir, branchName, log);
713
+ const pushSkipped = await pushBranchWithRebase(workDir, branchName, log);
582
714
  log.stageEnd('push');
583
715
  auditLog.record({
584
716
  actor: 'system',
585
717
  action: 'push',
586
718
  resource: `branch/${branchName}`,
587
719
  decision: 'allowed',
588
- details: { filesChanged: result.filesChanged.length, issueId },
720
+ details: { filesChanged: result.filesChanged.length, issueId, localOnly: pushSkipped },
589
721
  });
590
722
  // 13. Validate agent output against guardrails (after push)
591
723
  const validation = await withSpan(SPAN_NAMES.PIPELINE_STAGE, {
@@ -774,26 +906,39 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
774
906
  targetBranch,
775
907
  });
776
908
  log.stageEnd('create-pr');
909
+ // Local-only mode (AISDLC-530): when the source-control adapter is local,
910
+ // createPR returns a sentinel pr.url === 'local' — record as skipped.
911
+ const isLocalPr = prResult.url === 'local';
912
+ if (isLocalPr) {
913
+ log.info(`[pipeline] create-pr skipped: local-only mode (no remote). ` +
914
+ `Branch '${branchName}' is available locally.`);
915
+ }
777
916
  auditLog.record({
778
917
  actor: 'system',
779
918
  action: 'create',
780
919
  resource: 'pull-request',
781
920
  decision: 'allowed',
782
- details: { prUrl: prResult.url, issueId },
921
+ details: { prUrl: prResult.url, issueId, localOnly: isLocalPr },
783
922
  });
784
923
  return prResult;
785
924
  });
786
- // 14. Comment on issue with success (use notification template when available)
787
- const notifTemplates = config.pipeline?.spec.notifications?.templates;
788
- const prCreatedTemplate = notifTemplates?.['pr-created'];
789
- const prCreatedComment = prCreatedTemplate
790
- ? renderTemplate(prCreatedTemplate, {
791
- prUrl: pr.url,
792
- issueNumber: issueId,
793
- })
794
- : { title: NOTIFICATION_TITLES.prCreated, body: `Pull request created: ${pr.url}` };
795
- await tracker.addComment(issueId, `## ${prCreatedComment.title}\n\n${prCreatedComment.body}`);
796
- await notifySlack(`:pull_request: PR created: ${pr.url}`);
925
+ // 14. Comment on issue with success (use notification template when available).
926
+ // Local-only mode (AISDLC-530): skip tracker comment + Slack notification when
927
+ // there is no remote PR URL — posting "PR created: local" to the tracker would
928
+ // be misleading and is not actionable.
929
+ const isLocalOnlyPr = pr.url === 'local';
930
+ if (!isLocalOnlyPr) {
931
+ const notifTemplates = config.pipeline?.spec.notifications?.templates;
932
+ const prCreatedTemplate = notifTemplates?.['pr-created'];
933
+ const prCreatedComment = prCreatedTemplate
934
+ ? renderTemplate(prCreatedTemplate, {
935
+ prUrl: pr.url,
936
+ issueNumber: issueId,
937
+ })
938
+ : { title: NOTIFICATION_TITLES.prCreated, body: `Pull request created: ${pr.url}` };
939
+ await tracker.addComment(issueId, `## ${prCreatedComment.title}\n\n${prCreatedComment.body}`);
940
+ await notifySlack(`:pull_request: PR created: ${pr.url}`);
941
+ }
797
942
  // 14b. Record cost from agent result
798
943
  if (options.costTracker && result.tokenUsage) {
799
944
  const tu = result.tokenUsage;
@@ -872,8 +1017,13 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
872
1017
  details: { averageCoverage: avgCoverage, frameworks: complianceReports.length },
873
1018
  });
874
1019
  // 16b. Extended diagnostics (non-blocking)
1020
+ // Awaited so that every async operation started inside the diagnostics pass
1021
+ // completes before executePipeline returns. Without the await, fire-and-forget
1022
+ // promises (resolveAdapterFromGit, scanPipelineAdapters, loadAuditEntries) stay
1023
+ // pending past the function boundary and can fire into a closed vitest worker
1024
+ // MessagePort, producing ERR_IPC_CHANNEL_CLOSED (AISDLC-542).
875
1025
  try {
876
- runPipelineDiagnostics({
1026
+ await runPipelineDiagnostics({
877
1027
  config,
878
1028
  qualityGate,
879
1029
  agentRole,
@@ -942,9 +1092,15 @@ async function executePipelineBody(issueId, issue, config, qualityGate, agentRol
942
1092
  /**
943
1093
  * Non-blocking diagnostics pass that exercises all previously unwired modules.
944
1094
  * Runs after the main pipeline succeeds — failures are silently ignored.
1095
+ *
1096
+ * Returns a Promise so the caller can await all async work, preventing dangling
1097
+ * promises from outliving the pipeline call and firing into a closed vitest
1098
+ * worker MessagePort (AISDLC-542: ERR_IPC_CHANNEL_CLOSED teardown race).
945
1099
  */
946
- function runPipelineDiagnostics(input) {
1100
+ async function runPipelineDiagnostics(input) {
947
1101
  const { config, qualityGate, agentRole, autonomyPolicy, log } = input;
1102
+ // Collect async operations; await all at the end so no promise escapes the function.
1103
+ const asyncOps = [];
948
1104
  // Policy evaluators: Rego, CEL, ABAC, gate evaluation, complexity scoring
949
1105
  const _rego = createPipelineRegoEvaluator();
950
1106
  const _cel = createPipelineCELEvaluator();
@@ -969,9 +1125,10 @@ function runPipelineDiagnostics(input) {
969
1125
  const adapterRegistry = createPipelineAdapterRegistry();
970
1126
  log.info(`Adapter registry: ${adapterRegistry.list().length} adapters registered`);
971
1127
  const _bridge = createPipelineWebhookBridge();
972
- // resolveAdapterFromGit and scanPipelineAdapters are asyncfire and forget
973
- void resolveAdapterFromGit('github:ai-sdlc-framework/ai-sdlc').catch(() => { });
974
- void scanPipelineAdapters({ basePath: `${input.workDir}/.ai-sdlc/adapters` }).catch(() => { });
1128
+ // Collect async ops instead of fire-and-forget awaited below via Promise.allSettled
1129
+ // so no pending promise outlives this function (AISDLC-542).
1130
+ asyncOps.push(resolveAdapterFromGit('github:ai-sdlc-framework/ai-sdlc').catch(() => { }));
1131
+ asyncOps.push(scanPipelineAdapters({ basePath: `${input.workDir}/.ai-sdlc/adapters` }).catch(() => { }));
975
1132
  // Extended compliance: per-framework checks, control catalog, mappings
976
1133
  const controlIds = getControlCatalog();
977
1134
  const frameworks = listSupportedFrameworks();
@@ -1026,10 +1183,15 @@ function runPipelineDiagnostics(input) {
1026
1183
  resource: 'pipeline',
1027
1184
  decision: 'allowed',
1028
1185
  });
1029
- void loadAuditEntries(auditPath).catch(() => { });
1186
+ // Collected into asyncOps — awaited below so the promise doesn't escape (AISDLC-542).
1187
+ asyncOps.push(loadAuditEntries(auditPath).catch(() => { }));
1030
1188
  // Note: rotateAuditLog intentionally omitted — it truncates the file,
1031
1189
  // which would empty it before artifact upload can capture the contents.
1032
1190
  }
1191
+ // Await all async diagnostic operations so no promise outlives this function.
1192
+ // allSettled keeps the best-effort semantic: individual failures are silently swallowed,
1193
+ // but the caller can await the diagnostics without unhandled rejections leaking.
1194
+ await Promise.allSettled(asyncOps);
1033
1195
  }
1034
1196
  // ── Gitignore helper ─────────────────────────────────────────────────
1035
1197
  const RUNTIME_GITIGNORE_PATHS = ['.ai-sdlc/state.db', '.ai-sdlc/state/', '.ai-sdlc/audit.jsonl'];