@loucompanion/forge-bridge 0.1.3-dev.b6be5e7f9668 → 0.1.3-dev.ba7d36726eda

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 (141) hide show
  1. package/README.md +78 -0
  2. package/dist/bridge/bridge.base.d.ts +5 -1
  3. package/dist/bridge/bridge.service.d.ts +52 -3
  4. package/dist/bridge/bridge.service.js +796 -22
  5. package/dist/bridge/bridge.types.d.ts +135 -1
  6. package/dist/cleanup/cleanup.service.d.ts +13 -2
  7. package/dist/cleanup/cleanup.service.js +435 -3
  8. package/dist/cleanup/cleanup.types.d.ts +10 -0
  9. package/dist/cleanup/index.d.ts +1 -1
  10. package/dist/cleanup/index.js +1 -1
  11. package/dist/cleanup/internals.d.ts +2 -2
  12. package/dist/cleanup/internals.js +1 -1
  13. package/dist/cli/cli.help.d.ts +2 -0
  14. package/dist/cli/cli.help.js +217 -0
  15. package/dist/cli/cli.service.d.ts +42 -2
  16. package/dist/cli/cli.service.js +356 -205
  17. package/dist/cli/local-config.d.ts +6 -0
  18. package/dist/cli/local-config.js +45 -2
  19. package/dist/runtime/attempt-node-runtime-launcher.d.ts +15 -0
  20. package/dist/runtime/attempt-node-runtime-launcher.js +50 -0
  21. package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
  22. package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
  23. package/dist/runtime/attempt-process-supervisor.d.ts +74 -0
  24. package/dist/runtime/attempt-process-supervisor.js +290 -0
  25. package/dist/runtime/host-listener-inspector.d.ts +28 -0
  26. package/dist/runtime/host-listener-inspector.js +185 -0
  27. package/dist/runtime/host-port-broker.d.ts +76 -0
  28. package/dist/runtime/host-port-broker.js +333 -0
  29. package/dist/runtime/host-runtime-contract.d.ts +39 -0
  30. package/dist/runtime/host-runtime-contract.js +295 -0
  31. package/dist/runtime/host-runtime-lease.d.ts +122 -0
  32. package/dist/runtime/host-runtime-lease.js +1241 -0
  33. package/dist/runtime/host-runtime-promotion.d.ts +21 -0
  34. package/dist/runtime/host-runtime-promotion.js +128 -0
  35. package/dist/runtime/node-host-runtime-adapter.d.ts +44 -0
  36. package/dist/runtime/node-host-runtime-adapter.js +205 -0
  37. package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
  38. package/dist/runtime/playwright-host-runtime-adapter.js +136 -0
  39. package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
  40. package/dist/runtime/runtime-isolation-lease.js +158 -0
  41. package/dist/sandbox/command-runner.d.ts +4 -0
  42. package/dist/sandbox/command-runner.js +47 -0
  43. package/dist/sandbox/contract.d.ts +5 -0
  44. package/dist/sandbox/contract.js +276 -0
  45. package/dist/sandbox/event-budget.d.ts +11 -0
  46. package/dist/sandbox/event-budget.js +27 -0
  47. package/dist/sandbox/image-policy.d.ts +35 -0
  48. package/dist/sandbox/image-policy.js +247 -0
  49. package/dist/sandbox/index.d.ts +5 -0
  50. package/dist/sandbox/index.js +5 -0
  51. package/dist/sandbox/lease-watchdog.d.ts +18 -0
  52. package/dist/sandbox/lease-watchdog.js +67 -0
  53. package/dist/sandbox/preflight.d.ts +45 -0
  54. package/dist/sandbox/preflight.js +300 -0
  55. package/dist/sandbox/process.d.ts +20 -0
  56. package/dist/sandbox/process.js +209 -0
  57. package/dist/sandbox/provider-auth/codex.d.ts +26 -0
  58. package/dist/sandbox/provider-auth/codex.js +212 -0
  59. package/dist/sandbox/repository-lock.d.ts +5 -0
  60. package/dist/sandbox/repository-lock.js +119 -0
  61. package/dist/sandbox/runtime.d.ts +297 -0
  62. package/dist/sandbox/runtime.js +2338 -0
  63. package/dist/sandbox/sandbox.types.d.ts +86 -0
  64. package/dist/sandbox/sandbox.types.js +1 -0
  65. package/dist/sandbox/smoke.d.ts +76 -0
  66. package/dist/sandbox/smoke.js +380 -0
  67. package/dist/sandbox/worker-supervisor.d.ts +13 -0
  68. package/dist/sandbox/worker-supervisor.js +36 -0
  69. package/dist/sandbox/worker.d.ts +1 -0
  70. package/dist/sandbox/worker.js +225 -0
  71. package/dist/sandbox/workspace.service.d.ts +13 -0
  72. package/dist/sandbox/workspace.service.js +67 -0
  73. package/dist/session/phase-completion.artifacts.d.ts +10 -0
  74. package/dist/session/phase-completion.artifacts.js +620 -0
  75. package/dist/session/phase-completion.d.ts +32 -44
  76. package/dist/session/phase-completion.helpers.d.ts +112 -0
  77. package/dist/session/phase-completion.helpers.js +665 -0
  78. package/dist/session/phase-completion.js +28 -3485
  79. package/dist/session/phase-completion.prompts.d.ts +14 -0
  80. package/dist/session/phase-completion.prompts.js +220 -0
  81. package/dist/session/phase-completion.runtime.d.ts +36 -0
  82. package/dist/session/phase-completion.runtime.js +611 -0
  83. package/dist/session/phase-completion.schemas.d.ts +3 -0
  84. package/dist/session/phase-completion.schemas.js +707 -0
  85. package/dist/session/phase-completion.validation.d.ts +34 -0
  86. package/dist/session/phase-completion.validation.js +724 -0
  87. package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
  88. package/dist/session/provider-cli/provider-cli.artifacts.js +778 -0
  89. package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
  90. package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
  91. package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
  92. package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
  93. package/dist/session/provider-cli/provider-cli.deterministic.js +329 -0
  94. package/dist/session/provider-cli/provider-cli.process.d.ts +34 -0
  95. package/dist/session/provider-cli/provider-cli.process.js +316 -0
  96. package/dist/session/provider-cli/provider-cli.prompts.d.ts +47 -0
  97. package/dist/session/provider-cli/provider-cli.prompts.js +573 -0
  98. package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
  99. package/dist/session/provider-cli/provider-cli.qa-sidecars.js +65 -0
  100. package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
  101. package/dist/session/provider-cli/provider-cli.runtime.js +41 -0
  102. package/dist/session/provider-cli/provider-cli.service.d.ts +9 -53
  103. package/dist/session/provider-cli/provider-cli.service.js +182 -2536
  104. package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
  105. package/dist/session/provider-cli/provider-output-state.d.ts +20 -0
  106. package/dist/session/provider-cli/provider-output-state.js +445 -0
  107. package/dist/session/session-redaction.d.ts +3 -0
  108. package/dist/session/session-redaction.js +23 -0
  109. package/dist/session/session.base.d.ts +4 -0
  110. package/dist/session/session.service.d.ts +6 -1
  111. package/dist/session/session.service.js +202 -53
  112. package/dist/session/session.types.d.ts +123 -1
  113. package/dist/session/session.types.js +301 -3
  114. package/dist/shared/path/path.js +3 -1
  115. package/dist/shared/process/host-process-recovery.d.ts +12 -0
  116. package/dist/shared/process/host-process-recovery.js +78 -0
  117. package/dist/shared/process/host-process-supervisor.d.ts +40 -0
  118. package/dist/shared/process/host-process-supervisor.js +195 -0
  119. package/dist/shared/process/windows-launch.d.ts +8 -0
  120. package/dist/shared/process/windows-launch.js +41 -0
  121. package/dist/shared/provider-capabilities.d.ts +1 -0
  122. package/dist/shared/provider-capabilities.js +3 -0
  123. package/dist/shared/redaction/redaction.js +10 -3
  124. package/dist/version.d.ts +1 -1
  125. package/dist/version.js +1 -1
  126. package/dist/worktree/artifact-injector.js +39 -6
  127. package/dist/worktree/dependency-warmer.d.ts +11 -0
  128. package/dist/worktree/dependency-warmer.js +45 -1
  129. package/dist/worktree/index.d.ts +1 -0
  130. package/dist/worktree/index.js +1 -0
  131. package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
  132. package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
  133. package/dist/worktree/worktree-paths.d.ts +4 -0
  134. package/dist/worktree/worktree-paths.js +30 -0
  135. package/dist/worktree/worktree.base.d.ts +3 -0
  136. package/dist/worktree/worktree.service.d.ts +8 -3
  137. package/dist/worktree/worktree.service.js +89 -52
  138. package/dist/worktree/worktree.types.d.ts +3 -1
  139. package/package.json +6 -3
  140. package/runtime/windows-job-supervisor.ps1 +287 -0
  141. package/runtime/windows-listener-owner.ps1 +33 -0
@@ -0,0 +1,665 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { debugCompletionPhaseOrder, debugGateToolNames, decomposeCompletionGateOrder, decomposeGateToolNames, draftCompletionGateOrder, draftGateToolNames, qaCompletionGateOrder, qaGateToolNames, resolvedFindingStatuses } from "./phase-completion.js";
6
+ export function normalizeArtifactPayload(payload, defaults) {
7
+ const normalized = stripUndefined({ ...payload });
8
+ delete normalized.report_markdown;
9
+ delete normalized.content_hash;
10
+ delete normalized.contentHash;
11
+ normalized.schema_version = numberOrDefault(normalized.schema_version, 1);
12
+ normalized.phase = defaults.phase;
13
+ normalized.work_item_id = defaults.workItemId;
14
+ const ownerFields = artifactOwnerFields(defaults);
15
+ Object.assign(normalized, ownerFields);
16
+ normalized.status = defaults.status;
17
+ normalized.produced_by = defaults.producedBy;
18
+ return normalized;
19
+ }
20
+ export function normalizeQaGateArtifactPayload(payload, defaults) {
21
+ const normalized = stripUndefined({ ...payload });
22
+ delete normalized.report_markdown;
23
+ delete normalized.content_hash;
24
+ delete normalized.contentHash;
25
+ normalized.schema_version = numberOrDefault(normalized.schema_version, 1);
26
+ normalized.phase = "qa";
27
+ normalized.gate = defaults.gate;
28
+ normalized.work_item_id = defaults.workItemId;
29
+ const ownerFields = artifactOwnerFields(defaults);
30
+ Object.assign(normalized, ownerFields);
31
+ normalized.status = defaults.status;
32
+ normalized.produced_by = defaults.producedBy;
33
+ if (defaults.gate === "qa_plan") {
34
+ const checklist = qaStringArray(normalized, "checklist");
35
+ normalized.checklist = checklist.length > 0 ? checklist : defaultQaChecklist(defaults.status);
36
+ normalized.acceptance_criteria = qaStringArray(normalized, "acceptance_criteria");
37
+ }
38
+ if (defaults.gate === "qa_findings") {
39
+ normalized.findings = qaObjectArray(normalized, "findings");
40
+ normalized.candidate_bugs = qaObjectArray(normalized, "candidate_bugs");
41
+ }
42
+ return normalized;
43
+ }
44
+ export function reportMarkdown(payload, phase, status, producedBy, contentHash) {
45
+ const explicit = optionalString(payload, "report_markdown");
46
+ if (explicit) {
47
+ return explicit.endsWith("\n") ? explicit : `${explicit}\n`;
48
+ }
49
+ const summary = optionalString(payload, "summary") ?? "No summary provided.";
50
+ return `# Forge ${phase.replace(/_/g, " ")} completion
51
+
52
+ Status: ${status}
53
+ Produced by: ${producedBy}
54
+ Content hash: ${contentHash}
55
+
56
+ ## Summary
57
+
58
+ ${summary}
59
+ `;
60
+ }
61
+ export function parseStrictCompletionPhase(value) {
62
+ const normalized = (value ?? "").trim().toLowerCase().replace(/-/g, "_");
63
+ const debugPhase = normalizeDebugCompletionPhase(normalized);
64
+ if (debugPhase) {
65
+ return debugPhase;
66
+ }
67
+ if (normalized === "implementation" || normalized === "implement" || normalized === "develop") {
68
+ return "implementation";
69
+ }
70
+ if (normalized === "self_review" || normalized === "selfreview" || normalized === "review") {
71
+ return "self_review";
72
+ }
73
+ if (normalized === "validate" || normalized === "validation") {
74
+ return "validation";
75
+ }
76
+ if (normalized === "qa" || normalized === "quality_assurance" || normalized === "qualityassurance") {
77
+ return "qa";
78
+ }
79
+ if (normalized === "architect" || normalized === "architect_plan" || normalized === "planning" || normalized === "plan") {
80
+ return "architect";
81
+ }
82
+ if (normalized === "draft" || normalized === "drafting" || normalized === "draft_work_order") {
83
+ return "draft";
84
+ }
85
+ if (normalized === "decompose" || normalized === "decomposition" || normalized === "work_order_decomposition" || normalized === "feature_decomposition") {
86
+ return "decompose";
87
+ }
88
+ return null;
89
+ }
90
+ export function normalizeDebugCompletionPhase(value) {
91
+ const normalized = (value ?? "").trim().toLowerCase().replace(/-/g, "_");
92
+ switch (normalized) {
93
+ case "debug_repro":
94
+ case "debugrepro":
95
+ case "repro":
96
+ case "reproduce":
97
+ return "debug_repro";
98
+ case "debug_root_cause":
99
+ case "debugrootcause":
100
+ case "root_cause":
101
+ case "rootcause":
102
+ return "debug_root_cause";
103
+ case "debug_fix":
104
+ case "debugfix":
105
+ case "fix":
106
+ return "debug_fix";
107
+ case "debug_regression":
108
+ case "debugregression":
109
+ case "regression":
110
+ return "debug_regression";
111
+ default:
112
+ return null;
113
+ }
114
+ }
115
+ export function isDevelopCompletionPhase(phase) {
116
+ return phase === "implementation" || phase === "self_review" || phase === "validation";
117
+ }
118
+ export function isDebugCompletionPhase(phase) {
119
+ return debugCompletionPhaseOrder.includes(phase);
120
+ }
121
+ export function asDebugCompletionPhase(phase) {
122
+ if (isDebugCompletionPhase(phase)) {
123
+ return phase;
124
+ }
125
+ throw new Error(`Expected Debug completion phase, got ${phase}.`);
126
+ }
127
+ export function parseCompletionRedactionSecrets(raw) {
128
+ if (!raw?.trim()) {
129
+ return [];
130
+ }
131
+ try {
132
+ const parsed = JSON.parse(raw);
133
+ if (!Array.isArray(parsed)) {
134
+ return [];
135
+ }
136
+ return parsed.filter((value) => typeof value === "string" && value.length > 0);
137
+ }
138
+ catch {
139
+ return [];
140
+ }
141
+ }
142
+ export function phaseFromToolName(toolName) {
143
+ const normalized = toolName.trim().toLowerCase();
144
+ if (normalized === "forge.complete_implementation" || normalized === "complete_implementation") {
145
+ return "implementation";
146
+ }
147
+ if (normalized === "forge.complete_self_review" || normalized === "complete_self_review") {
148
+ return "self_review";
149
+ }
150
+ if (normalized === "forge.complete_validation" || normalized === "complete_validation") {
151
+ return "validation";
152
+ }
153
+ if (normalized === "forge.complete_qa" || normalized === "complete_qa") {
154
+ return "qa";
155
+ }
156
+ if (normalized === "forge.complete_architect_plan" || normalized === "complete_architect_plan") {
157
+ return "architect";
158
+ }
159
+ if (draftGateFromToolName(toolName)) {
160
+ return "draft";
161
+ }
162
+ if (decomposeGateFromToolName(toolName)) {
163
+ return "decompose";
164
+ }
165
+ const debugGate = debugGateFromToolName(toolName);
166
+ if (debugGate) {
167
+ return debugGate;
168
+ }
169
+ if (qaGateFromToolName(toolName)) {
170
+ return "qa";
171
+ }
172
+ throw new Error(`Unknown Forge completion tool: ${toolName}`);
173
+ }
174
+ export function draftGateFromToolName(toolName) {
175
+ const normalized = toolName.trim().toLowerCase();
176
+ for (const gate of draftCompletionGateOrder) {
177
+ const base = draftGateToolNames[gate];
178
+ if (normalized === `forge.${base}` || normalized === base) {
179
+ return gate;
180
+ }
181
+ }
182
+ return null;
183
+ }
184
+ export function decomposeGateFromToolName(toolName) {
185
+ for (const gate of decomposeCompletionGateOrder) {
186
+ const base = decomposeGateToolNames[gate];
187
+ if (toolName === base || toolName === `forge.${base}`) {
188
+ return gate;
189
+ }
190
+ }
191
+ return null;
192
+ }
193
+ export function isDecomposeDispatch(dispatch) {
194
+ const draftKind = dispatch.draft?.kind?.trim().toLowerCase();
195
+ return dispatch.execution.lane.trim().toLowerCase() === "decompose"
196
+ || dispatch.session.lane.trim().toLowerCase() === "decompose"
197
+ || draftKind === "work_order_decomposition"
198
+ || draftKind === "feature_decomposition";
199
+ }
200
+ export function debugGateFromToolName(toolName) {
201
+ const normalized = toolName.trim().toLowerCase();
202
+ for (const gate of debugCompletionPhaseOrder) {
203
+ const base = debugGateToolNames[gate];
204
+ if (normalized === `forge.${base}` || normalized === base) {
205
+ return gate;
206
+ }
207
+ }
208
+ return null;
209
+ }
210
+ export function qaGateFromToolName(toolName) {
211
+ const normalized = toolName.trim().toLowerCase();
212
+ for (const gate of qaCompletionGateOrder) {
213
+ const base = qaGateToolNames[gate];
214
+ if (normalized === `forge.${base}` || normalized === base) {
215
+ return gate;
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+ export function requireField(payload, key) {
221
+ if (payload[key] === undefined || payload[key] === null || (typeof payload[key] === "string" && payload[key].trim().length === 0)) {
222
+ throw new Error(`${key} is required.`);
223
+ }
224
+ }
225
+ export function requireNonPassEvidence(payload) {
226
+ const evidenceKeys = ["blockers", "known_gaps", "findings", "remaining_risks", "commands", "failures"];
227
+ const hasEvidence = evidenceKeys.some((key) => {
228
+ const value = payload[key];
229
+ if (typeof value === "string") {
230
+ return value.trim().length > 0;
231
+ }
232
+ if (Array.isArray(value)) {
233
+ return value.length > 0;
234
+ }
235
+ return Boolean(value && typeof value === "object");
236
+ });
237
+ if (!hasEvidence) {
238
+ throw new Error("non-pass completion requires explicit blockers, findings, failures, or known gaps.");
239
+ }
240
+ }
241
+ export function requireImplementationEvidence(payload) {
242
+ const knownGaps = payload.known_gaps;
243
+ const hasKnownGap = Array.isArray(knownGaps) && knownGaps.length > 0;
244
+ for (const key of ["changed_files", "tests_added_or_updated", "docs_added_or_updated"]) {
245
+ const value = payload[key];
246
+ if (Array.isArray(value) && value.length === 0 && !hasKnownGap) {
247
+ throw new Error(`${key} is empty; known_gaps must explain why.`);
248
+ }
249
+ }
250
+ }
251
+ export function requirePassingCommands(payload) {
252
+ const commands = payload.commands;
253
+ if (!Array.isArray(commands)) {
254
+ return;
255
+ }
256
+ if (commands.length === 0) {
257
+ throw new Error("commands must include at least one deterministic validation command.");
258
+ }
259
+ for (const [index, command] of commands.entries()) {
260
+ if (!command || typeof command !== "object" || Array.isArray(command)) {
261
+ throw new Error(`commands[${index}] must be an object.`);
262
+ }
263
+ const record = command;
264
+ if (typeof record.command !== "string" || record.command.trim().length === 0) {
265
+ throw new Error(`commands[${index}].command must be a non-empty string.`);
266
+ }
267
+ if (typeof record.summary !== "string" || record.summary.trim().length === 0) {
268
+ throw new Error(`commands[${index}].summary must be a non-empty string.`);
269
+ }
270
+ if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
271
+ throw new Error(`commands[${index}].exit_code must be a finite number.`);
272
+ }
273
+ if (record.exit_code !== 0) {
274
+ throw new Error(`commands[${index}].exit_code must be 0 when status is pass.`);
275
+ }
276
+ const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
277
+ if (status !== "pass") {
278
+ throw new Error(`commands[${index}].status must be pass.`);
279
+ }
280
+ }
281
+ }
282
+ export function requireArray(payload, key) {
283
+ if (!Array.isArray(payload[key])) {
284
+ throw new Error(`${key} must be an array.`);
285
+ }
286
+ }
287
+ export function requireNonEmptyArray(payload, key) {
288
+ requireArray(payload, key);
289
+ if (arrayLength(payload, key) === 0) {
290
+ throw new Error(`${key} must include at least one value.`);
291
+ }
292
+ }
293
+ export function arrayLength(payload, key) {
294
+ const value = payload[key];
295
+ return Array.isArray(value) ? value.length : 0;
296
+ }
297
+ export function requireAllowedStatus(payload, key, allowed) {
298
+ const value = normalizedString(optionalString(payload, key));
299
+ if (!allowed.includes(value)) {
300
+ throw new Error(`${key} must be one of ${allowed.join(", ")}.`);
301
+ }
302
+ }
303
+ export function requireObject(payload, key) {
304
+ const value = payload[key];
305
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
306
+ throw new Error(`${key} must be an object.`);
307
+ }
308
+ }
309
+ export function requiredRecord(payload, key) {
310
+ requireObject(payload, key);
311
+ return payload[key];
312
+ }
313
+ export function requireBoolean(payload, key) {
314
+ if (typeof payload[key] !== "boolean") {
315
+ throw new Error(`${key} must be a boolean.`);
316
+ }
317
+ }
318
+ export function requireNumericOne(payload, key, pathLabel) {
319
+ if (payload[key] !== 1) {
320
+ throw new Error(`${pathLabel}: expected numeric 1; correction: set ${pathLabel} to 1.`);
321
+ }
322
+ }
323
+ export function requireSatisfiedAcceptanceCriteria(payload) {
324
+ const criteria = payload.acceptance_criteria;
325
+ if (!Array.isArray(criteria)) {
326
+ return;
327
+ }
328
+ for (const [index, criterion] of criteria.entries()) {
329
+ if (!criterion || typeof criterion !== "object" || Array.isArray(criterion)) {
330
+ throw new Error(`acceptance_criteria[${index}] must be an object.`);
331
+ }
332
+ const record = criterion;
333
+ const status = optionalString(record, "status")?.toLowerCase();
334
+ const evidence = optionalString(record, "evidence");
335
+ if (status !== "satisfied" || !evidence) {
336
+ throw new Error(`acceptance_criteria[${index}] must be satisfied with evidence for status pass.`);
337
+ }
338
+ }
339
+ }
340
+ export function validateResolvedFindings(payload) {
341
+ const resolvedFindings = payload.resolved_findings;
342
+ if (resolvedFindings === undefined || resolvedFindings === null) {
343
+ return;
344
+ }
345
+ if (!Array.isArray(resolvedFindings)) {
346
+ throw new Error("resolved_findings must be an array.");
347
+ }
348
+ for (const [index, finding] of resolvedFindings.entries()) {
349
+ const prefix = `resolved_findings[${index}].`;
350
+ if (!finding || typeof finding !== "object" || Array.isArray(finding)) {
351
+ throw new Error(`resolved_findings[${index}] must be an object.`);
352
+ }
353
+ const record = finding;
354
+ requireNonEmptyStringRecord(record, "qa_id", prefix);
355
+ requireKnownStringRecord(record, "status", resolvedFindingStatuses, prefix);
356
+ requireNonEmptyStringRecord(record, "summary", prefix);
357
+ requireSubstantiveValueRecord(record, "evidence", prefix);
358
+ requireArrayRecord(record, "changed_files", prefix);
359
+ requireArrayRecord(record, "tests_added_or_updated", prefix);
360
+ requireSubstantiveValueRecord(record, "regression_evidence", prefix);
361
+ if (normalizedString(record.status) === "resolved") {
362
+ if (record.changed_files.length === 0) {
363
+ throw new Error(`${prefix}changed_files must include at least one changed file for resolved QA findings.`);
364
+ }
365
+ if (record.tests_added_or_updated.length === 0) {
366
+ throw new Error(`${prefix}tests_added_or_updated must include at least one regression test for resolved QA findings.`);
367
+ }
368
+ }
369
+ }
370
+ }
371
+ export function qaEvidenceSource(payload) {
372
+ const evidence = payload.evidence;
373
+ return evidence && typeof evidence === "object" && !Array.isArray(evidence)
374
+ ? evidence
375
+ : {};
376
+ }
377
+ export function qaArray(payload, key, fallback = {}) {
378
+ const value = payload[key] ?? fallback[key];
379
+ if (value === undefined || value === null) {
380
+ return [];
381
+ }
382
+ if (!Array.isArray(value)) {
383
+ throw new Error(`${key} must be an array.`);
384
+ }
385
+ return value;
386
+ }
387
+ export function qaObjectArray(payload, key) {
388
+ return qaArray(payload, key).map((entry, index) => {
389
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
390
+ throw new Error(`${key}[${index}] must be an object.`);
391
+ }
392
+ return entry;
393
+ });
394
+ }
395
+ export function qaPolicyWarnings(payload) {
396
+ return qaArray(payload, "policy_warnings").map((entry, index) => {
397
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
398
+ throw new Error(`policy_warnings[${index}] must be an object.`);
399
+ }
400
+ const warning = entry;
401
+ const prefix = `policy_warnings[${index}].`;
402
+ requireNonEmptyStringRecord(warning, "code", prefix);
403
+ requireNonEmptyStringRecord(warning, "message", prefix);
404
+ const normalized = {
405
+ ...warning,
406
+ code: optionalString(warning, "code"),
407
+ message: optionalString(warning, "message")
408
+ };
409
+ if (warning.severity !== undefined && warning.severity !== null) {
410
+ requireNonEmptyStringRecord(warning, "severity", prefix);
411
+ normalized.severity = optionalString(warning, "severity");
412
+ }
413
+ return normalized;
414
+ });
415
+ }
416
+ export function qaStringArray(payload, key) {
417
+ const value = payload[key];
418
+ if (value === undefined || value === null) {
419
+ return [];
420
+ }
421
+ if (!Array.isArray(value)) {
422
+ throw new Error(`${key} must be an array.`);
423
+ }
424
+ return value.map((entry, index) => {
425
+ if (typeof entry !== "string" || entry.trim().length === 0) {
426
+ throw new Error(`${key}[${index}] must be a non-empty string.`);
427
+ }
428
+ return entry;
429
+ });
430
+ }
431
+ export function requireEvidenceRecord(value, label) {
432
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
433
+ throw new Error(`${label} must be an object.`);
434
+ }
435
+ }
436
+ export function requireNonEmptyStringRecord(source, key, prefix = "") {
437
+ if (!optionalString(source, key)) {
438
+ throw new Error(`${prefix}${key} is required.`);
439
+ }
440
+ }
441
+ export function requireArrayRecord(source, key, prefix = "") {
442
+ if (!Array.isArray(source[key])) {
443
+ throw new Error(`${prefix}${key} must be an array.`);
444
+ }
445
+ }
446
+ export function requireKnownStringRecord(source, key, allowed, prefix = "") {
447
+ const value = normalizedString(optionalString(source, key));
448
+ if (!allowed.has(value)) {
449
+ throw new Error(`${prefix}${key} must be one of ${[...allowed].sort().join(", ")}.`);
450
+ }
451
+ }
452
+ export function requireNonEmptyStringArrayRecord(source, key, prefix = "") {
453
+ const value = source[key];
454
+ if (!Array.isArray(value) || value.length === 0) {
455
+ throw new Error(`${prefix}${key} must include at least one value.`);
456
+ }
457
+ for (const [index, entry] of value.entries()) {
458
+ if (typeof entry !== "string" || entry.trim().length === 0) {
459
+ throw new Error(`${prefix}${key}[${index}] must be a non-empty string.`);
460
+ }
461
+ }
462
+ }
463
+ export function requireSubstantiveValueRecord(source, key, prefix = "") {
464
+ if (!hasSubstantiveJsonValue(source[key])) {
465
+ throw new Error(`${prefix}${key} must include at least one concrete evidence reference or summary.`);
466
+ }
467
+ }
468
+ export function requireSubstantiveEvidenceRecord(source, prefix = "") {
469
+ const evidence = source.evidence;
470
+ if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
471
+ throw new Error(`${prefix}evidence must be an object.`);
472
+ }
473
+ if (!Object.values(evidence).some(hasSubstantiveJsonValue)) {
474
+ throw new Error(`${prefix}evidence must include at least one concrete evidence reference or summary.`);
475
+ }
476
+ }
477
+ export function hasSubstantiveJsonValue(value) {
478
+ if (typeof value === "string") {
479
+ return value.trim().length > 0;
480
+ }
481
+ if (Array.isArray(value)) {
482
+ return value.length > 0;
483
+ }
484
+ if (value && typeof value === "object") {
485
+ return Object.keys(value).length > 0;
486
+ }
487
+ return false;
488
+ }
489
+ export function normalizedString(value) {
490
+ return typeof value === "string" ? value.trim().toLowerCase().replace(/-/g, "_") : "";
491
+ }
492
+ export function normalizedOwnerType(value) {
493
+ const normalized = normalizedString(value);
494
+ return normalized === "work_item" || normalized === "story" ? normalized : undefined;
495
+ }
496
+ export function artifactOwnerFields(config) {
497
+ const ownerType = normalizedOwnerType(config.ownerType);
498
+ const ownerId = config.ownerId?.trim();
499
+ if (!ownerType && !ownerId) {
500
+ return {};
501
+ }
502
+ return {
503
+ owner_type: ownerType ?? "work_item",
504
+ owner_id: ownerId || config.workItemId
505
+ };
506
+ }
507
+ export function artifactReferenceOwnerFields(config) {
508
+ const owner = artifactOwnerFields(config);
509
+ return owner.owner_type && owner.owner_id
510
+ ? { ownerType: owner.owner_type, ownerId: owner.owner_id }
511
+ : {};
512
+ }
513
+ export function artifactOwnerDirectory(ownerType) {
514
+ return normalizedOwnerType(ownerType) === "story" ? "stories" : "work-items";
515
+ }
516
+ export function artifactOwnerRelativeRoot(ownerType, ownerId, workItemId) {
517
+ const owner = artifactOwnerFields({ workItemId, ownerType, ownerId });
518
+ return owner.owner_type && owner.owner_id
519
+ ? [".forge", artifactOwnerDirectory(owner.owner_type), owner.owner_id, "artifacts"].join("/")
520
+ : [".forge", "work-items", workItemId, "artifacts"].join("/");
521
+ }
522
+ export function defaultQaChecklist(status) {
523
+ return [
524
+ "Review the validated work item against acceptance criteria.",
525
+ "Inspect implementation, self-review, and validation evidence when available.",
526
+ status === "blocked" ? "Record the blocker reason preventing a conclusive QA verdict." : "Record concrete QA evidence and findings."
527
+ ];
528
+ }
529
+ export function qaArtifactRelativePath(workItemId, fileName) {
530
+ return [".forge", "work-items", workItemId, "artifacts", fileName].join("/");
531
+ }
532
+ export function qaArtifactRelativePathForOwner(ownerType, ownerId, fileName) {
533
+ return [".forge", artifactOwnerDirectory(ownerType), ownerId, "artifacts", fileName].join("/");
534
+ }
535
+ export function jsonText(value) {
536
+ return `${JSON.stringify(value, null, 2)}\n`;
537
+ }
538
+ export function fallbackReport(parsed, phase, artifactPath) {
539
+ return `# Forge ${phase.replace(/_/g, " ")} completion
540
+
541
+ Artifact: ${artifactPath}
542
+ Status: ${optionalString(parsed, "status") ?? "unknown"}
543
+ `;
544
+ }
545
+ export function parseJsonObject(content, label) {
546
+ try {
547
+ return asRecord(JSON.parse(content), label);
548
+ }
549
+ catch (error) {
550
+ throw new Error(`${label} is not valid JSON: ${errorMessage(error)}`, { cause: error });
551
+ }
552
+ }
553
+ export function parseOptionalJsonObject(content, label) {
554
+ if (!content.trim()) {
555
+ return {};
556
+ }
557
+ return parseJsonObject(content, label);
558
+ }
559
+ export function asRecord(value, label) {
560
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
561
+ throw new Error(`${label} must be an object.`);
562
+ }
563
+ return value;
564
+ }
565
+ export function asMutableRecord(value) {
566
+ return value && typeof value === "object" && !Array.isArray(value)
567
+ ? { ...value }
568
+ : {};
569
+ }
570
+ export function requiredString(source, key) {
571
+ const value = source[key];
572
+ if (typeof value !== "string" || value.trim().length === 0) {
573
+ throw new Error(`${key} must be a non-empty string.`);
574
+ }
575
+ return value;
576
+ }
577
+ export function optionalString(source, key) {
578
+ const value = source[key];
579
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
580
+ }
581
+ export function numberOrDefault(value, fallback) {
582
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
583
+ }
584
+ export function stripUndefined(value) {
585
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
586
+ }
587
+ export function safePathSegment(value, label) {
588
+ if (!value.trim() || value.includes("/") || value.includes("\\") || value === "." || value === "..") {
589
+ throw new Error(`${label} is not a safe path segment.`);
590
+ }
591
+ return value;
592
+ }
593
+ export async function readOptionalFile(filePath) {
594
+ return await fs.readFile(filePath, "utf8").catch((error) => {
595
+ if (isNodeError(error) && error.code === "ENOENT") {
596
+ return null;
597
+ }
598
+ throw error;
599
+ });
600
+ }
601
+ export async function writeFileAtomic(filePath, content) {
602
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
603
+ const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
604
+ await fs.writeFile(tempPath, content, "utf8");
605
+ await fs.rename(tempPath, filePath);
606
+ }
607
+ export function stableStringify(value) {
608
+ return JSON.stringify(sortJson(value));
609
+ }
610
+ export function sortJson(value) {
611
+ if (Array.isArray(value)) {
612
+ return value.map(sortJson);
613
+ }
614
+ if (value && typeof value === "object") {
615
+ return Object.fromEntries(Object.entries(value)
616
+ .filter(([, entry]) => entry !== undefined)
617
+ .sort(([left], [right]) => left.localeCompare(right))
618
+ .map(([key, entry]) => [key, sortJson(entry)]));
619
+ }
620
+ return value;
621
+ }
622
+ export function sha256(content) {
623
+ return crypto.createHash("sha256").update(content).digest("hex");
624
+ }
625
+ export function forgeBridgeCliBinPath() {
626
+ return fileURLToPath(new URL("../cli/bin.js", import.meta.url));
627
+ }
628
+ export function tomlString(value) {
629
+ if (value.includes("'''")) {
630
+ throw new Error("Codex MCP configuration values cannot contain three consecutive apostrophes.");
631
+ }
632
+ // Codex config overrides pass through npm's PowerShell shim on Windows.
633
+ // Literal TOML strings survive that boundary; basic-string double quotes do not.
634
+ return `'''${value}'''`;
635
+ }
636
+ export function tomlStringArray(values) {
637
+ return `[${values.map(tomlString).join(", ")}]`;
638
+ }
639
+ export function writeJsonRpc(message) {
640
+ process.stdout.write(`${JSON.stringify(message)}\n`);
641
+ }
642
+ export function rpcError(code, message) {
643
+ return { code, message };
644
+ }
645
+ export function asJsonRpcMessage(value) {
646
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
647
+ return null;
648
+ }
649
+ const message = value;
650
+ if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
651
+ return null;
652
+ }
653
+ const id = message.id;
654
+ return {
655
+ id: typeof id === "string" || typeof id === "number" || id === null ? id : undefined,
656
+ method: message.method,
657
+ params: message.params
658
+ };
659
+ }
660
+ export function errorMessage(error) {
661
+ return error instanceof Error ? error.message : String(error);
662
+ }
663
+ export function isNodeError(error) {
664
+ return error instanceof Error && "code" in error;
665
+ }