@dev-loops/core 0.6.0 → 0.7.2

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.
@@ -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
- }
@@ -1,95 +0,0 @@
1
- import { z } from "zod";
2
-
3
- // ---------------------------------------------------------------------------
4
- // AC/DoD matrix item — one row in the refinement coverage matrix
5
- // ---------------------------------------------------------------------------
6
-
7
- export const AC_DOD_ITEM_TYPE = Object.freeze({
8
- AC: "AC",
9
- DOD: "DoD",
10
- NON_GOAL: "Non-goal",
11
- });
12
-
13
- export const AC_DOD_ITEM_STATUS = Object.freeze({
14
- MET: "Met",
15
- PARTIAL: "Partial",
16
- UNMET: "Unmet",
17
- UNVERIFIED: "Unverified",
18
- });
19
-
20
- /**
21
- * A single row in the AC/DoD coverage matrix.
22
- * Matches the refiner persona's required table output shape.
23
- */
24
- export const AcDodMatrixItemSchema = z.strictObject({
25
- /** Exact item text from the source issue/plan/spec */
26
- item: z.string().trim().min(1),
27
- /** Type classification */
28
- type: z.enum(Object.values(AC_DOD_ITEM_TYPE)),
29
- /** Verification status */
30
- status: z.enum(Object.values(AC_DOD_ITEM_STATUS)),
31
- /** Reference to supporting evidence (file, test, doc path, or URL) */
32
- evidence: z.string(),
33
- /** Additional context or caveats */
34
- notes: z.string(),
35
- });
36
-
37
- /**
38
- * The full AC/DoD/Non-goal coverage matrix.
39
- * Emitted by the refiner during issue refinement, consumed by implementation
40
- * agents via the handoff envelope as a structured contract.
41
- */
42
- export const AcDodMatrixSchema = z.strictObject({
43
- /** Schema identifier for dispatch and validation */
44
- schema: z.literal("ac-dod-matrix/v1"),
45
- /** Ordered list of matrix items */
46
- items: z.array(AcDodMatrixItemSchema).min(1),
47
- /** Source reference (issue URL, plan-doc path, etc.) */
48
- source: z.string().trim().min(1).optional(),
49
- /** ISO 8601 timestamp of matrix generation */
50
- generatedAt: z.string().datetime(),
51
- /**
52
- * True when every item has status "Met" — the contract is fully satisfied.
53
- * Implementation agents use this to gate merge-readiness.
54
- */
55
- isComplete: z.boolean(),
56
- }).refine(
57
- (data) => isMatrixComplete(data) === data.isComplete,
58
- {
59
- message: "isComplete must be true when (and only when) every item has status 'Met'",
60
- path: ["isComplete"],
61
- }
62
- );
63
-
64
- // ---------------------------------------------------------------------------
65
- // Convenience helpers
66
- // ---------------------------------------------------------------------------
67
-
68
- /**
69
- * Check whether a matrix is fully satisfied (all items Met).
70
- * Pure function — does not require a parsed Zod result.
71
- */
72
- export function isMatrixComplete(matrix) {
73
- if (!matrix || !Array.isArray(matrix.items) || matrix.items.length === 0) {
74
- return false;
75
- }
76
- return matrix.items.every((item) => item.status === AC_DOD_ITEM_STATUS.MET);
77
- }
78
-
79
- /**
80
- * Collect all items that are not "Met" — the outstanding work contract.
81
- */
82
- export function outstandingItems(matrix) {
83
- if (!matrix || !Array.isArray(matrix.items)) {
84
- return [];
85
- }
86
- return matrix.items.filter((item) => item.status !== AC_DOD_ITEM_STATUS.MET);
87
- }
88
-
89
- /**
90
- * Validate raw data against the AC/DoD matrix schema.
91
- * Returns a Zod safeParse result.
92
- */
93
- export function validateAcDodMatrix(data) {
94
- return AcDodMatrixSchema.safeParse(data);
95
- }