@dev-loops/core 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ import {
9
9
  transitionEntry,
10
10
  snapshotEntry,
11
11
  nextReadyEntry,
12
+ findEntry,
12
13
  allDone,
13
14
  RECOVERABLE_FAILURES,
14
15
  appendBugIssue,
@@ -19,7 +20,13 @@ import {
19
20
  boardColumnForLoopState,
20
21
  loadStateColumnMap,
21
22
  } from "./queue-board-sync.mjs";
22
- import { resolveNextUpOrder } from "./queue-board-ordering.mjs";
23
+ import {
24
+ resolveNextUpOrder,
25
+ REASON_NEXT_UP_EMPTY,
26
+ REASON_BOARD_QUERY_ERROR,
27
+ REASON_NEXT_UP_TARGET_MISSING_LOCALLY,
28
+ EMPTY_NEXT_UP_MESSAGE,
29
+ } from "./queue-board-ordering.mjs";
23
30
 
24
31
  export const DEFAULT_QUEUE_DRIVER_OPTIONS = {
25
32
  mergeAuthorized: false,
@@ -92,23 +99,88 @@ export async function runQueue(repoRoot, repo, options = {}) {
92
99
  // (e.g. a configured "Ready for Review") still syncs. (#793 round-1 #1)
93
100
  const lastSyncedColumn = new Map();
94
101
 
95
- // Optional board-aware ordering: fetch Next Up order before processing.
96
- // Fail-open: if the board is unreachable, orderHint stays empty and the
97
- // driver falls back to the existing queue order.
98
- const ordering = opts.useBoardOrdering !== false && !allDone(queue)
102
+ // Next Up is the NORMATIVE, fail-closed pickup source (#1091). When a board is
103
+ // configured, the driver picks ONLY entries whose target is in Next Up, by
104
+ // POSITION ascending; entries absent from Next Up are never auto-picked. It
105
+ // NEVER falls back to Backlog or to the non-board local queue order.
106
+ //
107
+ // Single-issue/PR runs do not reach this gating at all — they run via the
108
+ // dev-loop routing path, not the queue driver — so an explicit --issue/--pr
109
+ // target is inherently unaffected by Next Up.
110
+ const ordering = !allDone(queue)
99
111
  ? await resolveNextUpOrder(repo, repoRoot, opts.env ?? process.env, opts.queueBoardSyncDependencies ?? {})
100
- : { ok: true, order: [], reason: "board ordering disabled or queue idle" };
101
- const orderHint = ordering.ok ? ordering.order : [];
112
+ : { ok: true, configured: false, order: [], reason: "queue idle" };
113
+
114
+ // (b) Board-query ERROR → surface it and stop. Do NOT fall back to Backlog
115
+ // or local order (fail-closed). Distinct from an empty Next Up below.
116
+ if (ordering.ok === false) {
117
+ return {
118
+ ok: false,
119
+ stopped: true,
120
+ reason: REASON_BOARD_QUERY_ERROR,
121
+ message: `Next Up query failed (${ordering.reason}); refusing to fall back to Backlog/local order`,
122
+ error: ordering.reason ?? "board query failed",
123
+ results: [],
124
+ queue,
125
+ ordering,
126
+ };
127
+ }
128
+
129
+ // Board-gated only when a board is configured.
130
+ const boardGated = ordering.configured === true;
131
+ const orderHint = ordering.order;
132
+ const allowedTargets = boardGated ? new Set(orderHint) : null;
133
+
134
+ // (a) Empty Next Up (successful query, zero items) → fail CLOSED: idle/stop
135
+ // with an actionable, machine-readable outcome. Never pull from Backlog.
136
+ if (boardGated && orderHint.length === 0) {
137
+ return {
138
+ ok: true,
139
+ idle: true,
140
+ reason: REASON_NEXT_UP_EMPTY,
141
+ message: EMPTY_NEXT_UP_MESSAGE,
142
+ results: [],
143
+ queue,
144
+ ordering,
145
+ };
146
+ }
147
+
148
+ // (a2) Next Up resolved one or more targets that have NO matching local queue
149
+ // entry (membership reconcile not run/persisted, or the board changed between
150
+ // reconcile and this query). Filtering them out would return a silent empty
151
+ // idle while real Next Up work goes undispatched — so fail CLOSED with an
152
+ // actionable stop instead. Distinct from the genuine empty-Next-Up idle above.
153
+ // Never pull from Backlog. (#1091)
154
+ if (boardGated) {
155
+ const missingTargets = orderHint.filter((t) => !findEntry(queue, t));
156
+ if (missingTargets.length > 0) {
157
+ return {
158
+ ok: false,
159
+ stopped: true,
160
+ reason: REASON_NEXT_UP_TARGET_MISSING_LOCALLY,
161
+ missingTargets,
162
+ message:
163
+ "Next Up contains items with no local queue entry — run membership reconcile / re-add them",
164
+ results: [],
165
+ queue,
166
+ ordering,
167
+ };
168
+ }
169
+ }
102
170
 
103
171
  let autoFiledCount = 0;
104
172
  const results = [];
105
173
  let incomplete = false;
106
174
 
107
175
  while (!allDone(queue)) {
108
- const entry = nextReadyEntry(queue, opts.reDispatchMaxRetries, orderHint);
176
+ const entry = nextReadyEntry(queue, opts.reDispatchMaxRetries, orderHint, allowedTargets);
109
177
  if (!entry) {
178
+ // When board-gated, entries absent from Next Up are intentionally NOT
179
+ // picked (and are not "blocked by deps") — only unfinished Next Up members
180
+ // count toward an incomplete verdict.
110
181
  const remaining = queue.entries.filter(
111
182
  (e) => e.status !== "done" && e.status !== "blocked" && e.status !== "failed"
183
+ && (!allowedTargets || allowedTargets.has(e.target))
112
184
  );
113
185
  if (remaining.length > 0) {
114
186
  incomplete = true;
@@ -183,9 +183,20 @@ function applyOrderHint(ordered, orderHint) {
183
183
  return [...inHint, ...rest];
184
184
  }
185
185
 
186
- export function nextReadyEntry(queue, maxRetries = 1, orderHint = []) {
186
+ /**
187
+ * Pick the next ready entry.
188
+ *
189
+ * @param {object} queue
190
+ * @param {number} maxRetries
191
+ * @param {number[]} orderHint - preferred order (targets sorted to the front).
192
+ * @param {Set<number>|null} allowedTargets - when non-null, ONLY entries whose
193
+ * target is in this set are eligible. Used for board-gated (Next Up)
194
+ * selection (#1091): entries absent from Next Up are never auto-picked.
195
+ */
196
+ export function nextReadyEntry(queue, maxRetries = 1, orderHint = [], allowedTargets = null) {
187
197
  const ordered = topologicalOrder(queue.entries);
188
- const sorted = applyOrderHint(ordered, orderHint);
198
+ const restricted = allowedTargets ? ordered.filter((e) => allowedTargets.has(e.target)) : ordered;
199
+ const sorted = applyOrderHint(restricted, orderHint);
189
200
  for (const entry of sorted) {
190
201
  if (entry.status === "queued" && entryDependenciesSatisfied(queue, entry)) {
191
202
  return entry;
@@ -1,143 +0,0 @@
1
- #!/usr/bin/env node
2
- import { mkdir, writeFile } from "node:fs/promises";
3
- import { join, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { parseReviewThreads, readInput, parseJsonText, formatCliError } from "../src/github/review-threads.mjs";
6
- import { extractDeepPersonaSignals } from "../src/debt/deep-persona-signals.mjs";
7
-
8
- export const USAGE = [
9
- "Usage: capture-deep-persona-signals.mjs --input <path> --pr-number <n> --pr-url <url> [--output-dir <path>]",
10
- "",
11
- "Arguments:",
12
- " --input <path> Path to normalized review-thread JSON (required)",
13
- " --pr-number <n> PR number for metadata (required)",
14
- " --pr-url <url> PR URL for metadata (required)",
15
- " --output-dir <path> Output directory for emitted artifact (default: .pi/debt/signals/)",
16
- ].join("\n");
17
-
18
- /**
19
- * Parse CLI arguments for the capture-deep-persona-signals CLI.
20
- *
21
- * @param {string[]} argv - Argument list (e.g. process.argv.slice(2))
22
- * @returns {{ inputPath: string, prNumber: string, prUrl: string, outputDir: string }}
23
- */
24
- export function parseArgs(argv) {
25
- const args = [...argv];
26
- const options = {
27
- inputPath: undefined,
28
- prNumber: undefined,
29
- prUrl: undefined,
30
- outputDir: ".pi/debt/signals",
31
- };
32
-
33
- while (args.length > 0) {
34
- const token = args.shift();
35
-
36
- switch (token) {
37
- case "--input": {
38
- const value = args.shift();
39
- if (typeof value !== "string" || value.length === 0 || value.startsWith("--")) {
40
- throw Object.assign(new Error("Missing value for --input"), { usage: USAGE });
41
- }
42
- options.inputPath = value;
43
- break;
44
- }
45
- case "--pr-number": {
46
- const value = args.shift();
47
- if (typeof value !== "string" || value.length === 0 || value.startsWith("--")) {
48
- throw Object.assign(new Error("Missing value for --pr-number"), { usage: USAGE });
49
- }
50
- if (!/^\d+$/.test(value)) {
51
- throw Object.assign(new Error(`--pr-number must be a positive integer, got: ${value}`), { usage: USAGE });
52
- }
53
- options.prNumber = value;
54
- break;
55
- }
56
- case "--pr-url": {
57
- const value = args.shift();
58
- if (typeof value !== "string" || value.length === 0 || value.startsWith("--")) {
59
- throw Object.assign(new Error("Missing value for --pr-url"), { usage: USAGE });
60
- }
61
- options.prUrl = value;
62
- break;
63
- }
64
- case "--output-dir": {
65
- const value = args.shift();
66
- if (typeof value !== "string" || value.length === 0 || value.startsWith("--")) {
67
- throw Object.assign(new Error("Missing value for --output-dir"), { usage: USAGE });
68
- }
69
- options.outputDir = value;
70
- break;
71
- }
72
- default:
73
- throw Object.assign(new Error(`Unknown argument: ${token}`), { usage: USAGE });
74
- }
75
- }
76
-
77
- if (!options.inputPath) {
78
- throw Object.assign(new Error("--input is required"), { usage: USAGE });
79
- }
80
- if (!options.prNumber) {
81
- throw Object.assign(new Error("--pr-number is required"), { usage: USAGE });
82
- }
83
- if (!options.prUrl) {
84
- throw Object.assign(new Error("--pr-url is required"), { usage: USAGE });
85
- }
86
-
87
- return /** @type {{ inputPath: string, prNumber: string, prUrl: string, outputDir: string }} */ (options);
88
- }
89
-
90
- /**
91
- * Generate the output filename.
92
- * @param {string} prNumber
93
- * @returns {string}
94
- */
95
- export function outputFilename(prNumber) {
96
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
97
- return `deep-persona-signals-${prNumber}-${ts}.json`;
98
- }
99
-
100
- export async function run(argv = process.argv.slice(2)) {
101
- const options = parseArgs(argv);
102
-
103
- // Read and parse the review-thread input
104
- const rawText = await readInput({ inputPath: options.inputPath });
105
- const parsed = parseReviewThreads(parseJsonText(rawText));
106
-
107
- // Extract deep-persona signals
108
- const signals = extractDeepPersonaSignals(parsed, {
109
- prNumber: options.prNumber,
110
- prUrl: options.prUrl,
111
- });
112
-
113
- // Build artifact envelope
114
- const artifact = {
115
- version: 1,
116
- generatedAt: new Date().toISOString(),
117
- prNumber: Number(options.prNumber),
118
- prUrl: options.prUrl,
119
- source: "pr_review_deep_persona",
120
- signalCount: signals.length,
121
- signals,
122
- };
123
-
124
- // Write to output directory
125
- const outDir = resolve(options.outputDir);
126
- await mkdir(outDir, { recursive: true });
127
- const outPath = join(outDir, outputFilename(options.prNumber));
128
- await writeFile(outPath, JSON.stringify(artifact, null, 2) + "\n", "utf8");
129
-
130
- process.stdout.write(JSON.stringify({ ok: true, outputPath: outPath, signalCount: signals.length }) + "\n");
131
- }
132
-
133
- // Only auto-run when executed directly (not imported)
134
- const scriptPath = fileURLToPath(import.meta.url);
135
- if (process.argv[1] === scriptPath) {
136
- run().catch((error) => {
137
- if (error.usage) {
138
- process.stderr.write(error.usage + "\n\n");
139
- }
140
- process.stderr.write(formatCliError(error) + "\n");
141
- process.exitCode = 1;
142
- });
143
- }
@@ -1,266 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { DebtSignalSchema } from "./debt-signal.mjs";
3
- import { loadDevLoopConfig } from "../config/config.mjs";
4
-
5
- // ============================================================================
6
- // Flag phrase inventory — derived from personas.deep prompt in defaults.yaml
7
- //
8
- // Confidence values use the canonical DebtSignalSchema 0..1 range (0.9 = 90%).
9
- // This matches the schema default: z.number().min(0).max(1).default(1).
10
- // ============================================================================
11
-
12
- /** @type {Array<{ phrase: RegExp, category: string, severity: string, confidence: number }>} */
13
- const FLAG_PATTERNS = [
14
- {
15
- phrase: /(?:crossing|crossed|exceeds?)\s+(?:1000|1,000)\+?\s+lines/i,
16
- category: "file_size",
17
- severity: "high",
18
- confidence: 0.9,
19
- },
20
- {
21
- phrase: /conditionals?\s+bolted\s+onto\s+unrelated\s+paths/i,
22
- category: "spaghetti_branching",
23
- severity: "high",
24
- confidence: 0.9,
25
- },
26
- {
27
- phrase: /\bspaghetti\b/i,
28
- category: "spaghetti_branching",
29
- severity: "high",
30
- confidence: 0.9,
31
- },
32
- {
33
- phrase: /thin\s+wrapper/i,
34
- category: "thin_wrapper",
35
- severity: "medium",
36
- confidence: 0.9,
37
- },
38
- {
39
- phrase: /re-export\s*[- ]?only/i,
40
- category: "thin_wrapper",
41
- severity: "medium",
42
- confidence: 0.9,
43
- },
44
- {
45
- phrase: /identity\s+abstraction/i,
46
- category: "thin_wrapper",
47
- severity: "medium",
48
- confidence: 0.9,
49
- },
50
- {
51
- phrase: /feature\s+logic\s+leaking\s+into/i,
52
- category: "leaky_feature_logic",
53
- severity: "high",
54
- confidence: 0.9,
55
- },
56
- {
57
- phrase: /leaking\s+into\s+shared/i,
58
- category: "leaky_feature_logic",
59
- severity: "high",
60
- confidence: 0.9,
61
- },
62
- {
63
- phrase: /cast[- ]?heavy/i,
64
- category: "weak_contract",
65
- severity: "medium",
66
- confidence: 0.9,
67
- },
68
- {
69
- phrase: /optionality[- ]?heavy/i,
70
- category: "weak_contract",
71
- severity: "medium",
72
- confidence: 0.9,
73
- },
74
- {
75
- phrase: /any[- ]?typed\s+contract/i,
76
- category: "weak_contract",
77
- severity: "medium",
78
- confidence: 0.9,
79
- },
80
- {
81
- phrase: /code\s+judo/i,
82
- category: "simplification_opportunity",
83
- severity: "medium",
84
- confidence: 0.9,
85
- },
86
- {
87
- phrase: /prefer\s+deletion\s+over\s+addition/i,
88
- category: "simplification_opportunity",
89
- severity: "medium",
90
- confidence: 0.9,
91
- },
92
- ];
93
-
94
- const FILE_PATH_RE = /[\w/\-.]+\.(?:m?js|ts|tsx|jsx|mjs)/i;
95
-
96
- // ============================================================================
97
- // Helpers
98
- // ============================================================================
99
-
100
- /**
101
- * Match a comment body against known deep-persona flag phrases.
102
- * Returns the match when a specific pattern matches, or null when no
103
- * patterns match the body.
104
- *
105
- * @param {string} body
106
- * @returns {{ category: string, severity: string, confidence: number, matchedPhrase: string|null }|null}
107
- */
108
- function matchDeepPersonaFlags(body) {
109
- for (const pattern of FLAG_PATTERNS) {
110
- if (pattern.phrase.test(body)) {
111
- return {
112
- category: pattern.category,
113
- severity: pattern.severity,
114
- confidence: pattern.confidence,
115
- matchedPhrase: pattern.phrase.source,
116
- };
117
- }
118
- }
119
-
120
- return null;
121
- }
122
-
123
- /**
124
- * Extract a file path from a comment body, or return an empty string if none found.
125
- *
126
- * @param {string} body
127
- * @returns {string}
128
- */
129
- function extractFilePath(body) {
130
- const match = body.match(FILE_PATH_RE);
131
- return match ? match[0] : "";
132
- }
133
-
134
- /**
135
- * Build a debt_signal object from a matched deep-persona comment.
136
- *
137
- * @param {object} comment - Normalized comment from parseReviewThreads output
138
- * @param {string} category - Inferred category
139
- * @param {string} severity - Severity hint
140
- * @param {number} confidence - Confidence score (0..1)
141
- * @param {string|null} matchedPhrase - The regex source that matched
142
- * @param {{ prNumber: string|number, prUrl: string }} prMeta
143
- * @returns {object}
144
- */
145
- function buildDebtSignal(comment, category, severity, confidence, matchedPhrase, prMeta) {
146
- const filePath = extractFilePath(comment.body);
147
-
148
- return {
149
- id: randomUUID(),
150
- sourceType: "pr_review_deep_persona",
151
- signalKind: category,
152
- location: filePath ? { filePath } : {},
153
- severityHint: severity,
154
- timestamp: new Date().toISOString(),
155
- confidence,
156
- rawPayload: {
157
- description: comment.body,
158
- metadata: {
159
- prNumber: String(prMeta.prNumber),
160
- prUrl: prMeta.prUrl,
161
- commentId: comment.id,
162
- threadId: comment.threadId,
163
- isResolved: comment.isResolved ?? false,
164
- category,
165
- matchedPhrase,
166
- },
167
- },
168
- };
169
- }
170
-
171
- // ============================================================================
172
- // Public API
173
- // ============================================================================
174
-
175
- /**
176
- * Extract deep-persona debt_signal artifacts from normalized review-thread JSON.
177
- *
178
- * Accepts the output of `parseReviewThreads()` (the `comments` array with
179
- * normalized `{ id, threadId, author, body, isActionable }` entries).
180
- *
181
- * Filters to only bot-authored comments that match known deep-persona flag
182
- * phrases. Bots are identified by `author.isBot === true` or `author.type === "Bot"`.
183
- *
184
- * @param {{ comments: Array<{ id: string, threadId: string, author: { login: string, type: string, isBot: boolean }, body: string, isActionable?: boolean, isResolved?: boolean }>, threads?: Array<{ id: string, isResolved: boolean }> }} parsedOutput - parseReviewThreads() output
185
- * @param {{ prNumber: string|number, prUrl: string }} prMeta
186
- * @returns {Array<object>} Array of debt_signal objects compatible with DebtSignalSchema
187
- */
188
- export function extractDeepPersonaSignals(parsedOutput, prMeta) {
189
- if (!parsedOutput || !Array.isArray(parsedOutput.comments)) {
190
- throw new Error("Invalid parsed output: expected { comments: [...] } from parseReviewThreads()");
191
- }
192
-
193
- // Build a thread-id → isResolved map for fast lookup
194
- const threadResolved = new Map();
195
- if (Array.isArray(parsedOutput.threads)) {
196
- for (const thread of parsedOutput.threads) {
197
- threadResolved.set(thread.id, Boolean(thread.isResolved));
198
- }
199
- }
200
-
201
- const signals = [];
202
-
203
- for (const comment of parsedOutput.comments) {
204
- // Only process bot-authored comments (all Copilot personas emit as bots)
205
- if (!comment.author || (!comment.author.isBot && comment.author.type !== "Bot")) {
206
- continue;
207
- }
208
-
209
- if (!comment.body || comment.body.trim().length === 0) {
210
- continue;
211
- }
212
-
213
- const match = matchDeepPersonaFlags(comment.body);
214
- if (!match) {
215
- continue;
216
- }
217
-
218
- // Enrich comment with isResolved from thread data
219
- const isResolved = threadResolved.has(comment.threadId)
220
- ? threadResolved.get(comment.threadId)
221
- : (comment.isResolved ?? false);
222
-
223
- const signal = buildDebtSignal(
224
- { ...comment, isResolved },
225
- match.category,
226
- match.severity,
227
- match.confidence,
228
- match.matchedPhrase,
229
- prMeta,
230
- );
231
-
232
- // Validate against canonical schema — throw on regression
233
- signals.push(DebtSignalSchema.parse(signal));
234
- }
235
-
236
- return signals;
237
- }
238
-
239
- /**
240
- * Return the known deep-persona flag phrase regex sources for inspection.
241
- *
242
- * @returns {Array<string>}
243
- */
244
- export function getDeepPersonaFlagPhrases() {
245
- return FLAG_PATTERNS.map((p) => p.phrase.source);
246
- }
247
-
248
- /**
249
- * Verify that all known deep-persona flag phrase regex patterns match
250
- * the loaded deep persona prompt text. Returns an array of regex sources
251
- * whose patterns did not find any match in the prompt (empty = all match).
252
- *
253
- * @returns {Promise<Array<string>>}
254
- */
255
- export async function verifyPromptStability() {
256
- const { config, errors } = await loadDevLoopConfig();
257
- if (errors.length > 0) {
258
- throw new Error("Cannot verify prompt stability: config load errors: " +
259
- errors.map(e => e.message).join("; "));
260
- }
261
- const deepPrompt = config?.personas?.deep?.prompt ?? "";
262
-
263
- return FLAG_PATTERNS
264
- .filter((p) => !p.phrase.test(deepPrompt))
265
- .map((p) => p.phrase.source);
266
- }
@@ -1,102 +0,0 @@
1
- import { execFile } from "node:child_process";
2
-
3
- import { createExtensionHarnessAdapter } from "./extension-adapter.mjs";
4
-
5
- /**
6
- * Create the Claude Code extension-surface adapter.
7
- *
8
- * Implements the same `ExtensionHarnessAdapter` interface as the Pi adapter so
9
- * `executeDevLoopsCommand` and the extension wiring run unchanged under Claude.
10
- *
11
- * - `exec` shells out via `bash -lc` (Claude has no native exec API in core).
12
- * - lifecycle `on(...)` registrations are stored in `listeners` for hook-driven
13
- * dispatch (wired in CA4 / #773); they are not auto-fired here.
14
- * - `registerCommand(...)` registrations are stored in `commands`.
15
- * - Claude core has no interactive widget/status surface, so the default
16
- * `HarnessContext` reports `hasUI: false` and routes `ui` calls to a sink.
17
- *
18
- * @param {Object} [options]
19
- * @param {string} [options.cwd] - Default cwd for exec and contexts (default: process.cwd()).
20
- * @param {NodeJS.ProcessEnv} [options.env] - Env for exec (default: process.env).
21
- * @param {(message: string, level: string) => void} [options.onNotify] - Optional sink for
22
- * `ui.notify` (e.g. console). Defaults to a no-op.
23
- * @returns {import("./extension-adapter.mjs").ExtensionHarnessAdapter & {
24
- * listeners: Map<string, Function>,
25
- * commands: Map<string, import("./extension-adapter.mjs").HarnessCommandConfig>,
26
- * makeContext: (overrides?: {cwd?: string}) => import("./extension-adapter.mjs").HarnessContext,
27
- * }}
28
- */
29
- export function createClaudeExtensionAdapter({
30
- cwd = process.cwd(),
31
- env = process.env,
32
- onNotify = () => {},
33
- } = {}) {
34
- const listeners = new Map();
35
- const commands = new Map();
36
-
37
- function exec(command, options = {}) {
38
- return new Promise((resolve) => {
39
- execFile(
40
- "bash",
41
- ["-lc", command],
42
- {
43
- cwd: options.cwd ?? cwd,
44
- env,
45
- timeout: options.timeout ?? 0,
46
- encoding: "utf8",
47
- maxBuffer: 64 * 1024 * 1024,
48
- },
49
- (error, stdout, stderr) => {
50
- if (error) {
51
- // Match the documented HarnessExecResult contract: `code` is undefined when
52
- // the process was killed (e.g. timeout). Consumers branch on `killed` first.
53
- const killed = Boolean(error.killed);
54
- resolve({
55
- code: killed ? undefined : (typeof error.code === "number" ? error.code : 1),
56
- stdout: stdout ?? "",
57
- stderr: stderr ?? "",
58
- killed,
59
- });
60
- return;
61
- }
62
- resolve({ code: 0, stdout: stdout ?? "", stderr: stderr ?? "", killed: false });
63
- },
64
- );
65
- });
66
- }
67
-
68
- function makeContext({ cwd: ctxCwd } = {}) {
69
- return {
70
- cwd: ctxCwd ?? cwd,
71
- hasUI: false,
72
- ui: {
73
- notify(message, level = "info") {
74
- onNotify(message, level);
75
- },
76
- setWidget() {},
77
- setStatus() {},
78
- },
79
- };
80
- }
81
-
82
- const adapter = createExtensionHarnessAdapter({
83
- exec,
84
- on(event, handler) {
85
- listeners.set(event, handler);
86
- },
87
- registerCommand(name, config) {
88
- commands.set(name, config);
89
- },
90
- });
91
-
92
- // Freeze the composite so the validated interface guarantee from the factory carries
93
- // through to the returned object (which also exposes the #773 dispatch registries).
94
- return Object.freeze({
95
- exec: adapter.exec,
96
- on: adapter.on,
97
- registerCommand: adapter.registerCommand,
98
- listeners,
99
- commands,
100
- makeContext,
101
- });
102
- }